mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-04 01:26:09 +00:00
feat: initial integration of cert template management
This commit is contained in:
@@ -476,7 +476,11 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
|
|||||||
CertificateTemplatesSchema.pick({
|
CertificateTemplatesSchema.pick({
|
||||||
id: true,
|
id: true,
|
||||||
name: true
|
name: true
|
||||||
})
|
}).merge(
|
||||||
|
z.object({
|
||||||
|
caName: z.string()
|
||||||
|
})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export const certificateTemplateDALFactory = (db: TDbClient) => {
|
|||||||
`${TableName.CertificateTemplate}.caId`
|
`${TableName.CertificateTemplate}.caId`
|
||||||
)
|
)
|
||||||
.where(`${TableName.CertificateAuthority}.projectId`, "=", projectId)
|
.where(`${TableName.CertificateAuthority}.projectId`, "=", projectId)
|
||||||
.select(selectAllTableCols(TableName.CertificateTemplate));
|
.select(selectAllTableCols(TableName.CertificateTemplate))
|
||||||
|
.select(db.ref("friendlyName").as("caName").withSchema(TableName.CertificateAuthority));
|
||||||
|
|
||||||
return certTemplates;
|
return certTemplates;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export enum ProjectPermissionSub {
|
|||||||
Identity = "identity",
|
Identity = "identity",
|
||||||
CertificateAuthorities = "certificate-authorities",
|
CertificateAuthorities = "certificate-authorities",
|
||||||
Certificates = "certificates",
|
Certificates = "certificates",
|
||||||
|
CertificateTemplates = "certificate-templates",
|
||||||
PkiAlerts = "pki-alerts",
|
PkiAlerts = "pki-alerts",
|
||||||
PkiCollections = "pki-collections",
|
PkiCollections = "pki-collections",
|
||||||
Kms = "kms"
|
Kms = "kms"
|
||||||
@@ -59,6 +60,7 @@ export type ProjectPermissionSet =
|
|||||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
| [ProjectPermissionActions, ProjectPermissionSub.SecretRotation]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
| [ProjectPermissionActions, ProjectPermissionSub.Certificates]
|
||||||
|
| [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts]
|
||||||
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
| [ProjectPermissionActions, ProjectPermissionSub.PkiCollections]
|
||||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { useCreateCertTemplate, useDeleteCertTemplate, useUpdateCertTemplate } from "./mutations";
|
||||||
|
export { useGetCertTemplate } from "./queries";
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { workspaceKeys } from "../workspace/queries";
|
||||||
|
import { certTemplateKeys } from "./queries";
|
||||||
|
import {
|
||||||
|
TCertificateTemplate,
|
||||||
|
TCreateCertificateTemplateDTO,
|
||||||
|
TDeleteCertificateTemplateDTO,
|
||||||
|
TUpdateCertificateTemplateDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
|
export const useCreateCertTemplate = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TCertificateTemplate, {}, TCreateCertificateTemplateDTO>({
|
||||||
|
mutationFn: async (data) => {
|
||||||
|
const {
|
||||||
|
data: { certificateTemplate }
|
||||||
|
} = await apiRequest.post<{ certificateTemplate: TCertificateTemplate }>(
|
||||||
|
"/api/v1/pki/certificate-templates",
|
||||||
|
data
|
||||||
|
);
|
||||||
|
return certificateTemplate;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateCertTemplate = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<TCertificateTemplate, {}, TUpdateCertificateTemplateDTO>({
|
||||||
|
mutationFn: async (data) => {
|
||||||
|
const {
|
||||||
|
data: { certificateTemplate }
|
||||||
|
} = await apiRequest.patch<{ certificateTemplate: TCertificateTemplate }>(
|
||||||
|
`/api/v1/pki/certificate-templates/${data.id}`,
|
||||||
|
data
|
||||||
|
);
|
||||||
|
|
||||||
|
return certificateTemplate;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId, id }) => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||||
|
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteCertTemplate = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<void, {}, TDeleteCertificateTemplateDTO>({
|
||||||
|
mutationFn: async (data) => {
|
||||||
|
return apiRequest.delete(`/api/v1/pki/certificate-templates/${data.id}`);
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId, id }) => {
|
||||||
|
queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
|
||||||
|
queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { TCertificateTemplate } from "./types";
|
||||||
|
|
||||||
|
export const certTemplateKeys = {
|
||||||
|
getCertTemplateById: (id: string) => [{ id }, "cert-template"]
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetCertTemplate = (id: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: certTemplateKeys.getCertTemplateById(id),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { certificateTemplate }
|
||||||
|
} = await apiRequest.get<{ certificateTemplate: TCertificateTemplate }>(
|
||||||
|
`/api/v1/pki/certificate-templates/${id}`
|
||||||
|
);
|
||||||
|
return certificateTemplate;
|
||||||
|
},
|
||||||
|
enabled: Boolean(id)
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
export type TCertificateTemplateListEntry = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
caName: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCertificateTemplate = {
|
||||||
|
id: string;
|
||||||
|
caId: string;
|
||||||
|
name: string;
|
||||||
|
commonName: string;
|
||||||
|
ttl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TCreateCertificateTemplateDTO = {
|
||||||
|
caId: string;
|
||||||
|
name: string;
|
||||||
|
commonName: string;
|
||||||
|
ttl: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TUpdateCertificateTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
caId?: string;
|
||||||
|
name?: string;
|
||||||
|
commonName?: string;
|
||||||
|
ttl?: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteCertificateTemplateDTO = {
|
||||||
|
id: string;
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ export * from "./auth";
|
|||||||
export * from "./bots";
|
export * from "./bots";
|
||||||
export * from "./ca";
|
export * from "./ca";
|
||||||
export * from "./certificates";
|
export * from "./certificates";
|
||||||
|
export * from "./certificateTemplates";
|
||||||
export * from "./dynamicSecret";
|
export * from "./dynamicSecret";
|
||||||
export * from "./dynamicSecretLease";
|
export * from "./dynamicSecretLease";
|
||||||
export * from "./groups";
|
export * from "./groups";
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export {
|
|||||||
useGetWorkspaceUsers,
|
useGetWorkspaceUsers,
|
||||||
useListWorkspaceCas,
|
useListWorkspaceCas,
|
||||||
useListWorkspaceCertificates,
|
useListWorkspaceCertificates,
|
||||||
|
useListWorkspaceCertificateTemplates,
|
||||||
useListWorkspaceGroups,
|
useListWorkspaceGroups,
|
||||||
useListWorkspacePkiAlerts,
|
useListWorkspacePkiAlerts,
|
||||||
useListWorkspacePkiCollections,
|
useListWorkspacePkiCollections,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import { CaStatus } from "../ca/enums";
|
import { CaStatus } from "../ca/enums";
|
||||||
import { TCertificateAuthority } from "../ca/types";
|
import { TCertificateAuthority } from "../ca/types";
|
||||||
import { TCertificate } from "../certificates/types";
|
import { TCertificate } from "../certificates/types";
|
||||||
|
import { TCertificateTemplateListEntry } from "../certificateTemplates/types";
|
||||||
import { TGroupMembership } from "../groups/types";
|
import { TGroupMembership } from "../groups/types";
|
||||||
import { identitiesKeys } from "../identities/queries";
|
import { identitiesKeys } from "../identities/queries";
|
||||||
import { IdentityMembership } from "../identities/types";
|
import { IdentityMembership } from "../identities/types";
|
||||||
@@ -68,7 +69,9 @@ export const workspaceKeys = {
|
|||||||
getWorkspacePkiAlerts: (workspaceId: string) =>
|
getWorkspacePkiAlerts: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
[{ workspaceId }, "workspace-pki-alerts"] as const,
|
||||||
getWorkspacePkiCollections: (workspaceId: string) =>
|
getWorkspacePkiCollections: (workspaceId: string) =>
|
||||||
[{ workspaceId }, "workspace-pki-collections"] as const
|
[{ workspaceId }, "workspace-pki-collections"] as const,
|
||||||
|
getWorkspaceCertificateTemplates: (workspaceId: string) =>
|
||||||
|
[{ workspaceId }, "workspace-certificate-templates"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchWorkspaceById = async (workspaceId: string) => {
|
const fetchWorkspaceById = async (workspaceId: string) => {
|
||||||
@@ -639,3 +642,19 @@ export const useListWorkspacePkiCollections = ({ workspaceId }: { workspaceId: s
|
|||||||
enabled: Boolean(workspaceId)
|
enabled: Boolean(workspaceId)
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspaceId: string }) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: workspaceKeys.getWorkspaceCertificateTemplates(workspaceId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const {
|
||||||
|
data: { certificateTemplates }
|
||||||
|
} = await apiRequest.get<{ certificateTemplates: TCertificateTemplateListEntry[] }>(
|
||||||
|
`/api/v2/workspace/${workspaceId}/certificate-templates`
|
||||||
|
);
|
||||||
|
|
||||||
|
return { certificateTemplates };
|
||||||
|
},
|
||||||
|
enabled: Boolean(workspaceId)
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
+2
@@ -1,5 +1,6 @@
|
|||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { CertificateTemplatesSection } from "./components/CertificateTemplatesSection";
|
||||||
import { CertificatesSection } from "./components";
|
import { CertificatesSection } from "./components";
|
||||||
|
|
||||||
export const CertificatesTab = () => {
|
export const CertificatesTab = () => {
|
||||||
@@ -11,6 +12,7 @@ export const CertificatesTab = () => {
|
|||||||
animate={{ opacity: 1, translateX: 0 }}
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
exit={{ opacity: 0, translateX: 30 }}
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
>
|
>
|
||||||
|
<CertificateTemplatesSection />
|
||||||
<CertificatesSection />
|
<CertificatesSection />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
);
|
);
|
||||||
|
|||||||
+234
@@ -0,0 +1,234 @@
|
|||||||
|
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 {
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
Select,
|
||||||
|
SelectItem
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import {
|
||||||
|
CaStatus,
|
||||||
|
useCreateCertTemplate,
|
||||||
|
useGetCertTemplate,
|
||||||
|
useListWorkspaceCas,
|
||||||
|
useUpdateCertTemplate
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
import { caTypeToNameMap } from "@app/hooks/api/ca/constants";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
caId: z.string(),
|
||||||
|
name: z.string().min(1),
|
||||||
|
commonName: z.string().trim().min(1),
|
||||||
|
ttl: z.string().trim().min(1)
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof schema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["certificateTemplate"]>;
|
||||||
|
handlePopUpToggle: (
|
||||||
|
popUpName: keyof UsePopUpState<["certificateTemplate"]>,
|
||||||
|
state?: boolean
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CertificateTemplateModal = ({ popUp, handlePopUpToggle }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data: certTemplate } = useGetCertTemplate(
|
||||||
|
(popUp?.certificateTemplate?.data as { id: string })?.id || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
const { data: cas } = useListWorkspaceCas({
|
||||||
|
projectSlug: currentWorkspace?.slug ?? "",
|
||||||
|
status: CaStatus.ACTIVE
|
||||||
|
});
|
||||||
|
|
||||||
|
const { mutateAsync: createCertTemplate } = useCreateCertTemplate();
|
||||||
|
const { mutateAsync: updateCertTemplate } = useUpdateCertTemplate();
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(schema)
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (certTemplate) {
|
||||||
|
reset({
|
||||||
|
caId: certTemplate.caId,
|
||||||
|
name: certTemplate.name,
|
||||||
|
commonName: certTemplate.commonName,
|
||||||
|
ttl: certTemplate.ttl
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reset({
|
||||||
|
caId: "",
|
||||||
|
name: "",
|
||||||
|
commonName: "",
|
||||||
|
ttl: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [certTemplate]);
|
||||||
|
|
||||||
|
const onFormSubmit = async ({ caId, name, commonName, ttl }: FormData) => {
|
||||||
|
if (!currentWorkspace?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (certTemplate) {
|
||||||
|
await updateCertTemplate({
|
||||||
|
id: certTemplate.id,
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
caId,
|
||||||
|
name,
|
||||||
|
commonName,
|
||||||
|
ttl
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated certificate template",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await createCertTemplate({
|
||||||
|
projectId: currentWorkspace.id,
|
||||||
|
caId,
|
||||||
|
name,
|
||||||
|
commonName,
|
||||||
|
ttl
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully created certificate template",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reset();
|
||||||
|
handlePopUpToggle("certificateTemplate", false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to save changes",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.certificateTemplate?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => {
|
||||||
|
handlePopUpToggle("certificateTemplate", isOpen);
|
||||||
|
reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ModalContent title={certTemplate ? "Certificate Template" : "Create Certificate Template"}>
|
||||||
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="name"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Template Name"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="My Certificate Template" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="caId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Issuing CA"
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mt-4"
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{(cas || []).map(({ id, type, dn }) => (
|
||||||
|
<SelectItem value={id} key={`ca-${id}`}>
|
||||||
|
{`${caTypeToNameMap[type]}: ${dn}`}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue=""
|
||||||
|
name="commonName"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Common Name (CN)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="service.acme.com" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="ttl"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="TTL"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
isRequired
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="2 days, 1d, 2h, 1y, ..." />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handlePopUpToggle("certificateTemplate", false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import { Button, DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { usePopUp } from "@app/hooks";
|
||||||
|
import { useDeleteCertTemplate } from "@app/hooks/api";
|
||||||
|
|
||||||
|
import { CertificateTemplateModal } from "./CertificateTemplateModal";
|
||||||
|
import { CertificateTemplatesTable } from "./CertificateTemplatesTable";
|
||||||
|
|
||||||
|
export const CertificateTemplatesSection = () => {
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
|
"certificateTemplate",
|
||||||
|
"deleteCertificateTemplate"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { mutateAsync: deleteCertTemplate } = useDeleteCertTemplate();
|
||||||
|
|
||||||
|
const onRemoveCertificateTemplateSubmit = async (id: string) => {
|
||||||
|
if (!currentWorkspace?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await deleteCertTemplate({
|
||||||
|
id,
|
||||||
|
projectId: currentWorkspace.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await createNotification({
|
||||||
|
text: "Successfully deleted certificate template",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
handlePopUpClose("deleteCertificateTemplate");
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to delete certificate template",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex justify-between">
|
||||||
|
<p className="text-xl font-semibold text-mineshaft-100">Certificate Templates</p>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={ProjectPermissionSub.Certificates}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("certificateTemplate")}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Create Certificate Template
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
<CertificateTemplatesTable handlePopUpOpen={handlePopUpOpen} />
|
||||||
|
<CertificateTemplateModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteCertificateTemplate.isOpen}
|
||||||
|
title={`Are you sure want to delete the certificate template ${
|
||||||
|
(popUp?.deleteCertificateTemplate?.data as { name: string })?.name || ""
|
||||||
|
} from the project?`}
|
||||||
|
onChange={(isOpen) => handlePopUpToggle("deleteCertificateTemplate", isOpen)}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={() =>
|
||||||
|
onRemoveCertificateTemplateSubmit(
|
||||||
|
(popUp?.deleteCertificateTemplate?.data as { id: string })?.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+117
@@ -0,0 +1,117 @@
|
|||||||
|
import { faEllipsis, faGear, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context";
|
||||||
|
import { useListWorkspaceCertificateTemplates } from "@app/hooks/api";
|
||||||
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
handlePopUpOpen: (
|
||||||
|
popUpName: keyof UsePopUpState<["certificateTemplate", "deleteCertificateTemplate"]>,
|
||||||
|
data?: {
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CertificateTemplatesTable = ({ handlePopUpOpen }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const { data, isLoading } = useListWorkspaceCertificateTemplates({
|
||||||
|
workspaceId: currentWorkspace?.id ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th>Name</Th>
|
||||||
|
<Th>Certificate Authority</Th>
|
||||||
|
<Th />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isLoading && <TableSkeleton columns={2} innerKey="project-cas" />}
|
||||||
|
{!isLoading &&
|
||||||
|
data?.certificateTemplates.map((certificateTemplate) => {
|
||||||
|
return (
|
||||||
|
<Tr className="h-10" key={`certificate-${certificateTemplate.id}`}>
|
||||||
|
<Td>{certificateTemplate.name}</Td>
|
||||||
|
<Td>{certificateTemplate.caName}</Td>
|
||||||
|
<Td className="flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild className="rounded-lg">
|
||||||
|
<div className="hover:text-primary-400 data-[state=open]:text-primary-400">
|
||||||
|
<Tooltip content="More options">
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="start" className="p-1">
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("certificateTemplate", {
|
||||||
|
id: certificateTemplate.id
|
||||||
|
})
|
||||||
|
}
|
||||||
|
icon={<FontAwesomeIcon icon={faGear} />}
|
||||||
|
>
|
||||||
|
Manage
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={ProjectPermissionSub.CertificateTemplates}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
className={twMerge(
|
||||||
|
!isAllowed && "pointer-events-none cursor-not-allowed opacity-50"
|
||||||
|
)}
|
||||||
|
disabled={!isAllowed}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
onClick={() =>
|
||||||
|
handlePopUpOpen("deleteCertificateTemplate", {
|
||||||
|
id: certificateTemplate.id,
|
||||||
|
name: certificateTemplate.name
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete Template
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isLoading && !data?.certificateTemplates?.length && (
|
||||||
|
<EmptyState title="No certificate templates have been created" icon={faGear} />
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user