feat: Add Project Descriptions (#2774)

* feat:  initial backend project description
This commit is contained in:
McPizza
2024-11-25 21:59:14 +01:00
committed by GitHub
parent 21403f6fe5
commit 32430a6a16
24 changed files with 615 additions and 701 deletions

View File

@ -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");
});
}
}

View File

@ -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()
});

View File

@ -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>;

View File

@ -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: {

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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;

View File

@ -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">;

View 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>
);
};

View File

@ -0,0 +1 @@
export { NewProjectModal } from "./NewProjectModal";

View File

@ -34,9 +34,9 @@ export type {
CreateWorkspaceDTO,
DeleteEnvironmentDTO,
DeleteWorkspaceDTO,
RenameWorkspaceDTO,
ToggleAutoCapitalizationDTO,
UpdateEnvironmentDTO,
UpdateProjectDTO,
Workspace,
WorkspaceEnv,
WorkspaceTag

View File

@ -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";

View File

@ -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);

View File

@ -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 };

View File

@ -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)}

View File

@ -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)}

View File

@ -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 />

View File

@ -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>
);
};

View File

@ -1 +0,0 @@
export { ProjectNameChangeSection } from "./ProjectNameChangeSection";

View File

@ -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>
);
};

View File

@ -0,0 +1 @@
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";

View File

@ -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";