mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 18:28:12 +00:00
feat: completed KMIP client overview
This commit is contained in:
@@ -208,21 +208,17 @@ export const registerKmipRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const {
|
|
||||||
query: { projectId }
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({
|
const { kmipClients, totalCount } = await server.services.kmip.listKmipClientsByProjectId({
|
||||||
projectId,
|
|
||||||
actor: req.permission.type,
|
actor: req.permission.type,
|
||||||
actorId: req.permission.id,
|
actorId: req.permission.id,
|
||||||
actorAuthMethod: req.permission.authMethod,
|
actorAuthMethod: req.permission.authMethod,
|
||||||
actorOrgId: req.permission.orgId
|
actorOrgId: req.permission.orgId,
|
||||||
|
...req.query
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.services.auditLog.createAuditLog({
|
await server.services.auditLog.createAuditLog({
|
||||||
...req.auditLogInfo,
|
...req.auditLogInfo,
|
||||||
projectId,
|
projectId: req.query.projectId,
|
||||||
event: {
|
event: {
|
||||||
type: EventType.GET_KMIP_CLIENTS,
|
type: EventType.GET_KMIP_CLIENTS,
|
||||||
metadata: {
|
metadata: {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./mutation";
|
||||||
|
export * from "./queries";
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { kmipKeys } from "./queries";
|
||||||
|
import { TCreateKmipClient, TDeleteKmipClient, TUpdateKmipClient } from "./types";
|
||||||
|
|
||||||
|
export const useCreateKmipClient = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (payload: TCreateKmipClient) => {
|
||||||
|
const { data } = await apiRequest.post("/api/v1/kmip/clients", payload);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateKmipClient = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id, name, description, permissions }: TUpdateKmipClient) => {
|
||||||
|
const { data } = await apiRequest.patch(`/api/v1/kmip/clients/${id}`, {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
permissions
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteKmipClients = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({ id }: TDeleteKmipClient) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/kmip/clients/${id}`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId }) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: kmipKeys.getKmipClientsByProjectId({ projectId })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
|
import { OrderByDirection } from "../generic/types";
|
||||||
|
import { KmipClientOrderBy, TListProjectKmipClientsDTO, TProjectKmipClientList } from "./types";
|
||||||
|
|
||||||
|
export const kmipKeys = {
|
||||||
|
getKmipClientsByProjectId: ({ projectId, ...filters }: TListProjectKmipClientsDTO) =>
|
||||||
|
[projectId, filters] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetKmipClientsByProjectId = (
|
||||||
|
{
|
||||||
|
projectId,
|
||||||
|
offset = 0,
|
||||||
|
limit = 100,
|
||||||
|
orderBy = KmipClientOrderBy.Name,
|
||||||
|
orderDirection = OrderByDirection.ASC,
|
||||||
|
search = ""
|
||||||
|
}: TListProjectKmipClientsDTO,
|
||||||
|
options?: Omit<
|
||||||
|
UseQueryOptions<
|
||||||
|
TProjectKmipClientList,
|
||||||
|
unknown,
|
||||||
|
TProjectKmipClientList,
|
||||||
|
ReturnType<typeof kmipKeys.getKmipClientsByProjectId>
|
||||||
|
>,
|
||||||
|
"queryKey" | "queryFn"
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: kmipKeys.getKmipClientsByProjectId({
|
||||||
|
projectId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
search
|
||||||
|
}),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TProjectKmipClientList>("/api/v1/kmip/clients", {
|
||||||
|
params: { projectId, offset, limit, search, orderBy, orderDirection }
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
enabled: Boolean(projectId) && (options?.enabled ?? true),
|
||||||
|
placeholderData: (previousData) => previousData,
|
||||||
|
...options
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { OrderByDirection } from "../generic/types";
|
||||||
|
|
||||||
|
export enum KmipPermission {
|
||||||
|
Create = "create",
|
||||||
|
Locate = "locate",
|
||||||
|
Check = "check",
|
||||||
|
Get = "get"
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TKmipClient = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
permissions: KmipPermission[];
|
||||||
|
projectId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ProjectRef = { projectId: string };
|
||||||
|
type KeyRef = { id: string };
|
||||||
|
|
||||||
|
export type TCreateKmipClient = Pick<TKmipClient, "name" | "description" | "permissions"> &
|
||||||
|
ProjectRef;
|
||||||
|
|
||||||
|
export type TUpdateKmipClient = KeyRef &
|
||||||
|
Partial<Pick<TKmipClient, "name" | "description" | "permissions">> &
|
||||||
|
ProjectRef;
|
||||||
|
|
||||||
|
export type TProjectKmipClientList = {
|
||||||
|
kmipClients: TKmipClient[];
|
||||||
|
totalCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TDeleteKmipClient = KeyRef & ProjectRef;
|
||||||
|
|
||||||
|
export type TListProjectKmipClientsDTO = {
|
||||||
|
projectId: string;
|
||||||
|
offset?: number;
|
||||||
|
limit?: number;
|
||||||
|
orderBy?: KmipClientOrderBy;
|
||||||
|
orderDirection?: OrderByDirection;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum KmipClientOrderBy {
|
||||||
|
Name = "name"
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import { useTranslation } from "react-i18next";
|
|||||||
|
|
||||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
import { PageHeader } from "@app/components/v2";
|
import { PageHeader } from "@app/components/v2";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionKmipActions, ProjectPermissionSub } from "@app/context";
|
||||||
|
|
||||||
|
import { KmipClientTable } from "./components/KmipClientTable";
|
||||||
|
|
||||||
export const KmipPage = () => {
|
export const KmipPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -15,14 +17,17 @@ export const KmipPage = () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||||
<div className="mx-auto mb-6 w-full max-w-7xl">
|
<div className="mx-auto mb-6 w-full max-w-7xl">
|
||||||
<PageHeader title="KMIP clients" description="Manage KMIP clients" />
|
<PageHeader
|
||||||
|
title="KMIP"
|
||||||
|
description="Integrate with Infisical KMS via Key Management Interoperability Protocol."
|
||||||
|
/>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
passThrough={false}
|
passThrough={false}
|
||||||
renderGuardBanner
|
renderGuardBanner
|
||||||
I={ProjectPermissionActions.Read}
|
I={ProjectPermissionKmipActions.ReadClients}
|
||||||
a={ProjectPermissionSub.Cmek}
|
a={ProjectPermissionSub.Kmip}
|
||||||
>
|
>
|
||||||
<div>KMIP clients here</div>
|
<KmipClientTable />
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import { DeleteActionModal } from "@app/components/v2";
|
||||||
|
import { useDeleteKmipClients } from "@app/hooks/api/kmip";
|
||||||
|
import { TKmipClient } from "@app/hooks/api/kmip/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
kmipClient: TKmipClient;
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeleteKmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => {
|
||||||
|
const deleteKmipClients = useDeleteKmipClients();
|
||||||
|
|
||||||
|
if (!kmipClient) return null;
|
||||||
|
|
||||||
|
const { id, projectId, name } = kmipClient;
|
||||||
|
|
||||||
|
const handleDeleteKmipClient = async () => {
|
||||||
|
try {
|
||||||
|
await deleteKmipClients.mutateAsync({
|
||||||
|
id,
|
||||||
|
projectId
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "KMIP client successfully deleted",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
|
||||||
|
onOpenChange(false);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
const error = err as any;
|
||||||
|
const text = error?.response?.data?.message ?? "Failed to delete KMIP client";
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
title={`Are you sure want to delete ${name}?`}
|
||||||
|
onChange={onOpenChange}
|
||||||
|
deleteKey="confirm"
|
||||||
|
onDeleteApproved={handleDeleteKmipClient}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
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,
|
||||||
|
Checkbox,
|
||||||
|
FormControl,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalContent,
|
||||||
|
TextArea
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useCreateKmipClient, useUpdateKmipClient } from "@app/hooks/api/kmip";
|
||||||
|
import { KmipPermission, TKmipClient } from "@app/hooks/api/kmip/types";
|
||||||
|
|
||||||
|
const KMIP_PERMISSIONS_OPTIONS = [
|
||||||
|
{ value: KmipPermission.Check, label: "Check" },
|
||||||
|
{ value: KmipPermission.Create, label: "Create" },
|
||||||
|
{ value: KmipPermission.Get, label: "Get" },
|
||||||
|
{ value: KmipPermission.Locate, label: "Locate" }
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
name: z.string().trim().min(1),
|
||||||
|
description: z.string().max(500).optional(),
|
||||||
|
permissions: z.object({
|
||||||
|
[KmipPermission.Check]: z.boolean().optional(),
|
||||||
|
[KmipPermission.Create]: z.boolean().optional(),
|
||||||
|
[KmipPermission.Get]: z.boolean().optional(),
|
||||||
|
[KmipPermission.Locate]: z.boolean().optional()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
kmipClient?: TKmipClient | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormProps = Pick<Props, "kmipClient"> & {
|
||||||
|
onComplete: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const KmipClientForm = ({ onComplete, kmipClient }: FormProps) => {
|
||||||
|
const createKmipClient = useCreateKmipClient();
|
||||||
|
const updateKmipClient = useUpdateKmipClient();
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const projectId = currentWorkspace.id;
|
||||||
|
const isUpdate = !!kmipClient;
|
||||||
|
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
handleSubmit,
|
||||||
|
register,
|
||||||
|
formState: { isSubmitting, errors }
|
||||||
|
} = useForm<FormData>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: kmipClient?.name,
|
||||||
|
description: kmipClient?.description,
|
||||||
|
permissions: Object.fromEntries((kmipClient?.permissions || []).map((name) => [name, true]))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleKmipClientSubmit = async ({ permissions, name, description }: FormData) => {
|
||||||
|
const mutation = isUpdate
|
||||||
|
? updateKmipClient.mutateAsync({
|
||||||
|
id: kmipClient.id,
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
permissions: Object.entries(permissions)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key as KmipPermission)
|
||||||
|
})
|
||||||
|
: createKmipClient.mutateAsync({
|
||||||
|
projectId,
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
permissions: Object.entries(permissions)
|
||||||
|
.filter(([, value]) => value)
|
||||||
|
.map(([key]) => key as KmipPermission)
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mutation;
|
||||||
|
createNotification({
|
||||||
|
text: `Successfully ${isUpdate ? "updated" : "added"} KMIP client`,
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
|
onComplete();
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: `Failed to ${isUpdate ? "update" : "add"} KMIP client`,
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(handleKmipClientSubmit)}>
|
||||||
|
<FormControl
|
||||||
|
errorText={errors.name?.message}
|
||||||
|
isError={Boolean(errors.name?.message)}
|
||||||
|
label="Name"
|
||||||
|
>
|
||||||
|
<Input autoFocus placeholder="My KMIP Client" {...register("name")} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl
|
||||||
|
label="Description (optional)"
|
||||||
|
errorText={errors.description?.message}
|
||||||
|
isError={Boolean(errors.description?.message)}
|
||||||
|
>
|
||||||
|
<TextArea
|
||||||
|
className="max-h-[20rem] min-h-[10rem] min-w-full max-w-full"
|
||||||
|
{...register("description")}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="permissions"
|
||||||
|
render={({ field: { onChange, value }, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl label="Key Usage" errorText={error?.message} isError={Boolean(error)}>
|
||||||
|
<div className="mb-7 mt-2 grid grid-cols-2 gap-2">
|
||||||
|
{KMIP_PERMISSIONS_OPTIONS.map(({ label, value: optionValue }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
id={optionValue}
|
||||||
|
key={optionValue}
|
||||||
|
className="data-[state=checked]:bg-primary"
|
||||||
|
isChecked={value[optionValue]}
|
||||||
|
onCheckedChange={(state) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[optionValue]: state
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Checkbox>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Button
|
||||||
|
className="mr-4"
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
|
isDisabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isUpdate ? "Update" : "Add"} KMIP client
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KmipClientModal = ({ isOpen, onOpenChange, kmipClient }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent title={`${kmipClient ? "Update" : "Add"} KMIP Client`}>
|
||||||
|
<KmipClientForm onComplete={() => onOpenChange(false)} kmipClient={kmipClient} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
import {
|
||||||
|
faArrowDown,
|
||||||
|
faArrowUp,
|
||||||
|
faArrowUpRightFromSquare,
|
||||||
|
faEdit,
|
||||||
|
faEllipsis,
|
||||||
|
faMagnifyingGlass,
|
||||||
|
faPlus,
|
||||||
|
faTrash,
|
||||||
|
faUser
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
EmptyState,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
Pagination,
|
||||||
|
Spinner,
|
||||||
|
Table,
|
||||||
|
TableContainer,
|
||||||
|
TableSkeleton,
|
||||||
|
TBody,
|
||||||
|
Td,
|
||||||
|
Th,
|
||||||
|
THead,
|
||||||
|
Tooltip,
|
||||||
|
Tr
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
ProjectPermissionKmipActions,
|
||||||
|
ProjectPermissionSub,
|
||||||
|
useProjectPermission,
|
||||||
|
useWorkspace
|
||||||
|
} from "@app/context";
|
||||||
|
import { usePagination, usePopUp, useResetPageHelper } from "@app/hooks";
|
||||||
|
import { OrderByDirection } from "@app/hooks/api/generic/types";
|
||||||
|
import { useGetKmipClientsByProjectId } from "@app/hooks/api/kmip";
|
||||||
|
import { KmipClientOrderBy, TKmipClient } from "@app/hooks/api/kmip/types";
|
||||||
|
|
||||||
|
import { DeleteKmipClientModal } from "./DeleteKmipClientModal";
|
||||||
|
import { KmipClientModal } from "./KmipClientModal";
|
||||||
|
|
||||||
|
export const KmipClientTable = () => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
|
const projectId = currentWorkspace?.id ?? "";
|
||||||
|
|
||||||
|
const {
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
orderBy,
|
||||||
|
orderDirection,
|
||||||
|
setOrderDirection,
|
||||||
|
search,
|
||||||
|
debouncedSearch,
|
||||||
|
setPage,
|
||||||
|
setSearch,
|
||||||
|
perPage,
|
||||||
|
page,
|
||||||
|
setPerPage
|
||||||
|
} = usePagination(KmipClientOrderBy.Name);
|
||||||
|
|
||||||
|
const { data, isPending, isFetching } = useGetKmipClientsByProjectId({
|
||||||
|
projectId,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
search: debouncedSearch,
|
||||||
|
orderBy,
|
||||||
|
orderDirection
|
||||||
|
});
|
||||||
|
|
||||||
|
const { permission } = useProjectPermission();
|
||||||
|
|
||||||
|
const { kmipClients = [], totalCount = 0 } = data ?? {};
|
||||||
|
useResetPageHelper({
|
||||||
|
totalCount,
|
||||||
|
offset,
|
||||||
|
setPage
|
||||||
|
});
|
||||||
|
|
||||||
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
|
"upsertKmipClient",
|
||||||
|
"deleteKmipClient"
|
||||||
|
] as const);
|
||||||
|
|
||||||
|
const handleSort = () => {
|
||||||
|
setOrderDirection((prev) =>
|
||||||
|
prev === OrderByDirection.ASC ? OrderByDirection.DESC : OrderByDirection.ASC
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const cannotEditKmipClient = permission.cannot(
|
||||||
|
ProjectPermissionKmipActions.UpdateClients,
|
||||||
|
ProjectPermissionSub.Kmip
|
||||||
|
);
|
||||||
|
|
||||||
|
const cannotDeleteKmipClient = permission.cannot(
|
||||||
|
ProjectPermissionKmipActions.DeleteClients,
|
||||||
|
ProjectPermissionSub.Kmip
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
key="kmip-clients-tab"
|
||||||
|
transition={{ duration: 0.15 }}
|
||||||
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
|
>
|
||||||
|
<div className="mb-6 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-4">
|
||||||
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
<p className="whitespace-nowrap text-xl font-semibold text-mineshaft-100">KMIP Clients</p>
|
||||||
|
<div className="flex w-full justify-end pr-4">
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
href="https://infisical.com/docs/documentation/platform/kms"
|
||||||
|
>
|
||||||
|
<span className="flex w-max cursor-pointer items-center rounded-md border border-mineshaft-500 bg-mineshaft-600 px-4 py-2 text-mineshaft-200 duration-200 hover:border-primary/40 hover:bg-primary/10 hover:text-white">
|
||||||
|
Documentation{" "}
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={faArrowUpRightFromSquare}
|
||||||
|
className="mb-[0.06rem] ml-1 text-xs"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionKmipActions.CreateClients}
|
||||||
|
a={ProjectPermissionSub.Kmip}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
colorSchema="primary"
|
||||||
|
type="submit"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen("upsertKmipClient", null)}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
Add KMIP Client
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</div>
|
||||||
|
<Input
|
||||||
|
containerClassName="mb-4"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
|
placeholder="Search clients by name..."
|
||||||
|
/>
|
||||||
|
<TableContainer>
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr className="h-14">
|
||||||
|
<Th>
|
||||||
|
<div className="flex items-center">
|
||||||
|
Name
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
className="ml-2"
|
||||||
|
ariaLabel="sort"
|
||||||
|
onClick={handleSort}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={orderDirection === OrderByDirection.DESC ? faArrowUp : faArrowDown}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</Th>
|
||||||
|
<Th>Description</Th>
|
||||||
|
<Th>Permissions</Th>
|
||||||
|
<Th className="w-16">{isFetching ? <Spinner size="xs" /> : null}</Th>
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{isPending && <TableSkeleton columns={3} innerKey="project-kmip-clients" />}
|
||||||
|
{!isPending &&
|
||||||
|
kmipClients.length > 0 &&
|
||||||
|
kmipClients.map((kmipClient) => {
|
||||||
|
const { name, id, description, permissions } = kmipClient;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr className="group h-10 hover:bg-mineshaft-700" key={`st-v3-${id}`}>
|
||||||
|
<Td>{name}</Td>
|
||||||
|
<Td className="max-w-80 break-all">{description}</Td>
|
||||||
|
<Td className="max-w-40 break-all">{[permissions.join(", ")]}</Td>
|
||||||
|
<Td className="flex justify-end">
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="primary"
|
||||||
|
className="ml-4 p-0 data-[state=open]:text-primary-400"
|
||||||
|
ariaLabel="More options"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon size="lg" icon={faEllipsis} />
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="min-w-[160px]">
|
||||||
|
<Tooltip
|
||||||
|
content={cannotEditKmipClient ? "Access Restricted" : ""}
|
||||||
|
position="left"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handlePopUpOpen("upsertKmipClient", kmipClient)}
|
||||||
|
icon={<FontAwesomeIcon icon={faEdit} />}
|
||||||
|
iconPos="left"
|
||||||
|
isDisabled={cannotEditKmipClient}
|
||||||
|
>
|
||||||
|
Edit KMIP Client
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip
|
||||||
|
content={cannotDeleteKmipClient ? "Access Restricted" : ""}
|
||||||
|
position="left"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handlePopUpOpen("deleteKmipClient", kmipClient)}
|
||||||
|
icon={<FontAwesomeIcon icon={faTrash} />}
|
||||||
|
iconPos="left"
|
||||||
|
isDisabled={cannotDeleteKmipClient}
|
||||||
|
>
|
||||||
|
Delete KMIP Client
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
{!isPending && totalCount > 0 && (
|
||||||
|
<Pagination
|
||||||
|
count={totalCount}
|
||||||
|
page={page}
|
||||||
|
perPage={perPage}
|
||||||
|
onChangePage={(newPage) => setPage(newPage)}
|
||||||
|
onChangePerPage={(newPerPage) => setPerPage(newPerPage)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isPending && kmipClients.length === 0 && (
|
||||||
|
<EmptyState
|
||||||
|
title={
|
||||||
|
debouncedSearch.trim().length > 0
|
||||||
|
? "No KMIP clients match search filter"
|
||||||
|
: "No KMIP clients have been added to this project"
|
||||||
|
}
|
||||||
|
icon={faUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</TableContainer>
|
||||||
|
<DeleteKmipClientModal
|
||||||
|
isOpen={popUp.deleteKmipClient.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("deleteKmipClient", isOpen)}
|
||||||
|
kmipClient={popUp.deleteKmipClient.data as TKmipClient}
|
||||||
|
/>
|
||||||
|
<KmipClientModal
|
||||||
|
isOpen={popUp.upsertKmipClient.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("upsertKmipClient", isOpen)}
|
||||||
|
kmipClient={popUp.upsertKmipClient.data as TKmipClient | null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user