mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 10:28:21 +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({
|
export const KmsRootConfigSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
encryptedRootKey: zodBuffer,
|
encryptedRootKey: zodBuffer,
|
||||||
encryptionStrategy: z.string(),
|
encryptionStrategy: z.string().default("SOFTWARE").nullable().optional(),
|
||||||
createdAt: z.date(),
|
createdAt: z.date(),
|
||||||
updatedAt: z.date()
|
updatedAt: z.date()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ export const ProjectsSchema = z.object({
|
|||||||
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
kmsCertificateKeyId: z.string().uuid().nullable().optional(),
|
||||||
auditLogsRetentionDays: z.number().nullable().optional(),
|
auditLogsRetentionDays: z.number().nullable().optional(),
|
||||||
kmsSecretManagerKeyId: z.string().uuid().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>;
|
export type TProjects = z.infer<typeof ProjectsSchema>;
|
||||||
|
|||||||
@@ -391,6 +391,7 @@ export const PROJECTS = {
|
|||||||
CREATE: {
|
CREATE: {
|
||||||
organizationSlug: "The slug of the organization to create the project in.",
|
organizationSlug: "The slug of the organization to create the project in.",
|
||||||
projectName: "The name of the project to create.",
|
projectName: "The name of the project to create.",
|
||||||
|
projectDescription: "An optional description label for the project.",
|
||||||
slug: "An optional slug for the project.",
|
slug: "An optional slug for the project.",
|
||||||
template: "The name of the project template, if specified, to apply to this project."
|
template: "The name of the project template, if specified, to apply to this project."
|
||||||
},
|
},
|
||||||
@@ -403,6 +404,7 @@ export const PROJECTS = {
|
|||||||
UPDATE: {
|
UPDATE: {
|
||||||
workspaceId: "The ID of the project to update.",
|
workspaceId: "The ID of the project to update.",
|
||||||
name: "The new name of the project.",
|
name: "The new name of the project.",
|
||||||
|
projectDescription: "An optional description label for the project.",
|
||||||
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
autoCapitalization: "Disable or enable auto-capitalization for the project."
|
||||||
},
|
},
|
||||||
GET_KEY: {
|
GET_KEY: {
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ export const SanitizedAuditLogStreamSchema = z.object({
|
|||||||
export const SanitizedProjectSchema = ProjectsSchema.pick({
|
export const SanitizedProjectSchema = ProjectsSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
description: true,
|
||||||
slug: true,
|
slug: true,
|
||||||
autoCapitalization: true,
|
autoCapitalization: true,
|
||||||
orgId: true,
|
orgId: true,
|
||||||
|
|||||||
@@ -296,6 +296,12 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
.max(64, { message: "Name must be 64 or fewer characters" })
|
.max(64, { message: "Name must be 64 or fewer characters" })
|
||||||
.optional()
|
.optional()
|
||||||
.describe(PROJECTS.UPDATE.name),
|
.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)
|
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
@@ -313,6 +319,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
|
description: req.body.description,
|
||||||
autoCapitalization: req.body.autoCapitalization
|
autoCapitalization: req.body.autoCapitalization
|
||||||
},
|
},
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
],
|
],
|
||||||
body: z.object({
|
body: z.object({
|
||||||
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
|
projectName: z.string().trim().describe(PROJECTS.CREATE.projectName),
|
||||||
|
projectDescription: z.string().trim().optional().describe(PROJECTS.CREATE.projectDescription),
|
||||||
slug: z
|
slug: z
|
||||||
.string()
|
.string()
|
||||||
.min(5)
|
.min(5)
|
||||||
@@ -194,6 +195,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
actorOrgId: req.permission.orgId,
|
actorOrgId: req.permission.orgId,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
workspaceName: req.body.projectName,
|
workspaceName: req.body.projectName,
|
||||||
|
workspaceDescription: req.body.projectDescription,
|
||||||
slug: req.body.slug,
|
slug: req.body.slug,
|
||||||
kmsKeyId: req.body.kmsKeyId,
|
kmsKeyId: req.body.kmsKeyId,
|
||||||
template: req.body.template
|
template: req.body.template
|
||||||
@@ -312,8 +314,9 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
slug: slugSchema.describe("The slug of the project to update.")
|
slug: slugSchema.describe("The slug of the project to update.")
|
||||||
}),
|
}),
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim().optional().describe("The new name of the project."),
|
name: z.string().trim().optional().describe(PROJECTS.UPDATE.name),
|
||||||
autoCapitalization: z.boolean().optional().describe("The new auto-capitalization setting.")
|
description: z.string().trim().optional().describe(PROJECTS.UPDATE.projectDescription),
|
||||||
|
autoCapitalization: z.boolean().optional().describe(PROJECTS.UPDATE.autoCapitalization)
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: SanitizedProjectSchema
|
200: SanitizedProjectSchema
|
||||||
@@ -330,6 +333,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
name: req.body.name,
|
name: req.body.name,
|
||||||
|
description: req.body.description,
|
||||||
autoCapitalization: req.body.autoCapitalization
|
autoCapitalization: req.body.autoCapitalization
|
||||||
},
|
},
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
|
|||||||
@@ -149,6 +149,7 @@ export const projectServiceFactory = ({
|
|||||||
actorOrgId,
|
actorOrgId,
|
||||||
actorAuthMethod,
|
actorAuthMethod,
|
||||||
workspaceName,
|
workspaceName,
|
||||||
|
workspaceDescription,
|
||||||
slug: projectSlug,
|
slug: projectSlug,
|
||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
tx: trx,
|
tx: trx,
|
||||||
@@ -206,6 +207,7 @@ export const projectServiceFactory = ({
|
|||||||
const project = await projectDAL.create(
|
const project = await projectDAL.create(
|
||||||
{
|
{
|
||||||
name: workspaceName,
|
name: workspaceName,
|
||||||
|
description: workspaceDescription,
|
||||||
orgId: organization.id,
|
orgId: organization.id,
|
||||||
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
slug: projectSlug || slugify(`${workspaceName}-${alphaNumericNanoId(4)}`),
|
||||||
kmsSecretManagerKeyId: kmsKeyId,
|
kmsSecretManagerKeyId: kmsKeyId,
|
||||||
@@ -496,6 +498,7 @@ export const projectServiceFactory = ({
|
|||||||
|
|
||||||
const updatedProject = await projectDAL.updateById(project.id, {
|
const updatedProject = await projectDAL.updateById(project.id, {
|
||||||
name: update.name,
|
name: update.name,
|
||||||
|
description: update.description,
|
||||||
autoCapitalization: update.autoCapitalization
|
autoCapitalization: update.autoCapitalization
|
||||||
});
|
});
|
||||||
return updatedProject;
|
return updatedProject;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export type TCreateProjectDTO = {
|
|||||||
actorId: string;
|
actorId: string;
|
||||||
actorOrgId?: string;
|
actorOrgId?: string;
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
|
workspaceDescription?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
createDefaultEnvs?: boolean;
|
createDefaultEnvs?: boolean;
|
||||||
@@ -69,6 +70,7 @@ export type TUpdateProjectDTO = {
|
|||||||
filter: Filter;
|
filter: Filter;
|
||||||
update: {
|
update: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
description?: string;
|
||||||
autoCapitalization?: boolean;
|
autoCapitalization?: boolean;
|
||||||
};
|
};
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { NewProjectModal } from "./NewProjectModal";
|
||||||
@@ -34,9 +34,9 @@ export type {
|
|||||||
CreateWorkspaceDTO,
|
CreateWorkspaceDTO,
|
||||||
DeleteEnvironmentDTO,
|
DeleteEnvironmentDTO,
|
||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
RenameWorkspaceDTO,
|
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
|
UpdateProjectDTO,
|
||||||
Workspace,
|
Workspace,
|
||||||
WorkspaceEnv,
|
WorkspaceEnv,
|
||||||
WorkspaceTag
|
WorkspaceTag
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ export {
|
|||||||
useListWorkspacePkiAlerts,
|
useListWorkspacePkiAlerts,
|
||||||
useListWorkspacePkiCollections,
|
useListWorkspacePkiCollections,
|
||||||
useNameWorkspaceSecrets,
|
useNameWorkspaceSecrets,
|
||||||
useRenameWorkspace,
|
|
||||||
useToggleAutoCapitalization,
|
useToggleAutoCapitalization,
|
||||||
useUpdateIdentityWorkspaceRole,
|
useUpdateIdentityWorkspaceRole,
|
||||||
|
useUpdateProject,
|
||||||
useUpdateUserWorkspaceRole,
|
useUpdateUserWorkspaceRole,
|
||||||
useUpdateWsEnvironment,
|
useUpdateWsEnvironment,
|
||||||
useUpgradeProject} from "./queries";
|
useUpgradeProject
|
||||||
|
} from "./queries";
|
||||||
export { workspaceKeys } from "./query-keys";
|
export { workspaceKeys } from "./query-keys";
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
DeleteWorkspaceDTO,
|
DeleteWorkspaceDTO,
|
||||||
NameWorkspaceSecretsDTO,
|
NameWorkspaceSecretsDTO,
|
||||||
ProjectIdentityOrderBy,
|
ProjectIdentityOrderBy,
|
||||||
RenameWorkspaceDTO,
|
|
||||||
TGetUpgradeProjectStatusDTO,
|
TGetUpgradeProjectStatusDTO,
|
||||||
TListProjectIdentitiesDTO,
|
TListProjectIdentitiesDTO,
|
||||||
ToggleAutoCapitalizationDTO,
|
ToggleAutoCapitalizationDTO,
|
||||||
@@ -35,6 +34,7 @@ import {
|
|||||||
UpdateAuditLogsRetentionDTO,
|
UpdateAuditLogsRetentionDTO,
|
||||||
UpdateEnvironmentDTO,
|
UpdateEnvironmentDTO,
|
||||||
UpdatePitVersionLimitDTO,
|
UpdatePitVersionLimitDTO,
|
||||||
|
UpdateProjectDTO,
|
||||||
Workspace
|
Workspace
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
@@ -208,19 +208,26 @@ export const useGetWorkspaceIntegrations = (workspaceId: string) =>
|
|||||||
|
|
||||||
export const createWorkspace = ({
|
export const createWorkspace = ({
|
||||||
projectName,
|
projectName,
|
||||||
|
projectDescription,
|
||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
template
|
template
|
||||||
}: CreateWorkspaceDTO): Promise<{ data: { project: Workspace } }> => {
|
}: 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 = () => {
|
export const useCreateWorkspace = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
return useMutation<{ data: { project: Workspace } }, {}, CreateWorkspaceDTO>({
|
||||||
mutationFn: async ({ projectName, kmsKeyId, template }) =>
|
mutationFn: async ({ projectName, projectDescription, kmsKeyId, template }) =>
|
||||||
createWorkspace({
|
createWorkspace({
|
||||||
projectName,
|
projectName,
|
||||||
|
projectDescription,
|
||||||
kmsKeyId,
|
kmsKeyId,
|
||||||
template
|
template
|
||||||
}),
|
}),
|
||||||
@@ -230,12 +237,15 @@ export const useCreateWorkspace = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useRenameWorkspace = () => {
|
export const useUpdateProject = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, RenameWorkspaceDTO>({
|
return useMutation<{}, {}, UpdateProjectDTO>({
|
||||||
mutationFn: ({ workspaceID, newWorkspaceName }) => {
|
mutationFn: ({ projectID, newProjectName, newProjectDescription }) => {
|
||||||
return apiRequest.post(`/api/v1/workspace/${workspaceID}/name`, { name: newWorkspaceName });
|
return apiRequest.patch(`/api/v1/workspace/${projectID}`, {
|
||||||
|
name: newProjectName,
|
||||||
|
description: newProjectDescription
|
||||||
|
});
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
queryClient.invalidateQueries(workspaceKeys.getAllUserWorkspace);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export type Workspace = {
|
|||||||
__v: number;
|
__v: number;
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string;
|
||||||
orgId: string;
|
orgId: string;
|
||||||
version: ProjectVersion;
|
version: ProjectVersion;
|
||||||
upgradeStatus: string | null;
|
upgradeStatus: string | null;
|
||||||
@@ -26,7 +27,6 @@ export type Workspace = {
|
|||||||
auditLogsRetentionDays: number;
|
auditLogsRetentionDays: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
||||||
roles?: TProjectRole[];
|
roles?: TProjectRole[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -56,11 +56,17 @@ export type TGetUpgradeProjectStatusDTO = {
|
|||||||
// mutation dto
|
// mutation dto
|
||||||
export type CreateWorkspaceDTO = {
|
export type CreateWorkspaceDTO = {
|
||||||
projectName: string;
|
projectName: string;
|
||||||
|
projectDescription?: string;
|
||||||
kmsKeyId?: string;
|
kmsKeyId?: string;
|
||||||
template?: 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 UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||||
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
export type UpdateAuditLogsRetentionDTO = { projectSlug: string; auditLogsRetentionDays: number };
|
||||||
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
export type ToggleAutoCapitalizationDTO = { workspaceID: string; state: boolean };
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
/* eslint-disable func-names */
|
/* eslint-disable func-names */
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -21,66 +20,48 @@ import {
|
|||||||
faEnvelope,
|
faEnvelope,
|
||||||
faInfinity,
|
faInfinity,
|
||||||
faInfo,
|
faInfo,
|
||||||
faInfoCircle,
|
|
||||||
faMobile,
|
faMobile,
|
||||||
faPlus,
|
faPlus,
|
||||||
faQuestion,
|
faQuestion,
|
||||||
faStar as faSolidStar
|
faStar as faSolidStar
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
import { tempLocalStorage } from "@app/components/utilities/checks/tempLocalStorage";
|
||||||
import SecurityClient from "@app/components/utilities/SecurityClient";
|
import SecurityClient from "@app/components/utilities/SecurityClient";
|
||||||
import {
|
import {
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
FormControl,
|
|
||||||
Input,
|
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
UpgradePlanModal
|
UpgradePlanModal
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { NewProjectModal } from "@app/components/v2/projects/NewProjectModal";
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useOrgPermission,
|
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import { usePopUp, useToggle } from "@app/hooks";
|
import { usePopUp, useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
fetchOrgUsers,
|
|
||||||
useAddUserToWsNonE2EE,
|
|
||||||
useCreateWorkspace,
|
|
||||||
useGetAccessRequestsCount,
|
useGetAccessRequestsCount,
|
||||||
useGetExternalKmsList,
|
|
||||||
useGetOrgTrialUrl,
|
useGetOrgTrialUrl,
|
||||||
useGetSecretApprovalRequestCount,
|
useGetSecretApprovalRequestCount,
|
||||||
useLogoutUser,
|
useLogoutUser,
|
||||||
useSelectOrganization
|
useSelectOrganization
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
import { MfaMethod } from "@app/hooks/api/auth/types";
|
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 { Workspace } from "@app/hooks/api/types";
|
||||||
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
import { useUpdateUserProjectFavorites } from "@app/hooks/api/users/mutation";
|
||||||
import { useGetUserProjectFavorites } from "@app/hooks/api/users/queries";
|
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) => {
|
export const AppLayout = ({ children }: LayoutProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -165,10 +132,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
|
|
||||||
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
const { data: secretApprovalReqCount } = useGetSecretApprovalRequestCount({ workspaceId });
|
||||||
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
const { data: accessApprovalRequestCount } = useGetAccessRequestsCount({ projectSlug });
|
||||||
const { permission } = useOrgPermission();
|
|
||||||
const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
|
|
||||||
enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
|
|
||||||
});
|
|
||||||
|
|
||||||
const pendingRequestsCount = useMemo(() => {
|
const pendingRequestsCount = useMemo(() => {
|
||||||
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
return (secretApprovalReqCount?.open || 0) + (accessApprovalRequestCount?.pendingCount || 0);
|
||||||
@@ -178,27 +141,13 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
const createWs = useCreateWorkspace();
|
|
||||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
|
||||||
|
|
||||||
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
const infisicalPlatformVersion = process.env.NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION;
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
"upgradePlan",
|
"upgradePlan",
|
||||||
"createOrg"
|
"createOrg"
|
||||||
] as const);
|
] as const);
|
||||||
const {
|
|
||||||
control,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
reset,
|
|
||||||
handleSubmit
|
|
||||||
} = useForm<TAddProjectFormData>({
|
|
||||||
resolver: yupResolver(formSchema),
|
|
||||||
defaultValues: {
|
|
||||||
kmsKeyId: INTERNAL_KMS_KEY_ID
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -281,58 +230,6 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
putUserInOrg();
|
putUserInOrg();
|
||||||
}, [router.query.id]);
|
}, [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) => {
|
const addProjectToFavorites = async (projectId: string) => {
|
||||||
try {
|
try {
|
||||||
if (currentOrg?.id) {
|
if (currentOrg?.id) {
|
||||||
@@ -916,176 +813,10 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
<Modal
|
<NewProjectModal
|
||||||
isOpen={popUp.addNewWs.isOpen}
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
onOpenChange={(isModalOpen) => {
|
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||||
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>
|
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
// REFACTOR(akhilmhdh): This file needs to be split into multiple components too complex
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
@@ -19,7 +18,6 @@ import {
|
|||||||
faExclamationCircle,
|
faExclamationCircle,
|
||||||
faFileShield,
|
faFileShield,
|
||||||
faHandPeace,
|
faHandPeace,
|
||||||
faInfoCircle,
|
|
||||||
faList,
|
faList,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faNetworkWired,
|
faNetworkWired,
|
||||||
@@ -29,48 +27,22 @@ import {
|
|||||||
faUserPlus
|
faUserPlus
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import * as Tabs from "@radix-ui/react-tabs";
|
||||||
import * as yup from "yup";
|
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { OrgPermissionCan } from "@app/components/permissions";
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
import onboardingCheck from "@app/components/utilities/checks/OnboardingCheck";
|
||||||
import {
|
import { Button, IconButton, Input, Skeleton, UpgradePlanModal } from "@app/components/v2";
|
||||||
Accordion,
|
import { NewProjectModal } from "@app/components/v2/projects";
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
FormControl,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Skeleton,
|
|
||||||
UpgradePlanModal
|
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
import {
|
||||||
OrgPermissionActions,
|
OrgPermissionActions,
|
||||||
OrgPermissionSubjects,
|
OrgPermissionSubjects,
|
||||||
useOrganization,
|
useOrganization,
|
||||||
useOrgPermission,
|
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useUser,
|
useUser,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
import {
|
import { useRegisterUserAction } from "@app/hooks/api";
|
||||||
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 { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
// import { fetchUserWsKey } from "@app/hooks/api/keys/queries";
|
||||||
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
|
||||||
import { Workspace } from "@app/hooks/api/types";
|
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
|
// #TODO: Update all the workspaceIds
|
||||||
const OrganizationPage = () => {
|
const OrganizationPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -498,7 +456,6 @@ const OrganizationPage = () => {
|
|||||||
|
|
||||||
const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
|
const { workspaces, isLoading: isWorkspaceLoading } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { permission } = useOrgPermission();
|
|
||||||
const routerOrgId = String(router.query.id);
|
const routerOrgId = String(router.query.id);
|
||||||
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
|
const orgWorkspaces = workspaces?.filter((workspace) => workspace.orgId === routerOrgId) || [];
|
||||||
const { data: projectFavorites, isLoading: isProjectFavoritesLoading } =
|
const { data: projectFavorites, isLoading: isProjectFavoritesLoading } =
|
||||||
@@ -506,92 +463,25 @@ const OrganizationPage = () => {
|
|||||||
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
const { mutateAsync: updateUserProjectFavorites } = useUpdateUserProjectFavorites();
|
||||||
|
|
||||||
const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading;
|
const isProjectViewLoading = isWorkspaceLoading || isProjectFavoritesLoading;
|
||||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
"addNewWs",
|
"addNewWs",
|
||||||
"upgradePlan"
|
"upgradePlan"
|
||||||
] as const);
|
] 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 [hasUserClickedSlack, setHasUserClickedSlack] = useState(false);
|
||||||
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
const [hasUserClickedIntro, setHasUserClickedIntro] = useState(false);
|
||||||
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
const [hasUserPushedSecrets, setHasUserPushedSecrets] = useState(false);
|
||||||
const [usersInOrg, setUsersInOrg] = useState(false);
|
const [usersInOrg, setUsersInOrg] = useState(false);
|
||||||
const [searchFilter, setSearchFilter] = useState("");
|
const [searchFilter, setSearchFilter] = useState("");
|
||||||
const createWs = useCreateWorkspace();
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { data: serverDetails } = useFetchServerStatus();
|
const { data: serverDetails } = useFetchServerStatus();
|
||||||
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
|
const [projectsViewMode, setProjectsViewMode] = useState<ProjectsViewMode>(
|
||||||
(localStorage.getItem("projectsViewMode") as ProjectsViewMode) || ProjectsViewMode.GRID
|
(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 { subscription } = useSubscription();
|
||||||
|
|
||||||
const canReadProjectTemplates = permission.can(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.ProjectTemplates
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: projectTemplates = [] } = useListProjectTemplates({
|
|
||||||
enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
|
|
||||||
});
|
|
||||||
|
|
||||||
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
const isAddingProjectsAllowed = subscription?.workspaceLimit
|
||||||
? subscription.workspacesUsed < subscription.workspaceLimit
|
? subscription.workspacesUsed < subscription.workspaceLimit
|
||||||
: true;
|
: true;
|
||||||
@@ -669,7 +559,7 @@ const OrganizationPage = () => {
|
|||||||
localStorage.setItem("projectData.id", workspace.id);
|
localStorage.setItem("projectData.id", workspace.id);
|
||||||
}}
|
}}
|
||||||
key={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="flex flex-row justify-between">
|
||||||
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
<div className="mt-0 truncate text-lg text-mineshaft-100">{workspace.name}</div>
|
||||||
@@ -693,18 +583,33 @@ const OrganizationPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</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">
|
<div className="flex w-full flex-row items-end justify-between place-self-end">
|
||||||
Explore{" "}
|
<div className="mt-0 text-xs text-mineshaft-400">
|
||||||
<FontAwesomeIcon
|
{workspace.environments?.length || 0} environments
|
||||||
icon={faArrowRight}
|
|
||||||
className="pl-1.5 pr-0.5 duration-200 hover:pl-2 hover:pr-0"
|
|
||||||
/>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1038,170 +943,10 @@ const OrganizationPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Modal
|
<NewProjectModal
|
||||||
isOpen={popUp.addNewWs.isOpen}
|
isOpen={popUp.addNewWs.isOpen}
|
||||||
onOpenChange={(isModalOpen) => {
|
onOpenChange={(isOpen) => handlePopUpToggle("addNewWs", isOpen)}
|
||||||
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>
|
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp.upgradePlan.isOpen}
|
isOpen={popUp.upgradePlan.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect
|
|||||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||||
import { EnvironmentSection } from "../EnvironmentSection";
|
import { EnvironmentSection } from "../EnvironmentSection";
|
||||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||||
import { ProjectNameChangeSection } from "../ProjectNameChangeSection";
|
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||||
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
||||||
import { SecretTagsSection } from "../SecretTagsSection";
|
import { SecretTagsSection } from "../SecretTagsSection";
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export const ProjectGeneralTab = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ProjectNameChangeSection />
|
<ProjectOverviewChangeSection />
|
||||||
<EnvironmentSection />
|
<EnvironmentSection />
|
||||||
<SecretTagsSection />
|
<SecretTagsSection />
|
||||||
<AutoCapitalizationSection />
|
<AutoCapitalizationSection />
|
||||||
|
|||||||
-119
@@ -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
@@ -1 +0,0 @@
|
|||||||
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
|
|
||||||
+168
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||||
@@ -2,5 +2,5 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
|
|||||||
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
||||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||||
export { EnvironmentSection } from "./EnvironmentSection";
|
export { EnvironmentSection } from "./EnvironmentSection";
|
||||||
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";
|
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||||
export { SecretTagsSection } from "./SecretTagsSection";
|
export { SecretTagsSection } from "./SecretTagsSection";
|
||||||
|
|||||||
Reference in New Issue
Block a user