mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-25 14:05:03 +00:00
feat: Add Project Descriptions (#2774)
* feat: ✨ initial backend project description
This commit is contained in:
@ -0,0 +1,23 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description");
|
||||
|
||||
if (!hasProjectDescription) {
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.string("description");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasProjectDescription = await knex.schema.hasColumn(TableName.Project, "description");
|
||||
|
||||
if (hasProjectDescription) {
|
||||
await knex.schema.alterTable(TableName.Project, (t) => {
|
||||
t.dropColumn("description");
|
||||
});
|
||||
}
|
||||
}
|
@ -12,7 +12,7 @@ import { TImmutableDBKeys } from "./models";
|
||||
export const KmsRootConfigSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
encryptedRootKey: zodBuffer,
|
||||
encryptionStrategy: z.string(),
|
||||
encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(),
|
||||
createdAt: z.date(),
|
||||
updatedAt: z.date()
|
||||
});
|
||||
|
@ -23,7 +23,8 @@ export const ProjectsSchema = z.object({
|
||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||
auditLogsRetentionDays: z.number().nullable().optional(),
|
||||
kmsSecretManagerKeyId: z.string().uuid().nullable().optional(),
|
||||
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional()
|
||||
kmsSecretManagerEncryptedDataKey: zodBuffer.nullable().optional(),
|
||||
description: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||
|
@ -391,6 +391,7 @@ export const PROJECTS = {
|
||||
CREATE: {
|
||||
organizationSlug: "The slug of the organization to create the project in.",
|
||||
projectName: "The name of the project to create.",
|
||||
projectDescription: "An optional description label for the project.",
|
||||
slug: "An optional slug for the project.",
|
||||
template: "The name of the project template, if specified, to apply to this project."
|
||||
},
|
||||
@ -403,6 +404,7 @@ export const PROJECTS = {
|
||||
UPDATE: {
|
||||
workspaceId: "The ID of the project to update.",
|
||||
name: "The new name of the project.",
|
||||
projectDescription: "An optional description label for the project.",
|
||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
||||
},
|
||||
GET_KEY: {
|
||||
|
@ -212,6 +212,7 @@ export const SanitizedAuditLogStreamSchema = z.object({
|
||||
export const SanitizedProjectSchema = ProjectsSchema.pick({
|
||||
id: true,
|
||||
name: true,
|
||||
description: true,
|
||||
slug: true,
|
||||
autoCapitalization: true,
|
||||
orgId: true,
|
||||
|
@ -296,6 +296,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||
.optional()
|
||||
.describe(PROJECTS.UPDATE.name),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, { message: "Description must be 256 or fewer characters" })
|
||||
.optional()
|
||||
.describe(PROJECTS.UPDATE.projectDescription),
|
||||
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
||||
}),
|
||||
response: {
|
||||
@ -313,6 +319,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
update: {
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
autoCapitalization: req.body.autoCapitalization
|
||||
},
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
|
@ -161,6 +161,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
],
|
||||
body: z.object({
|
||||
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
|
||||
projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription),
|
||||
slug: z
|
||||
.string()
|
||||
.min(5)
|
||||
@ -194,6 +195,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
actorOrgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
workspaceName: req.body.projectName,
|
||||
workspaceDescription: req.body.projectDescription,
|
||||
slug: req.body.slug,
|
||||
kmsKeyId: req.body.kmsKeyId,
|
||||
template: req.body.template
|
||||
@ -312,8 +314,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
slug: slugSchema.describe("The slug of the project to update.")
|
||||
}),
|
||||
body: z.object({
|
||||
name: z.string().trim().optional().describe("The new name of the project."),
|
||||
autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.")
|
||||
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
|
||||
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
|
||||
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
||||
}),
|
||||
response: {
|
||||
200: SanitizedProjectSchema
|
||||
@ -330,6 +333,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
update: {
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
autoCapitalization: req.body.autoCapitalization
|
||||
},
|
||||
actorId: req.permission.id,
|
||||
|
@ -149,6 +149,7 @@ export const projectServiceFactory = ({
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
workspaceName,
|
||||
workspaceDescription,
|
||||
slug: projectSlug,
|
||||
kmsKeyId,
|
||||
tx: trx,
|
||||
@ -206,6 +207,7 @@ export const projectServiceFactory = ({
|
||||
const project = await projectDAL.create(
|
||||
{
|
||||
name: workspaceName,
|
||||
description: workspaceDescription,
|
||||
orgId: organization.id,
|
||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||
kmsSecretManagerKeyId: kmsKeyId,
|
||||
@ -496,6 +498,7 @@ export const projectServiceFactory = ({
|
||||
|
||||
const updatedProject = await projectDAL.updateById(project.id, {
|
||||
name: update.name,
|
||||
description: update.description,
|
||||
autoCapitalization: update.autoCapitalization
|
||||
});
|
||||
return updatedProject;
|
||||
|
@ -29,6 +29,7 @@ export type TCreateProjectDTO = {
|
||||
actorId: string;
|
||||
actorOrgId?: string;
|
||||
workspaceName: string;
|
||||
workspaceDescription?: string;
|
||||
slug?: string;
|
||||
kmsKeyId?: string;
|
||||
createDefaultEnvs?: boolean;
|
||||
@ -69,6 +70,7 @@ export type TUpdateProjectDTO = {
|
||||
filter: Filter;
|
||||
update: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
autoCapitalization?: boolean;
|
||||
};
|
||||
} & Omit<TProjectPermission, "projectId">;
|
||||
|
328
frontend/src/components/v2/projects/NewProjectModal.tsx
Normal file
328
frontend/src/components/v2/projects/NewProjectModal.tsx
Normal file
@ -0,0 +1,328 @@
|
||||
import { FC, useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/router";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import z from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useOrgPermission,
|
||||
useSubscription,
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useGetExternalKmsList
|
||||
} from "@app/hooks/api";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description too long, max length is 256 characters")
|
||||
.optional(),
|
||||
addMembers: z.boolean(),
|
||||
kmsKeyId: z.string(),
|
||||
template: z.string()
|
||||
});
|
||||
|
||||
type TAddProjectFormData = z.infer<typeof formSchema>;
|
||||
|
||||
interface NewProjectModalProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
}
|
||||
|
||||
type NewProjectFormProps = Pick<NewProjectModalProps, "onOpenChange">;
|
||||
|
||||
const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
|
||||
const router = useRouter();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { permission } = useOrgPermission();
|
||||
const { user } = useUser();
|
||||
const createWs = useCreateWorkspace();
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const canReadProjectTemplates = permission.can(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.ProjectTemplates
|
||||
);
|
||||
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||
});
|
||||
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
|
||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
||||
});
|
||||
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting, errors }
|
||||
} = useForm<TAddProjectFormData>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
kmsKeyId: INTERNAL_KMS_KEY_ID,
|
||||
template: InfisicalProjectTemplate.Default
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (Object.keys(errors).length > 0) {
|
||||
console.log("Current form errors:", errors);
|
||||
}
|
||||
}, [errors]);
|
||||
|
||||
const onCreateProject = async ({
|
||||
name,
|
||||
description,
|
||||
addMembers,
|
||||
kmsKeyId,
|
||||
template
|
||||
}: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg) return;
|
||||
if (!user) return;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
project: { id: newProjectId }
|
||||
}
|
||||
} = await createWs.mutateAsync({
|
||||
projectName: name,
|
||||
projectDescription: description,
|
||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||
template
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||
await addUsersToProject.mutateAsync({
|
||||
usernames: orgUsers
|
||||
.filter(
|
||||
(member) => member.user.username !== user.username && member.status === "accepted"
|
||||
)
|
||||
.map((member) => member.user.username),
|
||||
projectId: newProjectId,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||
|
||||
createNotification({ text: "Project created", type: "success" });
|
||||
reset();
|
||||
onOpenChange(false);
|
||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create project", type: "error" });
|
||||
}
|
||||
};
|
||||
const onSubmit = handleSubmit((data) => {
|
||||
return onCreateProject(data);
|
||||
});
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<Input {...field} placeholder="Type your project name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="description"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Description"
|
||||
isError={Boolean(error)}
|
||||
isOptional
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Project description"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar w-full !resize-none bg-mineshaft-900"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="template"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Read}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<FormControl
|
||||
label="Project Template"
|
||||
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
|
||||
tooltipText={
|
||||
<>
|
||||
<p>
|
||||
Create this project from a template to provision it with custom environments
|
||||
and roles.
|
||||
</p>
|
||||
{subscription && !subscription.projectTemplates && (
|
||||
<p className="pt-2">Project templates are a paid feature.</p>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
defaultValue={InfisicalProjectTemplate.Default}
|
||||
placeholder={InfisicalProjectTemplate.Default}
|
||||
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-full"
|
||||
>
|
||||
{projectTemplates.length
|
||||
? projectTemplates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.name}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))
|
||||
: Object.values(InfisicalProjectTemplate).map((template) => (
|
||||
<SelectItem key={template} value={template}>
|
||||
{template}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 pl-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="addMembers"
|
||||
defaultValue={false}
|
||||
render={({ field: { onBlur, value, onChange } }) => (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Checkbox
|
||||
id="add-project-layout"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
isDisabled={!isAllowed}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
Add all members of my organization to this project
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-14 flex">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
<div className="order-1 ml-3">Advanced Settings</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl errorText={error?.message} isError={Boolean(error)} label="KMS">
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="mb-12 w-full bg-mineshaft-600"
|
||||
>
|
||||
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||
Default Infisical KMS
|
||||
</SelectItem>
|
||||
{externalKmsList?.map((kms) => (
|
||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||
{kms.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="kmsKeyId"
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
||||
<ModalClose>
|
||||
<Button colorSchema="secondary" variant="plain" className="py-2">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
<Button isDisabled={isSubmitting} isLoading={isSubmitting} className="ml-4" type="submit">
|
||||
Create Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export const NewProjectModal: FC<NewProjectModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
title="Create a new project"
|
||||
subTitle="This project will contain your secrets and configurations."
|
||||
>
|
||||
<NewProjectForm onOpenChange={onOpenChange} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
1
frontend/src/components/v2/projects/index.tsx
Normal file
1
frontend/src/components/v2/projects/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { NewProjectModal } from "./NewProjectModal";
|
@ -34,9 +34,9 @@ export type {
|
||||
CreateWorkspaceDTO,
|
||||
DeleteEnvironmentDTO,
|
||||
DeleteWorkspaceDTO,
|
||||
RenameWorkspaceDTO,
|
||||
ToggleAutoCapitalizationDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
UpdateProjectDTO,
|
||||
Workspace,
|
||||
WorkspaceEnv,
|
||||
WorkspaceTag
|
||||
|
@ -33,10 +33,11 @@ export {
|
||||
useListWorkspacePkiAlerts,
|
||||
useListWorkspacePkiCollections,
|
||||
useNameWorkspaceSecrets,
|
||||
useRenameWorkspace,
|
||||
useToggleAutoCapitalization,
|
||||
useUpdateIdentityWorkspaceRole,
|
||||
useUpdateProject,
|
||||
useUpdateUserWorkspaceRole,
|
||||
useUpdateWsEnvironment,
|
||||
useUpgradeProject} from "./queries";
|
||||
useUpgradeProject
|
||||
} from "./queries";
|
||||
export { workspaceKeys } from "./query-keys";
|
||||
|
@ -26,7 +26,6 @@ import {
|
||||
DeleteWorkspaceDTO,
|
||||
NameWorkspaceSecretsDTO,
|
||||
ProjectIdentityOrderBy,
|
||||
RenameWorkspaceDTO,
|
||||
TGetUpgradeProjectStatusDTO,
|
||||
TListProjectIdentitiesDTO,
|
||||
ToggleAutoCapitalizationDTO,
|
||||
@ -35,6 +34,7 @@ import {
|
||||
UpdateAuditLogsRetentionDTO,
|
||||
UpdateEnvironmentDTO,
|
||||
UpdatePitVersionLimitDTO,
|
||||
UpdateProjectDTO,
|
||||
Workspace
|
||||
} from "./types";
|
||||
|
||||
@ -208,19 +208,26 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
||||
|
||||
export const createWorkspace = ({
|
||||
projectName,
|
||||
projectDescription,
|
||||
kmsKeyId,
|
||||
template
|
||||
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
||||
return apiRequest.post("/api/v2/workspace", { projectName, kmsKeyId, template });
|
||||
return apiRequest.post("/api/v2/workspace", {
|
||||
projectName,
|
||||
projectDescription,
|
||||
kmsKeyId,
|
||||
template
|
||||
});
|
||||
};
|
||||
|
||||
export const useCreateWorkspace = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||
mutationFn: async ({ projectName, kmsKeyId, template }) =>
|
||||
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
|
||||
createWorkspace({
|
||||
projectName,
|
||||
projectDescription,
|
||||
kmsKeyId,
|
||||
template
|
||||
}),
|
||||
@ -230,12 +237,15 @@ export const useCreateWorkspace = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export const useRenameWorkspace = () => {
|
||||
export const useUpdateProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, RenameWorkspaceDTO>({
|
||||
mutationFn: ({ workspaceID, newWorkspaceName }) => {
|
||||
return apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName });
|
||||
return useMutation<{}, {}, UpdateProjectDTO>({
|
||||
mutationFn: ({ projectID, newProjectName, newProjectDescription }) => {
|
||||
return apiRequest.patch(`/api/v1/workspace/${projectID}`, {
|
||||
name: newProjectName,
|
||||
description: newProjectDescription
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||
|
@ -16,6 +16,7 @@ export type Workspace = {
|
||||
__v: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
orgId: string;
|
||||
version: ProjectVersion;
|
||||
upgradeStatus: string | null;
|
||||
@ -26,7 +27,6 @@ export type Workspace = {
|
||||
auditLogsRetentionDays: number;
|
||||
slug: string;
|
||||
createdAt: string;
|
||||
|
||||
roles?: TProjectRole[];
|
||||
};
|
||||
|
||||
@ -56,11 +56,17 @@ export type TGetUpgradeProjectStatusDTO = {
|
||||
// mutation dto
|
||||
export type CreateWorkspaceDTO = {
|
||||
projectName: string;
|
||||
projectDescription?: string;
|
||||
kmsKeyId?: string;
|
||||
template?: string;
|
||||
};
|
||||
|
||||
export type RenameWorkspaceDTO = { workspaceID: string; newWorkspaceName: string };
|
||||
export type UpdateProjectDTO = {
|
||||
projectID: string;
|
||||
newProjectName: string;
|
||||
newProjectDescription?: string;
|
||||
};
|
||||
|
||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||
|
@ -6,7 +6,6 @@
|
||||
/* eslint-disable func-names */
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@ -21,66 +20,48 @@ import {
|
||||
faEnvelope,
|
||||
faInfinity,
|
||||
faInfo,
|
||||
faInfoCircle,
|
||||
faMobile,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faStar as faSolidStar
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
FormControl,
|
||||
Input,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { NewProjectModal } from "@app/components/v2/projects/NewProjectModal";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useOrgPermission,
|
||||
useSubscription,
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { usePopUp, useToggle } from "@app/hooks";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useGetAccessRequestsCount,
|
||||
useGetExternalKmsList,
|
||||
useGetOrgTrialUrl,
|
||||
useGetSecretApprovalRequestCount,
|
||||
useLogoutUser,
|
||||
useSelectOrganization
|
||||
} from "@app/hooks/api";
|
||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { Workspace } from "@app/hooks/api/types";
|
||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
||||
@ -119,20 +100,6 @@ const supportOptions = [
|
||||
]
|
||||
];
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup
|
||||
.string()
|
||||
.required()
|
||||
.label("Project Name")
|
||||
.trim()
|
||||
.max(64, "Too long, maximum length is 64 characters"),
|
||||
addMembers: yup.bool().required().label("Add Members"),
|
||||
kmsKeyId: yup.string().label("KMS Key ID"),
|
||||
template: yup.string().label("Project Template Name")
|
||||
});
|
||||
|
||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const AppLayout = ({ children }: LayoutProps) => {
|
||||
const router = useRouter();
|
||||
|
||||
@ -165,10 +132,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
|
||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||
const { permission } = useOrgPermission();
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
|
||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
||||
});
|
||||
|
||||
const pendingRequestsCount = useMemo(() => {
|
||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||
@ -178,27 +141,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||
: true;
|
||||
|
||||
const createWs = useCreateWorkspace();
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
|
||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
"upgradePlan",
|
||||
"createOrg"
|
||||
] as const);
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
handleSubmit
|
||||
} = useForm<TAddProjectFormData>({
|
||||
resolver: yupResolver(formSchema),
|
||||
defaultValues: {
|
||||
kmsKeyId: INTERNAL_KMS_KEY_ID
|
||||
}
|
||||
});
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -281,58 +230,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
putUserInOrg();
|
||||
}, [router.query.id]);
|
||||
|
||||
const canReadProjectTemplates = permission.can(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.ProjectTemplates
|
||||
);
|
||||
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||
});
|
||||
|
||||
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg) return;
|
||||
if (!user) return;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
project: { id: newProjectId }
|
||||
}
|
||||
} = await createWs.mutateAsync({
|
||||
projectName: name,
|
||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||
template
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||
await addUsersToProject.mutateAsync({
|
||||
usernames: orgUsers
|
||||
.filter(
|
||||
(member) => member.user.username !== user.username && member.status === "accepted"
|
||||
)
|
||||
.map((member) => member.user.username),
|
||||
projectId: newProjectId,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||
|
||||
createNotification({ text: "Project created", type: "success" });
|
||||
handlePopUpClose("addNewWs");
|
||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create project", type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const addProjectToFavorites = async (projectId: string) => {
|
||||
try {
|
||||
if (currentOrg?.id) {
|
||||
@ -916,176 +813,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
<Modal
|
||||
<NewProjectModal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isModalOpen) => {
|
||||
handlePopUpToggle("addNewWs", isModalOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create a new project"
|
||||
subTitle="This project will contain your secrets and configurations."
|
||||
>
|
||||
<form onSubmit={handleSubmit(onCreateProject)}>
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<Input {...field} placeholder="Type your project name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="template"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Read}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<FormControl
|
||||
label="Project Template"
|
||||
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
|
||||
tooltipText={
|
||||
<>
|
||||
<p>
|
||||
Create this project from a template to provision it with custom
|
||||
environments and roles.
|
||||
</p>
|
||||
{subscription && !subscription.projectTemplates && (
|
||||
<p className="pt-2">Project templates are a paid feature.</p>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
defaultValue={InfisicalProjectTemplate.Default}
|
||||
placeholder={InfisicalProjectTemplate.Default}
|
||||
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-44"
|
||||
>
|
||||
{projectTemplates.length
|
||||
? projectTemplates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.name}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))
|
||||
: Object.values(InfisicalProjectTemplate).map((template) => (
|
||||
<SelectItem key={template} value={template}>
|
||||
{template}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 pl-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="addMembers"
|
||||
defaultValue={false}
|
||||
render={({ field: { onBlur, value, onChange } }) => (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Read}
|
||||
a={OrgPermissionSubjects.Member}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Checkbox
|
||||
id="add-project-layout"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
isDisabled={!isAllowed}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
Add all members of my organization to this project
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-14 flex">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem
|
||||
value="advance-settings"
|
||||
className="data-[state=open]:border-none"
|
||||
>
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
<div className="order-1 ml-3">Advanced Settings</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="KMS"
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="mb-12 w-full bg-mineshaft-600"
|
||||
>
|
||||
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||
Default Infisical KMS
|
||||
</SelectItem>
|
||||
{externalKmsList?.map((kms) => (
|
||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||
{kms.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="kmsKeyId"
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => handlePopUpClose("addNewWs")}
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
className="py-2"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="ml-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|
@ -1,7 +1,6 @@
|
||||
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
||||
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
@ -19,7 +18,6 @@ import {
|
||||
faExclamationCircle,
|
||||
faFileShield,
|
||||
faHandPeace,
|
||||
faInfoCircle,
|
||||
faList,
|
||||
faMagnifyingGlass,
|
||||
faNetworkWired,
|
||||
@ -29,48 +27,22 @@ import {
|
||||
faUserPlus
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as Tabs from "@radix-ui/react-tabs";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { OrgPermissionCan } from "@app/components/permissions";
|
||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem,
|
||||
Skeleton,
|
||||
UpgradePlanModal
|
||||
} from "@app/components/v2";
|
||||
import { Button, IconButton, Input, Skeleton, UpgradePlanModal } from "@app/components/v2";
|
||||
import { NewProjectModal } from "@app/components/v2/projects";
|
||||
import {
|
||||
OrgPermissionActions,
|
||||
OrgPermissionSubjects,
|
||||
useOrganization,
|
||||
useOrgPermission,
|
||||
useSubscription,
|
||||
useUser,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useGetExternalKmsList,
|
||||
useRegisterUserAction
|
||||
} from "@app/hooks/api";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { useRegisterUserAction } from "@app/hooks/api";
|
||||
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||
import { Workspace } from "@app/hooks/api/types";
|
||||
@ -476,20 +448,6 @@ const LearningItemSquare = ({
|
||||
);
|
||||
};
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup
|
||||
.string()
|
||||
.required()
|
||||
.label("Project Name")
|
||||
.trim()
|
||||
.max(64, "Too long, maximum length is 64 characters"),
|
||||
addMembers: yup.bool().required().label("Add Members"),
|
||||
kmsKeyId: yup.string().label("KMS Key ID"),
|
||||
template: yup.string().label("Project Template Name")
|
||||
});
|
||||
|
||||
type TAddProjectFormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
// #TODO: Update all the workspaceIds
|
||||
const OrganizationPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@ -498,7 +456,6 @@ const OrganizationPage = () => {
|
||||
|
||||
const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||
const { currentOrg } = useOrganization();
|
||||
const { permission } = useOrgPermission();
|
||||
const routerOrgId = String(router.query.id);
|
||||
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
|
||||
const { data: projectFavorites, isLoading: isProjectFavoritesLoading } =
|
||||
@ -506,92 +463,25 @@ const OrganizationPage = () => {
|
||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||
|
||||
const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading;
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
|
||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||
"addNewWs",
|
||||
"upgradePlan"
|
||||
] as const);
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
reset,
|
||||
handleSubmit
|
||||
} = useForm<TAddProjectFormData>({
|
||||
resolver: yupResolver(formSchema),
|
||||
defaultValues: {
|
||||
kmsKeyId: INTERNAL_KMS_KEY_ID
|
||||
}
|
||||
});
|
||||
|
||||
const [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
||||
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
||||
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
||||
const [usersInOrg, setUsersInOrg] = useState(false);
|
||||
const [searchFilter, setSearchFilter] = useState("");
|
||||
const createWs = useCreateWorkspace();
|
||||
const { user } = useUser();
|
||||
const { data: serverDetails } = useFetchServerStatus();
|
||||
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
|
||||
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
|
||||
);
|
||||
|
||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
|
||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
||||
});
|
||||
|
||||
const onCreateProject = async ({ name, addMembers, kmsKeyId, template }: TAddProjectFormData) => {
|
||||
// type check
|
||||
if (!currentOrg) return;
|
||||
if (!user) return;
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
project: { id: newProjectId }
|
||||
}
|
||||
} = await createWs.mutateAsync({
|
||||
projectName: name,
|
||||
kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
|
||||
template
|
||||
});
|
||||
|
||||
if (addMembers) {
|
||||
const orgUsers = await fetchOrgUsers(currentOrg.id);
|
||||
|
||||
await addUsersToProject.mutateAsync({
|
||||
usernames: orgUsers
|
||||
.filter(
|
||||
(member) => member.user.username !== user.username && member.status === "accepted"
|
||||
)
|
||||
.map((member) => member.user.username),
|
||||
projectId: newProjectId,
|
||||
orgId: currentOrg.id
|
||||
});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-promise-executor-return -- We do this because the function returns too fast, which sometimes causes an error when the user is redirected.
|
||||
await new Promise((resolve) => setTimeout(resolve, 2_000));
|
||||
|
||||
handlePopUpClose("addNewWs");
|
||||
createNotification({ text: "Project created", type: "success" });
|
||||
router.push(`/project/${newProjectId}/secrets/overview`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({ text: "Failed to create project", type: "error" });
|
||||
}
|
||||
};
|
||||
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const canReadProjectTemplates = permission.can(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.ProjectTemplates
|
||||
);
|
||||
|
||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
||||
});
|
||||
|
||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||
: true;
|
||||
@ -669,7 +559,7 @@ const OrganizationPage = () => {
|
||||
localStorage.setItem("projectData.id", workspace.id);
|
||||
}}
|
||||
key={workspace.id}
|
||||
className="min-w-72 flex h-40 cursor-pointer flex-col justify-between rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
className="min-w-72 flex h-40 cursor-pointer flex-col rounded-md border border-mineshaft-600 bg-mineshaft-800 p-4"
|
||||
>
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
||||
@ -693,18 +583,33 @@ const OrganizationPage = () => {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-0 pb-6 text-sm text-mineshaft-300">
|
||||
{workspace.environments?.length || 0} environments
|
||||
|
||||
<div
|
||||
className="mt-1 mb-2.5 grow text-sm text-mineshaft-300"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
display: "-webkit-box",
|
||||
WebkitBoxOrient: "vertical",
|
||||
WebkitLineClamp: 2
|
||||
}}
|
||||
>
|
||||
{workspace.description}
|
||||
</div>
|
||||
<button type="button">
|
||||
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
|
||||
Explore{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
|
||||
/>
|
||||
|
||||
<div className="flex w-full flex-row items-end justify-between place-self-end">
|
||||
<div className="mt-0 text-xs text-mineshaft-400">
|
||||
{workspace.environments?.length || 0} environments
|
||||
</div>
|
||||
</button>
|
||||
<button type="button">
|
||||
<div className="group ml-auto w-max cursor-pointer rounded-full border border-mineshaft-600 bg-mineshaft-900 py-2 px-4 text-sm text-mineshaft-300 transition-all hover:border-primary-500/80 hover:bg-primary-800/20 hover:text-mineshaft-200">
|
||||
Explore{" "}
|
||||
<FontAwesomeIcon
|
||||
icon={faArrowRight}
|
||||
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -1038,170 +943,10 @@ const OrganizationPage = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Modal
|
||||
<NewProjectModal
|
||||
isOpen={popUp.addNewWs.isOpen}
|
||||
onOpenChange={(isModalOpen) => {
|
||||
handlePopUpToggle("addNewWs", isModalOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent
|
||||
title="Create a new project"
|
||||
subTitle="This project will contain your secrets and configurations."
|
||||
>
|
||||
<form onSubmit={handleSubmit(onCreateProject)}>
|
||||
<div className="flex gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="name"
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Project Name"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
className="flex-1"
|
||||
>
|
||||
<Input {...field} placeholder="Type your project name" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="template"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<OrgPermissionCan
|
||||
I={OrgPermissionActions.Read}
|
||||
a={OrgPermissionSubjects.ProjectTemplates}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<FormControl
|
||||
label="Project Template"
|
||||
icon={<FontAwesomeIcon icon={faInfoCircle} size="sm" />}
|
||||
tooltipText={
|
||||
<>
|
||||
<p>
|
||||
Create this project from a template to provision it with custom
|
||||
environments and roles.
|
||||
</p>
|
||||
{subscription && !subscription.projectTemplates && (
|
||||
<p className="pt-2">Project templates are a paid feature.</p>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
defaultValue={InfisicalProjectTemplate.Default}
|
||||
placeholder={InfisicalProjectTemplate.Default}
|
||||
isDisabled={!isAllowed || !subscription?.projectTemplates}
|
||||
value={value}
|
||||
onValueChange={onChange}
|
||||
className="w-44"
|
||||
>
|
||||
{projectTemplates.length
|
||||
? projectTemplates.map((template) => (
|
||||
<SelectItem key={template.id} value={template.name}>
|
||||
{template.name}
|
||||
</SelectItem>
|
||||
))
|
||||
: Object.values(InfisicalProjectTemplate).map((template) => (
|
||||
<SelectItem key={template} value={template}>
|
||||
{template}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4 pl-1">
|
||||
<Controller
|
||||
control={control}
|
||||
name="addMembers"
|
||||
defaultValue={false}
|
||||
render={({ field: { onBlur, value, onChange } }) => (
|
||||
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.Member}>
|
||||
{(isAllowed) => (
|
||||
<div>
|
||||
<Checkbox
|
||||
id="add-project-layout"
|
||||
isChecked={value}
|
||||
onCheckedChange={onChange}
|
||||
isDisabled={!isAllowed}
|
||||
onBlur={onBlur}
|
||||
>
|
||||
Add all members of my organization to this project
|
||||
</Checkbox>
|
||||
</div>
|
||||
)}
|
||||
</OrgPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-14 flex">
|
||||
<Accordion type="single" collapsible className="w-full">
|
||||
<AccordionItem value="advance-settings" className="data-[state=open]:border-none">
|
||||
<AccordionTrigger className="h-fit flex-none pl-1 text-sm">
|
||||
<div className="order-1 ml-3">Advanced Settings</div>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Controller
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
label="KMS"
|
||||
>
|
||||
<Select
|
||||
{...field}
|
||||
onValueChange={(e) => {
|
||||
onChange(e);
|
||||
}}
|
||||
className="mb-12 w-full bg-mineshaft-600"
|
||||
>
|
||||
<SelectItem value={INTERNAL_KMS_KEY_ID} key="kms-internal">
|
||||
Default Infisical KMS
|
||||
</SelectItem>
|
||||
{externalKmsList?.map((kms) => (
|
||||
<SelectItem value={kms.id} key={`kms-${kms.id}`}>
|
||||
{kms.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="kmsKeyId"
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<div className="absolute right-0 bottom-0 mr-6 mb-6 flex items-start justify-end">
|
||||
<Button
|
||||
key="layout-cancel-create-project"
|
||||
onClick={() => handlePopUpClose("addNewWs")}
|
||||
colorSchema="secondary"
|
||||
variant="plain"
|
||||
className="py-2"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
isDisabled={isSubmitting}
|
||||
isLoading={isSubmitting}
|
||||
key="layout-create-project-submit"
|
||||
className="ml-4"
|
||||
type="submit"
|
||||
>
|
||||
Create Project
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||
/>
|
||||
<UpgradePlanModal
|
||||
isOpen={popUp.upgradePlan.isOpen}
|
||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||
|
@ -7,7 +7,7 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { EnvironmentSection } from "../EnvironmentSection";
|
||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||
import { ProjectNameChangeSection } from "../ProjectNameChangeSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
||||
import { SecretTagsSection } from "../SecretTagsSection";
|
||||
|
||||
@ -16,7 +16,7 @@ export const ProjectGeneralTab = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProjectNameChangeSection />
|
||||
<ProjectOverviewChangeSection />
|
||||
<EnvironmentSection />
|
||||
<SecretTagsSection />
|
||||
<AutoCapitalizationSection />
|
||||
|
@ -1,119 +0,0 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import * as yup from "yup";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useRenameWorkspace } from "@app/hooks/api";
|
||||
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
const formSchema = yup.object({
|
||||
name: yup
|
||||
.string()
|
||||
.required()
|
||||
.label("Project Name")
|
||||
.max(64, "Too long, maximum length is 64 characters")
|
||||
});
|
||||
|
||||
type FormData = yup.InferType<typeof formSchema>;
|
||||
|
||||
export const ProjectNameChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isLoading } = useRenameWorkspace();
|
||||
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: yupResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
name: currentWorkspace.name
|
||||
});
|
||||
}
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async ({ name }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
workspaceID: currentWorkspace.id,
|
||||
newWorkspaceName: name
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully renamed workspace",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to rename workspace",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4"
|
||||
>
|
||||
<div className="justify-betweens flex">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Name</h2>
|
||||
<div className="space-x-2">
|
||||
<CopyButton
|
||||
value={currentWorkspace?.slug || ""}
|
||||
hoverText="Click to project slug"
|
||||
notificationText="Copied project slug to clipboard"
|
||||
>
|
||||
Copy Project Slug
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
value={currentWorkspace?.id || ""}
|
||||
hoverText="Click to project ID"
|
||||
notificationText="Copied project ID to clipboard"
|
||||
>
|
||||
Copy Project ID
|
||||
</CopyButton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-md">
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
<ProjectPermissionCan I={ProjectPermissionActions.Edit} a={ProjectPermissionSub.Project}>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</form>
|
||||
);
|
||||
};
|
@ -1 +0,0 @@
|
||||
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
|
@ -0,0 +1,168 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import { Button, FormControl, Input, TextArea } from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||
import { useUpdateProject } from "@app/hooks/api";
|
||||
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
description: z
|
||||
.string()
|
||||
.trim()
|
||||
.max(256, "Description too long, max length is 256 characters")
|
||||
.optional()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
|
||||
export const ProjectOverviewChangeSection = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isLoading } = useUpdateProject();
|
||||
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? ""
|
||||
});
|
||||
}
|
||||
}, [currentWorkspace]);
|
||||
|
||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
projectID: currentWorkspace.id,
|
||||
newProjectName: name,
|
||||
newProjectDescription: description
|
||||
});
|
||||
|
||||
createNotification({
|
||||
text: "Successfully updated project overview",
|
||||
type: "success"
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
createNotification({
|
||||
text: "Failed to update project overview",
|
||||
type: "error"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||
<div className="justify-betweens flex">
|
||||
<h2 className="mb-8 flex-1 text-xl font-semibold text-mineshaft-100">Project Overview</h2>
|
||||
<div className="space-x-2">
|
||||
<CopyButton
|
||||
value={currentWorkspace?.slug || ""}
|
||||
hoverText="Click to project slug"
|
||||
notificationText="Copied project slug to clipboard"
|
||||
>
|
||||
Copy Project Slug
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
value={currentWorkspace?.id || ""}
|
||||
hoverText="Click to project ID"
|
||||
notificationText="Copied project ID to clipboard"
|
||||
>
|
||||
Copy Project ID
|
||||
</CopyButton>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="flex w-full flex-col gap-0">
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project name"
|
||||
>
|
||||
<Input
|
||||
placeholder="Project name"
|
||||
{...field}
|
||||
className=" bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
label="Project description"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="Project description"
|
||||
{...field}
|
||||
rows={3}
|
||||
className="thin-scrollbar max-w-md !resize-none bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="description"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={ProjectPermissionSub.Project}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
type="submit"
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
@ -2,5 +2,5 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
|
||||
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { EnvironmentSection } from "./EnvironmentSection";
|
||||
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
export { SecretTagsSection } from "./SecretTagsSection";
|
||||
|
Reference in New Issue
Block a user