mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 22:27:22 +00:00
Merge branch 'main' into feat/addMoreVisibilityToServerAdmins
This commit is contained in:
+89
-21
@@ -9,9 +9,7 @@ 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({
|
||||
const baseFormSchema = z.object({
|
||||
name: z.string().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
|
||||
description: z
|
||||
.string()
|
||||
@@ -20,31 +18,55 @@ const formSchema = z.object({
|
||||
.optional()
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof formSchema>;
|
||||
const formSchemaWithSlug = baseFormSchema.extend({
|
||||
slug: z
|
||||
.string()
|
||||
.min(1, "Required")
|
||||
.max(64, "Too long, maximum length is 64 characters")
|
||||
.regex(
|
||||
/^[a-z0-9]+(?:[_-][a-z0-9]+)*$/,
|
||||
"Project slug can only contain lowercase letters and numbers, with optional single hyphens (-) or underscores (_) between words. Cannot start or end with a hyphen or underscore."
|
||||
)
|
||||
});
|
||||
|
||||
export const ProjectOverviewChangeSection = () => {
|
||||
type BaseFormData = z.infer<typeof baseFormSchema>;
|
||||
type FormDataWithSlug = z.infer<typeof formSchemaWithSlug>;
|
||||
|
||||
type Props = {
|
||||
showSlugField?: boolean;
|
||||
};
|
||||
|
||||
export const ProjectOverviewChangeSection = ({ showSlugField = false }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { mutateAsync, isPending } = useUpdateProject();
|
||||
const { handleSubmit, control, reset, watch } = useForm<BaseFormData | FormDataWithSlug>({
|
||||
resolver: zodResolver(showSlugField ? formSchemaWithSlug : baseFormSchema)
|
||||
});
|
||||
|
||||
const { handleSubmit, control, reset } = useForm<FormData>({ resolver: zodResolver(formSchema) });
|
||||
const currentSlug = showSlugField ? watch("slug") : currentWorkspace?.slug;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentWorkspace) {
|
||||
reset({
|
||||
name: currentWorkspace.name,
|
||||
description: currentWorkspace.description ?? ""
|
||||
description: currentWorkspace.description ?? "",
|
||||
...(showSlugField && { slug: currentWorkspace.slug })
|
||||
});
|
||||
}
|
||||
}, [currentWorkspace]);
|
||||
}, [currentWorkspace, showSlugField]);
|
||||
|
||||
const onFormSubmit = async ({ name, description }: FormData) => {
|
||||
const onFormSubmit = async (data: BaseFormData | FormDataWithSlug) => {
|
||||
try {
|
||||
if (!currentWorkspace?.id) return;
|
||||
|
||||
await mutateAsync({
|
||||
projectID: currentWorkspace.id,
|
||||
newProjectName: name,
|
||||
newProjectDescription: description
|
||||
newProjectName: data.name,
|
||||
newProjectDescription: data.description,
|
||||
...(showSlugField &&
|
||||
"slug" in data && {
|
||||
newSlug: data.slug !== currentWorkspace.slug ? data.slug : undefined
|
||||
})
|
||||
});
|
||||
|
||||
createNotification({
|
||||
@@ -65,20 +87,34 @@ export const ProjectOverviewChangeSection = () => {
|
||||
<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"
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(currentSlug || "");
|
||||
createNotification({
|
||||
text: "Copied project slug to clipboard",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
title="Click to copy project slug"
|
||||
>
|
||||
Copy Project Slug
|
||||
</CopyButton>
|
||||
<CopyButton
|
||||
value={currentWorkspace?.id || ""}
|
||||
hoverText="Click to project ID"
|
||||
notificationText="Copied project ID to clipboard"
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(currentWorkspace?.id || "");
|
||||
createNotification({
|
||||
text: "Copied project ID to clipboard",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
title="Click to copy project ID"
|
||||
>
|
||||
Copy Project ID
|
||||
</CopyButton>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
@@ -113,6 +149,38 @@ export const ProjectOverviewChangeSection = () => {
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
{showSlugField && (
|
||||
<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 slug"
|
||||
>
|
||||
<Input
|
||||
placeholder="Project slug"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
isDisabled={!isAllowed}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="slug"
|
||||
/>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full flex-row items-end gap-4">
|
||||
<div className="w-full max-w-md">
|
||||
<ProjectPermissionCan
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
Button,
|
||||
Checkbox,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
@@ -33,13 +32,7 @@ import {
|
||||
useUser
|
||||
} from "@app/context";
|
||||
import { getProjectHomePage } from "@app/helpers/project";
|
||||
import {
|
||||
fetchOrgUsers,
|
||||
useAddUserToWsNonE2EE,
|
||||
useCreateWorkspace,
|
||||
useGetExternalKmsList,
|
||||
useGetUserWorkspaces
|
||||
} from "@app/hooks/api";
|
||||
import { useCreateWorkspace, useGetExternalKmsList, useGetUserWorkspaces } from "@app/hooks/api";
|
||||
import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
|
||||
import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
|
||||
import { ProjectType } from "@app/hooks/api/workspace/types";
|
||||
@@ -51,7 +44,6 @@ const formSchema = z.object({
|
||||
.trim()
|
||||
.max(256, "Description too long, max length is 256 characters")
|
||||
.optional(),
|
||||
addMembers: z.boolean(),
|
||||
kmsKeyId: z.string(),
|
||||
template: z.string()
|
||||
});
|
||||
@@ -73,7 +65,6 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
const { user } = useUser();
|
||||
const createWs = useCreateWorkspace();
|
||||
const { refetch: refetchWorkspaces } = useGetUserWorkspaces();
|
||||
const addUsersToProject = useAddUserToWsNonE2EE();
|
||||
const { subscription } = useSubscription();
|
||||
|
||||
const canReadProjectTemplates = permission.can(
|
||||
@@ -111,7 +102,6 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
const onCreateProject = async ({
|
||||
name,
|
||||
description,
|
||||
addMembers,
|
||||
kmsKeyId,
|
||||
template
|
||||
}: TAddProjectFormData) => {
|
||||
@@ -128,21 +118,6 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
template,
|
||||
type: projectType
|
||||
});
|
||||
const { id: newProjectId } = project;
|
||||
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
await refetchWorkspaces();
|
||||
|
||||
createNotification({ text: "Project created", type: "success" });
|
||||
@@ -246,31 +221,7 @@ const NewProjectForm = ({ onOpenChange, projectType }: NewProjectFormProps) => {
|
||||
)}
|
||||
/>
|
||||
</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">
|
||||
<div className="mt-4 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">
|
||||
|
||||
@@ -74,6 +74,107 @@ export const queryClient = new QueryClient({
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (serverResponse?.error === ApiErrorTypes.PermissionBoundaryError) {
|
||||
createNotification(
|
||||
{
|
||||
title: "Forbidden Access",
|
||||
type: "error",
|
||||
text: `${serverResponse.message}.`,
|
||||
callToAction: serverResponse?.details?.missingPermissions?.length ? (
|
||||
<Modal>
|
||||
<ModalTrigger asChild>
|
||||
<Button variant="outline_bg" size="xs">
|
||||
Show more
|
||||
</Button>
|
||||
</ModalTrigger>
|
||||
<ModalContent title="Missing Permission">
|
||||
<div className="flex flex-col gap-2">
|
||||
{serverResponse.details?.missingPermissions?.map((el, index) => {
|
||||
const hasConditions = Boolean(Object.keys(el.conditions || {}).length);
|
||||
return (
|
||||
<div
|
||||
key={`Forbidden-error-details-${index + 1}`}
|
||||
className="rounded-md border border-gray-600 p-4"
|
||||
>
|
||||
<div>
|
||||
You are not authorized to perform the <b>{el.action}</b> action on the{" "}
|
||||
<b>{el.subject}</b> resource.{" "}
|
||||
{hasConditions &&
|
||||
"Your permission does not allow access to the following conditions:"}
|
||||
</div>
|
||||
{hasConditions && (
|
||||
<ul className="flex list-disc flex-col gap-1 pl-5 pt-2 text-sm">
|
||||
{Object.keys(el.conditions || {}).flatMap((field, fieldIndex) => {
|
||||
const operators = (
|
||||
el.conditions as Record<
|
||||
string,
|
||||
| string
|
||||
| { [K in PermissionConditionOperators]: string | string[] }
|
||||
>
|
||||
)[field];
|
||||
|
||||
const formattedFieldName = camelCaseToSpaces(field).toLowerCase();
|
||||
if (typeof operators === "string") {
|
||||
return (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">equal to</span>{" "}
|
||||
<span className="text-yellow-600">{operators}</span>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return Object.keys(operators).map((operator, operatorIndex) => (
|
||||
<li
|
||||
key={`Forbidden-error-details-${index + 1}-${
|
||||
fieldIndex + 1
|
||||
}-${operatorIndex + 1}`}
|
||||
>
|
||||
<span className="font-bold capitalize">
|
||||
{formattedFieldName}
|
||||
</span>{" "}
|
||||
<span className="text-mineshaft-200">
|
||||
{
|
||||
formatedConditionsOperatorNames[
|
||||
operator as PermissionConditionOperators
|
||||
]
|
||||
}
|
||||
</span>{" "}
|
||||
<span className="text-yellow-600">
|
||||
{operators[
|
||||
operator as PermissionConditionOperators
|
||||
].toString()}
|
||||
</span>
|
||||
</li>
|
||||
));
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
) : undefined,
|
||||
copyActions: [
|
||||
{
|
||||
value: serverResponse.reqId,
|
||||
name: "Request ID",
|
||||
label: `Request ID: ${serverResponse.reqId}`
|
||||
}
|
||||
]
|
||||
},
|
||||
{ closeOnClick: false }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (serverResponse?.error === ApiErrorTypes.ForbiddenError) {
|
||||
createNotification(
|
||||
{
|
||||
|
||||
@@ -44,6 +44,7 @@ export type {
|
||||
|
||||
export enum ApiErrorTypes {
|
||||
ValidationError = "ValidationFailure",
|
||||
PermissionBoundaryError = "PermissionBoundaryError",
|
||||
BadRequestError = "BadRequest",
|
||||
UnauthorizedError = "UnauthorizedError",
|
||||
ForbiddenError = "PermissionDenied"
|
||||
@@ -74,4 +75,17 @@ export type TApiErrors =
|
||||
statusCode: 400;
|
||||
message: string;
|
||||
error: ApiErrorTypes.BadRequestError;
|
||||
}
|
||||
| {
|
||||
reqId: string;
|
||||
statusCode: 403;
|
||||
message: string;
|
||||
error: ApiErrorTypes.PermissionBoundaryError;
|
||||
details: {
|
||||
missingPermissions: {
|
||||
action: string;
|
||||
subject: string;
|
||||
conditions: Record<string, Record<string, string>>;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -251,12 +251,13 @@ export const useUpdateProject = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<Workspace, object, UpdateProjectDTO>({
|
||||
mutationFn: async ({ projectID, newProjectName, newProjectDescription }) => {
|
||||
mutationFn: async ({ projectID, newProjectName, newProjectDescription, newSlug }) => {
|
||||
const { data } = await apiRequest.patch<{ workspace: Workspace }>(
|
||||
`/api/v1/workspace/${projectID}`,
|
||||
{
|
||||
name: newProjectName,
|
||||
description: newProjectDescription
|
||||
description: newProjectDescription,
|
||||
slug: newSlug
|
||||
}
|
||||
);
|
||||
return data.workspace;
|
||||
|
||||
@@ -74,6 +74,7 @@ export type UpdateProjectDTO = {
|
||||
projectID: string;
|
||||
newProjectName: string;
|
||||
newProjectDescription?: string;
|
||||
newSlug?: string;
|
||||
};
|
||||
|
||||
export type UpdatePitVersionLimitDTO = { projectSlug: string; pitVersionLimit: number };
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
|
||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection />
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
<AuditLogsRetentionSection />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
hoverText: string;
|
||||
notificationText: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (isProjectIdCopied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProjectIdCopied.on();
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
createNotification({
|
||||
text: notificationText,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{children}
|
||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{hoverText}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
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, isPending } = 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={isPending}
|
||||
isDisabled={isPending || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
@@ -1,2 +1 @@
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
|
||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection />
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
<AuditLogsRetentionSection />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
hoverText: string;
|
||||
notificationText: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (isProjectIdCopied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProjectIdCopied.on();
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
createNotification({
|
||||
text: notificationText,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{children}
|
||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{hoverText}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
@@ -1,2 +1 @@
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
|
||||
+2
-2
@@ -36,7 +36,7 @@ const formSchema = z.object({
|
||||
revocationStatement: z.string().min(1),
|
||||
renewStatement: z.string().optional(),
|
||||
ca: z.string().optional(),
|
||||
projectGatewayId: z.string().optional()
|
||||
projectGatewayId: z.string().optional().nullable()
|
||||
})
|
||||
.partial(),
|
||||
defaultTTL: z.string().superRefine((val, ctx) => {
|
||||
@@ -207,7 +207,7 @@ export const EditDynamicSecretSqlProviderForm = ({
|
||||
helperText=""
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
value={value || undefined}
|
||||
onValueChange={onChange}
|
||||
className="w-full border border-mineshaft-500"
|
||||
dropdownContainerClassName="max-w-none"
|
||||
|
||||
+44
-22
@@ -5,6 +5,7 @@ import {
|
||||
faArrowRotateRight,
|
||||
faCheckCircle,
|
||||
faClock,
|
||||
faCopy,
|
||||
faDesktop,
|
||||
faEyeSlash,
|
||||
faPlus,
|
||||
@@ -462,7 +463,7 @@ export const SecretDetailSidebar = ({
|
||||
<div
|
||||
className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
|
||||
>
|
||||
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
|
||||
{tagFields.fields.map(({ tagColor, id: formId, slug }) => (
|
||||
<Tag
|
||||
className="flex w-min items-center space-x-2"
|
||||
key={formId}
|
||||
@@ -471,7 +472,8 @@ export const SecretDetailSidebar = ({
|
||||
createNotification({ type: "error", text: "Access denied" });
|
||||
return;
|
||||
}
|
||||
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
||||
|
||||
const tag = tags?.find(({ slug: tagSlug }) => slug === tagSlug);
|
||||
if (tag) handleTagSelect(tag);
|
||||
}}
|
||||
>
|
||||
@@ -990,29 +992,49 @@ export const SecretDetailSidebar = ({
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip content="Copy Secret ID">
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
ariaLabel="Delete Secret"
|
||||
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={onDeleteSecret}
|
||||
variant="outline_bg"
|
||||
ariaLabel="Copy Secret ID"
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(secret.id);
|
||||
|
||||
createNotification({
|
||||
title: "Secret ID Copied",
|
||||
text: "The secret ID has been copied to your clipboard.",
|
||||
type: "success"
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Tooltip content="Delete Secret">
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</Tooltip>
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretKey,
|
||||
secretTags: selectTagSlugs
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Tooltip content="Delete Secret">
|
||||
<IconButton
|
||||
colorSchema="danger"
|
||||
variant="outline_bg"
|
||||
ariaLabel="Delete Secret"
|
||||
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
|
||||
isDisabled={!isAllowed}
|
||||
onClick={onDeleteSecret}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -241,8 +241,8 @@ export const SecretListView = ({
|
||||
|
||||
let successMessage;
|
||||
if (isReminderEvent) {
|
||||
successMessage = reminderRepeatDays
|
||||
? "Successfully saved secret reminder"
|
||||
successMessage = reminderRepeatDays
|
||||
? "Successfully saved secret reminder"
|
||||
: "Successfully deleted secret reminder";
|
||||
} else {
|
||||
successMessage = "Successfully saved secrets";
|
||||
|
||||
+2
-2
@@ -1,3 +1,4 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { ProjectType, ProjectVersion } from "@app/hooks/api/workspace/types";
|
||||
|
||||
@@ -7,7 +8,6 @@ import { BackfillSecretReferenceSecretion } from "../BackfillSecretReferenceSect
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { EnvironmentSection } from "../EnvironmentSection";
|
||||
import { PointInTimeVersionLimitSection } from "../PointInTimeVersionLimitSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
import { RebuildSecretIndicesSection } from "../RebuildSecretIndicesSection/RebuildSecretIndicesSection";
|
||||
import { SecretTagsSection } from "../SecretTagsSection";
|
||||
|
||||
@@ -17,7 +17,7 @@ export const ProjectGeneralTab = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection />
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
{isSecretManager && <EnvironmentSection />}
|
||||
{isSecretManager && <SecretTagsSection />}
|
||||
{isSecretManager && <AutoCapitalizationSection />}
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
hoverText: string;
|
||||
notificationText: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (isProjectIdCopied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProjectIdCopied.on();
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
createNotification({
|
||||
text: notificationText,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{children}
|
||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{hoverText}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
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, isPending } = 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={isPending}
|
||||
isDisabled={isPending || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
-1
@@ -1 +0,0 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
@@ -2,5 +2,4 @@ export { AutoCapitalizationSection } from "./AutoCapitalizationSection";
|
||||
export { BackfillSecretReferenceSecretion } from "./BackfillSecretReferenceSection";
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { EnvironmentSection } from "./EnvironmentSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
export { SecretTagsSection } from "./SecretTagsSection";
|
||||
|
||||
+3
-2
@@ -1,11 +1,12 @@
|
||||
import { ProjectOverviewChangeSection } from "@app/components/project/ProjectOverviewChangeSection";
|
||||
|
||||
import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection";
|
||||
import { DeleteProjectSection } from "../DeleteProjectSection";
|
||||
import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection";
|
||||
|
||||
export const ProjectGeneralTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<ProjectOverviewChangeSection />
|
||||
<ProjectOverviewChangeSection showSlugField />
|
||||
<AuditLogsRetentionSection />
|
||||
<DeleteProjectSection />
|
||||
</div>
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
import { useCallback } from "react";
|
||||
import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import { Button } from "@app/components/v2";
|
||||
import { useToggle } from "@app/hooks";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
hoverText: string;
|
||||
notificationText: string;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => {
|
||||
const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false);
|
||||
|
||||
const copyToClipboard = useCallback(() => {
|
||||
if (isProjectIdCopied) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsProjectIdCopied.on();
|
||||
navigator.clipboard.writeText(value);
|
||||
|
||||
createNotification({
|
||||
text: notificationText,
|
||||
type: "success"
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000);
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
return () => clearTimeout(timer);
|
||||
}, [isProjectIdCopied]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
colorSchema="secondary"
|
||||
className="group relative"
|
||||
leftIcon={<FontAwesomeIcon icon={isProjectIdCopied ? faCheck : faCopy} />}
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{children}
|
||||
<span className="absolute -left-8 -top-20 hidden translate-y-full justify-center rounded-md bg-bunker-800 px-3 py-2 text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||
{hoverText}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
-168
@@ -1,168 +0,0 @@
|
||||
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, isPending } = 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={isPending}
|
||||
isDisabled={isPending || !isAllowed}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
@@ -1,2 +1 @@
|
||||
export { DeleteProjectSection } from "./DeleteProjectSection";
|
||||
export { ProjectOverviewChangeSection } from "./ProjectOverviewChangeSection";
|
||||
|
||||
Reference in New Issue
Block a user