From f34370cb9df96b7462daac81f5300856cac61b60 Mon Sep 17 00:00:00 2001 From: = Date: Tue, 18 Feb 2025 23:39:31 +0530 Subject: [PATCH] feat: completed frontend --- frontend/src/components/v2/Modal/Modal.tsx | 6 +- frontend/src/hooks/api/dynamicSecret/types.ts | 1 + frontend/src/hooks/api/gateways/index.tsx | 2 +- frontend/src/hooks/api/gateways/mutation.tsx | 17 ++- frontend/src/hooks/api/gateways/queries.tsx | 16 ++- frontend/src/hooks/api/gateways/types.ts | 9 ++ .../GatewayListPage/GatewayListPage.tsx | 111 +++++++++++++++++- .../components/EditGatewayDetailsModal.tsx | 75 ++++++++++++ .../RolePermissionsSection.tsx | 6 + .../SqlDatabaseInputForm.tsx | 42 ++++++- .../EditDynamicSecretSqlProviderForm.tsx | 49 +++++++- 11 files changed, 316 insertions(+), 18 deletions(-) create mode 100644 frontend/src/pages/organization/Gateways/GatewayListPage/components/EditGatewayDetailsModal.tsx diff --git a/frontend/src/components/v2/Modal/Modal.tsx b/frontend/src/components/v2/Modal/Modal.tsx index 9b7baa921..f3b63eae4 100644 --- a/frontend/src/components/v2/Modal/Modal.tsx +++ b/frontend/src/components/v2/Modal/Modal.tsx @@ -45,7 +45,11 @@ export const ModalContent = forwardRef( )} style={{ maxHeight: "90%" }} > - {title && {title}} + {title && ( + + {title} + + )} { +export const useDeleteGatewayById = () => { const queryClient = useQueryClient(); return useMutation({ mutationFn: (id: string) => { return apiRequest.delete(`/api/v1/gateways/${id}`); }, onSuccess: () => { - queryClient.invalidateQueries(gatewaysQueryKeys.list()); + queryClient.invalidateQueries(gatewaysQueryKeys.list({})); + } + }); +}; + +export const useUpdateGatewayById = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: ({ id, name }: TUpdateGatewayDTO) => { + return apiRequest.patch(`/api/v1/gateways/${id}`, { name }); + }, + onSuccess: () => { + queryClient.invalidateQueries(gatewaysQueryKeys.list({})); } }); }; diff --git a/frontend/src/hooks/api/gateways/queries.tsx b/frontend/src/hooks/api/gateways/queries.tsx index bb05b17a4..a38fe39bd 100644 --- a/frontend/src/hooks/api/gateways/queries.tsx +++ b/frontend/src/hooks/api/gateways/queries.tsx @@ -2,16 +2,22 @@ import { queryOptions } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { TGateway } from "./types"; +import { TGateway, TListGatewayDTO } from "./types"; export const gatewaysQueryKeys = { allKey: () => ["gateways"], - listKey: () => [...gatewaysQueryKeys.allKey(), "list"], - list: () => + listKey: ({ projectId }: TListGatewayDTO) => [ + ...gatewaysQueryKeys.allKey(), + "list", + { projectId } + ], + list: ({ projectId }: TListGatewayDTO = {}) => queryOptions({ - queryKey: gatewaysQueryKeys.listKey(), + queryKey: gatewaysQueryKeys.listKey({ projectId }), queryFn: async () => { - const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways"); + const { data } = await apiRequest.get<{ gateways: TGateway[] }>("/api/v1/gateways", { + params: { projectId } + }); return data.gateways; } }) diff --git a/frontend/src/hooks/api/gateways/types.ts b/frontend/src/hooks/api/gateways/types.ts index 0af9d04b8..9d7917ef4 100644 --- a/frontend/src/hooks/api/gateways/types.ts +++ b/frontend/src/hooks/api/gateways/types.ts @@ -12,6 +12,15 @@ export type TGateway = { }; }; +export type TUpdateGatewayDTO = { + id: string; + name?: string; +}; + export type TDeleteGatewayDTO = { id: string; }; + +export type TListGatewayDTO = { + projectId?: string; +}; diff --git a/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx b/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx index 0ced1319f..d252f259a 100644 --- a/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx +++ b/frontend/src/pages/organization/Gateways/GatewayListPage/GatewayListPage.tsx @@ -3,17 +3,30 @@ import { Helmet } from "react-helmet"; import { faArrowUpRightFromSquare, faBookOpen, + faEdit, + faEllipsisV, faMagnifyingGlass, faPlug, - faSearch + faSearch, + faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useQuery } from "@tanstack/react-query"; import { format } from "date-fns"; +import { createNotification } from "@app/components/notifications"; +import { OrgPermissionCan } from "@app/components/permissions"; import { + DeleteActionModal, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, EmptyState, + IconButton, Input, + Modal, + ModalContent, PageHeader, Table, TableContainer, @@ -22,20 +35,44 @@ import { Td, Th, THead, + Tooltip, Tr } from "@app/components/v2"; import { + OrgGatewayPermissionActions, OrgPermissionAppConnectionActions, OrgPermissionSubjects } from "@app/context/OrgPermissionContext/types"; import { withPermission } from "@app/hoc"; -import { gatewaysQueryKeys } from "@app/hooks/api/gateways"; +import { usePopUp } from "@app/hooks"; +import { gatewaysQueryKeys, useDeleteGatewayById } from "@app/hooks/api/gateways"; + +import { EditGatewayDetailsModal } from "./components/EditGatewayDetailsModal"; export const GatewayListPage = withPermission( () => { const [search, setSearch] = useState(""); const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list()); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "deleteGateway", + "editDetails" + ] as const); + + const deleteGatewayById = useDeleteGatewayById(); + + const handleDeleteGateway = async () => { + await deleteGatewayById.mutateAsync((popUp.deleteGateway.data as { id: string }).id, { + onSuccess: () => { + handlePopUpToggle("deleteGateway"); + createNotification({ + type: "success", + text: "Successfully delete gateway" + }); + } + }); + }; + const filteredGateway = gateways?.filter((el) => el.name.toLowerCase().includes(search.toLowerCase()) ); @@ -100,13 +137,70 @@ export const GatewayListPage = withPermission( {filteredGateway?.map((el) => ( {el.name} - {format(new Date(el.issuedAt), "yyyy-MM-dd")} + {format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")} {el.identity.name} - + + + + + + + + + + + {(isAllowed: boolean) => ( + } + onClick={() => handlePopUpOpen("editDetails", el)} + > + Edit Details + + )} + + + {(isAllowed: boolean) => ( + } + className="text-red" + onClick={() => handlePopUpOpen("deleteGateway", el)} + > + Delete Gateway + + )} + + + + + ))} + handlePopUpToggle("editDetails", isOpen)} + > + + handlePopUpToggle("editDetails")} + /> + + {!isGatewayLoading && !filteredGateway?.length && ( )} + handlePopUpToggle("deleteGateway", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => handleDeleteGateway()} + /> diff --git a/frontend/src/pages/organization/Gateways/GatewayListPage/components/EditGatewayDetailsModal.tsx b/frontend/src/pages/organization/Gateways/GatewayListPage/components/EditGatewayDetailsModal.tsx new file mode 100644 index 000000000..1e07036ba --- /dev/null +++ b/frontend/src/pages/organization/Gateways/GatewayListPage/components/EditGatewayDetailsModal.tsx @@ -0,0 +1,75 @@ +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 } from "@app/components/v2"; +import { useUpdateGatewayById } from "@app/hooks/api"; +import { TGateway } from "@app/hooks/api/gateways/types"; + +type Props = { + gatewayDetails: TGateway; + onClose: () => void; +}; + +const schema = z.object({ + name: z.string() +}); + +export type FormData = z.infer; + +export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => { + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: gatewayDetails?.name + } + }); + + const updateGatewayById = useUpdateGatewayById(); + + const onFormSubmit = ({ name }: FormData) => { + if (isSubmitting) return; + updateGatewayById.mutate( + { + id: gatewayDetails.id, + name + }, + { + onSuccess: () => { + createNotification({ + type: "success", + text: "Successfully updated gateway" + }); + onClose(); + } + } + ); + }; + + return ( +
+ ( + + + + )} + /> +
+ + +
+ + ); +}; diff --git a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx index 9f66e358a..de9fbde80 100644 --- a/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx +++ b/frontend/src/pages/organization/RoleByIDPage/components/RolePermissionsSection/RolePermissionsSection.tsx @@ -14,6 +14,7 @@ import { TFormSchema } from "../OrgRoleModifySection.utils"; import { OrgPermissionAdminConsoleRow } from "./OrgPermissionAdminConsoleRow"; +import { OrgGatewayPermissionRow } from "./OrgPermissionGatewayRow"; import { OrgPermissionKmipRow } from "./OrgPermissionKmipRow"; import { OrgRoleWorkspaceRow } from "./OrgRoleWorkspaceRow"; import { RolePermissionRow } from "./RolePermissionRow"; @@ -171,6 +172,11 @@ export const RolePermissionsSection = ({ roleId }: Props) => { setValue={setValue} isEditable={isCustomRole} /> + { const valMs = ms(val); @@ -124,6 +127,8 @@ export const SqlDatabaseInputForm = ({ secretPath, projectSlug }: Props) => { + const { currentWorkspace } = useWorkspace(); + const { control, setValue, @@ -137,6 +142,9 @@ export const SqlDatabaseInputForm = ({ }); const createDynamicSecret = useCreateDynamicSecret(); + const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( + gatewaysQueryKeys.list({ projectId: currentWorkspace.id }) + ); const handleCreateDynamicSecret = async ({ name, maxTTL, provider, defaultTTL }: TForm) => { // wait till previous request is finished @@ -222,6 +230,36 @@ export const SqlDatabaseInputForm = ({ /> +
+ ( + + + + )} + /> +
Configuration diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx index 18543502f..165b94314 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx @@ -1,5 +1,6 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import ms from "ms"; import { z } from "zod"; @@ -18,7 +19,8 @@ import { SelectItem, TextArea } from "@app/components/v2"; -import { useUpdateDynamicSecret } from "@app/hooks/api"; +import { useWorkspace } from "@app/context"; +import { gatewaysQueryKeys, useUpdateDynamicSecret } from "@app/hooks/api"; import { SqlProviders, TDynamicSecret } from "@app/hooks/api/dynamicSecret/types"; const formSchema = z.object({ @@ -33,7 +35,8 @@ const formSchema = z.object({ creationStatement: z.string().min(1), revocationStatement: z.string().min(1), renewStatement: z.string().optional(), - ca: z.string().optional() + ca: z.string().optional(), + gatewayId: z.string().optional() }) .partial(), defaultTTL: z.string().superRefine((val, ctx) => { @@ -81,6 +84,7 @@ export const EditDynamicSecretSqlProviderForm = ({ }: Props) => { const { control, + watch, formState: { isSubmitting }, handleSubmit } = useForm({ @@ -94,8 +98,14 @@ export const EditDynamicSecretSqlProviderForm = ({ } } }); + const { currentWorkspace } = useWorkspace(); + const { data: projectGateways, isPending: isProjectGatewaysLoading } = useQuery( + gatewaysQueryKeys.list({ projectId: currentWorkspace.id }) + ); const updateDynamicSecret = useUpdateDynamicSecret(); + const selectedGatewayId = watch("inputs.gatewayId"); + const isGatewayInActive = projectGateways?.findIndex((el) => el.id === selectedGatewayId) === -1; const handleUpdateDynamicSecret = async ({ inputs, maxTTL, defaultTTL, newName }: TForm) => { // wait till previous request is finished @@ -109,7 +119,7 @@ export const EditDynamicSecretSqlProviderForm = ({ data: { maxTTL: maxTTL || undefined, defaultTTL, - inputs, + inputs: { ...inputs, gatewayId: isGatewayInActive ? null : inputs.gatewayId }, newName: newName === dynamicSecret.name ? undefined : newName } }); @@ -176,6 +186,39 @@ export const EditDynamicSecretSqlProviderForm = ({ />
+
+ ( + + + + )} + /> +
Configuration