feat: completed frontend

This commit is contained in:
=
2025-02-18 23:39:31 +05:30
parent 78718cd299
commit f34370cb9d
11 changed files with 316 additions and 18 deletions

View File

@ -45,7 +45,11 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
)}
style={{ maxHeight: "90%" }}
>
{title && <CardTitle subTitle={subTitle}>{title}</CardTitle>}
{title && (
<DialogPrimitive.Title>
<CardTitle subTitle={subTitle}>{title}</CardTitle>
</DialogPrimitive.Title>
)}
<CardBody
className={twMerge("overflow-y-auto overflow-x-hidden", bodyClassName)}
style={{ maxHeight: "90%" }}

View File

@ -54,6 +54,7 @@ export type TDynamicSecretProvider =
revocationStatement: string;
renewStatement?: string;
ca?: string | undefined;
gatewayId?: string;
};
}
| {

View File

@ -1,2 +1,2 @@
export { useDeleteGateway } from "./mutation";
export { useDeleteGatewayById, useUpdateGatewayById } from "./mutation";
export { gatewaysQueryKeys } from "./queries";

View File

@ -3,15 +3,28 @@ import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { gatewaysQueryKeys } from "./queries";
import { TUpdateGatewayDTO } from "./types";
export const useDeleteGateway = () => {
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({}));
}
});
};

View File

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

View File

@ -12,6 +12,15 @@ export type TGateway = {
};
};
export type TUpdateGatewayDTO = {
id: string;
name?: string;
};
export type TDeleteGatewayDTO = {
id: string;
};
export type TListGatewayDTO = {
projectId?: string;
};

View File

@ -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) => (
<Tr key={el.id}>
<Td>{el.name}</Td>
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd")}</Td>
<Td>{format(new Date(el.issuedAt), "yyyy-MM-dd hh:mm:ss aaa")}</Td>
<Td>{el.identity.name}</Td>
<Td />
<Td className="w-5">
<Tooltip className="max-w-sm text-center" content="Options">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="Options"
colorSchema="secondary"
className="w-6"
variant="plain"
>
<FontAwesomeIcon icon={faEllipsisV} />
</IconButton>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<OrgPermissionCan
I={OrgGatewayPermissionActions.Edit}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faEdit} />}
onClick={() => handlePopUpOpen("editDetails", el)}
>
Edit Details
</DropdownMenuItem>
)}
</OrgPermissionCan>
<OrgPermissionCan
I={OrgPermissionAppConnectionActions.Delete}
a={OrgPermissionSubjects.AppConnections}
>
{(isAllowed: boolean) => (
<DropdownMenuItem
isDisabled={!isAllowed}
icon={<FontAwesomeIcon icon={faTrash} />}
className="text-red"
onClick={() => handlePopUpOpen("deleteGateway", el)}
>
Delete Gateway
</DropdownMenuItem>
)}
</OrgPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</Tooltip>
</Td>
</Tr>
))}
</TBody>
</Table>
<Modal
isOpen={popUp.editDetails.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("editDetails", isOpen)}
>
<ModalContent title="Edit Gateway">
<EditGatewayDetailsModal
gatewayDetails={popUp.editDetails.data}
onClose={() => handlePopUpToggle("editDetails")}
/>
</ModalContent>
</Modal>
{!isGatewayLoading && !filteredGateway?.length && (
<EmptyState
title={
@ -117,6 +211,15 @@ export const GatewayListPage = withPermission(
icon={gateways?.length ? faSearch : faPlug}
/>
)}
<DeleteActionModal
isOpen={popUp.deleteGateway.isOpen}
title={`Are you sure want to delete gateway ${
(popUp?.deleteGateway?.data as { name: string })?.name || ""
}?`}
onChange={(isOpen) => handlePopUpToggle("deleteGateway", isOpen)}
deleteKey="confirm"
onDeleteApproved={() => handleDeleteGateway()}
/>
</TableContainer>
</div>
</div>

View File

@ -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<typeof schema>;
export const EditGatewayDetailsModal = ({ gatewayDetails, onClose }: Props) => {
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
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 (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
render={({ field, fieldState: { error } }) => (
<FormControl label="Name" isError={Boolean(error)} errorText={error?.message} isRequired>
<Input {...field} placeholder="db-subnet-1" />
</FormControl>
)}
/>
<div className="mt-4 flex items-center">
<Button className="mr-4" size="sm" type="submit" isLoading={isSubmitting}>
Update
</Button>
<Button colorSchema="secondary" variant="plain" onClick={() => onClose()}>
Cancel
</Button>
</div>
</form>
);
};

View File

@ -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}
/>
<OrgGatewayPermissionRow
control={control}
setValue={setValue}
isEditable={isCustomRole}
/>
<OrgRoleWorkspaceRow
control={control}
setValue={setValue}

View File

@ -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 { useCreateDynamicSecret } from "@app/hooks/api";
import { useWorkspace } from "@app/context";
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types";
const formSchema = z.object({
@ -32,7 +34,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()
}),
defaultTTL: z.string().superRefine((val, ctx) => {
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 = ({
/>
</div>
</div>
<div>
<Controller
control={control}
name="provider.gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Select
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Select gateway"
position="popper"
>
{projectGateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div>
<div className="mb-4 mt-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
Configuration

View File

@ -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<TForm>({
@ -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 = ({
/>
</div>
</div>
<div>
<Controller
control={control}
name="inputs.gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message) || isGatewayInActive}
errorText={
isGatewayInActive ? `Gateway ${selectedGatewayId} is removed` : error?.message
}
label="Gateway"
helperText=""
>
<Select
value={value}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isProjectGatewaysLoading}
placeholder="Select gateway"
position="popper"
>
{projectGateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<div>
<div className="mb-4 border-b border-b-mineshaft-600 pb-2">Configuration</div>
<div className="flex flex-col">