mirror of
https://github.com/Infisical/infisical.git
synced 2025-04-17 19:37:38 +00:00
feat: completed frontend
This commit is contained in:
@ -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%" }}
|
||||
|
@ -54,6 +54,7 @@ export type TDynamicSecretProvider =
|
||||
revocationStatement: string;
|
||||
renewStatement?: string;
|
||||
ca?: string | undefined;
|
||||
gatewayId?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
|
@ -1,2 +1,2 @@
|
||||
export { useDeleteGateway } from "./mutation";
|
||||
export { useDeleteGatewayById, useUpdateGatewayById } from "./mutation";
|
||||
export { gatewaysQueryKeys } from "./queries";
|
||||
|
@ -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({}));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
})
|
||||
|
@ -12,6 +12,15 @@ export type TGateway = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TUpdateGatewayDTO = {
|
||||
id: string;
|
||||
name?: string;
|
||||
};
|
||||
|
||||
export type TDeleteGatewayDTO = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type TListGatewayDTO = {
|
||||
projectId?: string;
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
Reference in New Issue
Block a user