mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(secret-approval): implemented frontend ui for secret policies
This commit is contained in:
@@ -22,14 +22,14 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
|
||||
) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay
|
||||
className={twMerge("fixed inset-0 z-[70] h-full w-full animate-fadeIn", overlayClassName)}
|
||||
className={twMerge("fixed inset-0 z-30 h-full w-full animate-fadeIn", overlayClassName)}
|
||||
style={{ backgroundColor: "rgba(0, 0, 0, 0.7)" }}
|
||||
/>
|
||||
<DialogPrimitive.Content {...props} ref={forwardedRef}>
|
||||
<Card
|
||||
isRounded
|
||||
className={twMerge(
|
||||
"fixed top-1/2 left-1/2 z-[90] dark:[color-scheme:dark] max-h-screen overflow-y-auto thin-scrollbar max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
||||
"fixed top-1/2 left-1/2 z-30 dark:[color-scheme:dark] max-h-screen overflow-y-auto thin-scrollbar max-w-lg -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -7,6 +7,7 @@ export * from "./integrations";
|
||||
export * from "./keys";
|
||||
export * from "./organization";
|
||||
export * from "./roles";
|
||||
export * from "./secretApproval";
|
||||
export * from "./secretFolders";
|
||||
export * from "./secretImports";
|
||||
export * from "./secrets";
|
||||
|
||||
6
frontend/src/hooks/api/secretApproval/index.tsx
Normal file
6
frontend/src/hooks/api/secretApproval/index.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
export {
|
||||
useCreateSecretApprovalPolicy,
|
||||
useDeleteSecretApprovalPolicy,
|
||||
useUpdateSecretApprovalPolicy
|
||||
} from "./mutation";
|
||||
export { useGetSecretApprovalPolicies } from "./queries";
|
||||
58
frontend/src/hooks/api/secretApproval/mutation.tsx
Normal file
58
frontend/src/hooks/api/secretApproval/mutation.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { secretApprovalKeys } from "./queries";
|
||||
import { TCreateSecretPolicyDTO, TDeleteSecretPolicyDTO, TUpdateSecretPolicyDTO } from "./types";
|
||||
|
||||
export const useCreateSecretApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
|
||||
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath }) => {
|
||||
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
|
||||
environment,
|
||||
workspaceId,
|
||||
approvals,
|
||||
approvers,
|
||||
secretPath
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useUpdateSecretApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
|
||||
mutationFn: async ({ id, approvers, approvals, secretPath }) => {
|
||||
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
|
||||
approvals,
|
||||
approvers,
|
||||
secretPath
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const useDeleteSecretApprovalPolicy = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
|
||||
mutationFn: async ({ id }) => {
|
||||
const { data } = await apiRequest.delete(`/api/v1/secret-approvals/${id}`);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { workspaceId }) => {
|
||||
queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId));
|
||||
}
|
||||
});
|
||||
};
|
||||
36
frontend/src/hooks/api/secretApproval/queries.tsx
Normal file
36
frontend/src/hooks/api/secretApproval/queries.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
|
||||
|
||||
import { apiRequest } from "@app/config/request";
|
||||
|
||||
import { TSecretApprovalPolicy } from "./types";
|
||||
|
||||
export const secretApprovalKeys = {
|
||||
getApprovalPolicies: (workspaceId: string) =>
|
||||
[{ workspaceId }, "secret-approval-policies"] as const
|
||||
};
|
||||
|
||||
const fetchApprovalPolicies = async (workspaceId: string) => {
|
||||
const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>(
|
||||
"/api/v1/secret-approvals",
|
||||
{ params: { workspaceId } }
|
||||
);
|
||||
return data.approvals;
|
||||
};
|
||||
|
||||
export const useGetSecretApprovalPolicies = ({
|
||||
workspaceId,
|
||||
options = {}
|
||||
}: { workspaceId: string } & {
|
||||
options?: UseQueryOptions<
|
||||
TSecretApprovalPolicy[],
|
||||
unknown,
|
||||
TSecretApprovalPolicy[],
|
||||
ReturnType<typeof secretApprovalKeys.getApprovalPolicies>
|
||||
>;
|
||||
}) =>
|
||||
useQuery({
|
||||
queryKey: secretApprovalKeys.getApprovalPolicies(workspaceId),
|
||||
queryFn: () => fetchApprovalPolicies(workspaceId),
|
||||
...options,
|
||||
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
|
||||
});
|
||||
31
frontend/src/hooks/api/secretApproval/types.ts
Normal file
31
frontend/src/hooks/api/secretApproval/types.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
export type TSecretApprovalPolicy = {
|
||||
_id: string;
|
||||
workspace: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
approvers: string[];
|
||||
approvals: number;
|
||||
};
|
||||
|
||||
export type TCreateSecretPolicyDTO = {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
approvers?: string[];
|
||||
approvals?: number;
|
||||
};
|
||||
|
||||
export type TUpdateSecretPolicyDTO = {
|
||||
id: string;
|
||||
approvers?: string[];
|
||||
secretPath?: string;
|
||||
approvals?: number;
|
||||
// for invalidating list
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type TDeleteSecretPolicyDTO = {
|
||||
id: string;
|
||||
// for invalidating list
|
||||
workspaceId: string;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ export type { IntegrationAuth } from "./integrationAuth/types";
|
||||
export type { TCloudIntegration, TIntegration } from "./integrations/types";
|
||||
export type { UserWsKeyPair } from "./keys/types";
|
||||
export type { Organization } from "./organization/types";
|
||||
export type { TSecretApprovalPolicy } from "./secretApproval/types";
|
||||
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
|
||||
export type { SubscriptionPlan } from "./subscriptions/types";
|
||||
export type { WsTag } from "./tags/types";
|
||||
|
||||
@@ -475,18 +475,20 @@ export const AppLayout = ({ children }: LayoutProps) => {
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href={`/project/${currentWorkspace?._id}/approval`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
|
||||
}
|
||||
icon="system-outline-126-verified"
|
||||
>
|
||||
Secret Change Management
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
{process.env.NEXT_PUBLIC_SECRET_APPROVAL === "true" && (
|
||||
<Link href={`/project/${currentWorkspace?._id}/approval`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
isSelected={
|
||||
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
|
||||
}
|
||||
icon="system-outline-126-verified"
|
||||
>
|
||||
Admin Panel
|
||||
</MenuItem>
|
||||
</a>
|
||||
</Link>
|
||||
)}
|
||||
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
|
||||
<a>
|
||||
<MenuItem
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Head from "next/head";
|
||||
|
||||
import { SecretApprovalListPage } from "@app/views/SecretApproval/SecretApprovalListPage";
|
||||
import { SecretApprovalPage } from "@app/views/SecretApprovalPage";
|
||||
|
||||
const SecretApproval = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -16,7 +16,7 @@ const SecretApproval = () => {
|
||||
<meta name="og:description" content={String(t("approval.og-description"))} />
|
||||
</Head>
|
||||
<div className="h-full">
|
||||
<SecretApprovalListPage />
|
||||
<SecretApprovalPage />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { faCheck, faCodeBranch, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, Table, TableContainer, TBody, Td, Th, THead, Tr } from "@app/components/v2";
|
||||
|
||||
export const SecretApprovalListPage = () => {
|
||||
return (
|
||||
<div className="container mx-auto bg-bunker-800 text-white w-full h-full">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">Admin Panels</p>
|
||||
</div>
|
||||
<div className="rounded-md bg-mineshaft-800 text-gray-300">
|
||||
<div className="p-4 px-8 flex items-center space-x-8">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />
|
||||
27 Open
|
||||
</div>
|
||||
<div className="text-gray-500">
|
||||
<FontAwesomeIcon icon={faCheck} className="mr-2" />
|
||||
27 Closed
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col border-t border-mineshaft-600">
|
||||
<div className="flex flex-col px-8 py-4">
|
||||
<div className="mb-1">
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="mr-2" />2 secrets added and 1 deleted
|
||||
</div>
|
||||
<span className="text-xs text-gray-500">
|
||||
Opened 2 hours ago by akhilmhdh - Review required
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-4 p-4 rounded-md bg-bunker-600 text-gray-300">
|
||||
<div className="mb-4 text-lg">
|
||||
Request for <span className="text-yellow-600">secret change</span>
|
||||
</div>
|
||||
<div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="min-table-row">Secret</Th>
|
||||
<Th>Value</Th>
|
||||
<Th className="min-table-row">Comment</Th>
|
||||
<Th className="min-table-row">Tags</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
<Tr>
|
||||
<Td>TWILIO_SECRET_TOKEN</Td>
|
||||
<Td>value</Td>
|
||||
<Td>Some values</Td>
|
||||
<Td>-</Td>
|
||||
</Tr>
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<div className="flex items-center mt-8 space-x-6">
|
||||
<Button leftIcon={<FontAwesomeIcon icon={faCheck} />}>Approve</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
leftIcon={<FontAwesomeIcon icon={faXmark} />}
|
||||
colorSchema="danger"
|
||||
>
|
||||
Deny
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { SecretApprovalListPage } from "./SecretApprovalListPage";
|
||||
31
frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
Normal file
31
frontend/src/views/SecretApprovalPage/SecretApprovalPage.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
|
||||
import { SecretApprovalPolicyList } from "./components/SecretApprovalPolicyList";
|
||||
|
||||
enum TabSection {
|
||||
ApprovalRequests = "approval-requests",
|
||||
Rules = "approval-rules"
|
||||
}
|
||||
|
||||
export const SecretApprovalPage = () => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const workspaceId = currentWorkspace?._id || "";
|
||||
|
||||
return (
|
||||
<div className="container mx-auto bg-bunker-800 text-white w-full h-full">
|
||||
<div className="my-6">
|
||||
<p className="text-3xl font-semibold text-gray-200">Admin Panels</p>
|
||||
</div>
|
||||
<Tabs defaultValue={TabSection.ApprovalRequests}>
|
||||
<TabList>
|
||||
<Tab value={TabSection.ApprovalRequests}>Secret PRs</Tab>
|
||||
<Tab value={TabSection.Rules}>Policies</Tab>
|
||||
</TabList>
|
||||
<TabPanel value={TabSection.Rules}>
|
||||
<SecretApprovalPolicyList workspaceId={workspaceId} />
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
DeleteActionModal,
|
||||
Table,
|
||||
TableContainer,
|
||||
TableSkeleton,
|
||||
TBody,
|
||||
Th,
|
||||
THead,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useDeleteSecretApprovalPolicy,
|
||||
useGetSecretApprovalPolicies,
|
||||
useGetWorkspaceUsers
|
||||
} from "@app/hooks/api";
|
||||
import { TSecretApprovalPolicy } from "@app/hooks/api/types";
|
||||
|
||||
import { SecretApprovalPolicyRow } from "./components/SecretApprovalPolicyRow";
|
||||
import { SecretPolicyForm } from "./components/SecretPolicyForm";
|
||||
|
||||
type Props = {
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const SecretApprovalPolicyList = ({ workspaceId }: Props) => {
|
||||
const { handlePopUpToggle, handlePopUpOpen, handlePopUpClose, popUp } = usePopUp([
|
||||
"secretPolicyForm",
|
||||
"deletePolicy"
|
||||
] as const);
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const { data: members } = useGetWorkspaceUsers(workspaceId);
|
||||
const { data: policies, isLoading: isPoliciesLoading } = useGetSecretApprovalPolicies({
|
||||
workspaceId
|
||||
});
|
||||
|
||||
const { mutateAsync: deleteSecretApprovalPolicy } = useDeleteSecretApprovalPolicy();
|
||||
|
||||
const handleDeletePolicy = async () => {
|
||||
const { _id: id } = popUp.deletePolicy.data as TSecretApprovalPolicy;
|
||||
try {
|
||||
await deleteSecretApprovalPolicy({
|
||||
workspaceId,
|
||||
id
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted policy"
|
||||
});
|
||||
handlePopUpClose("deletePolicy");
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between mb-6">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xl font-semibold text-mineshaft-100">Approval Policies</span>
|
||||
<div className="text-bunker-300 mt-2 text-sm">
|
||||
Implement policies to prevent unauthorized secret changes.
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("secretPolicyForm")}
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
>
|
||||
Create policy
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th>Environment</Th>
|
||||
<Th>Secret Path</Th>
|
||||
<Th>Eligible Approvers</Th>
|
||||
<Th>Approval Required</Th>
|
||||
<Th />
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{isPoliciesLoading && (
|
||||
<TableSkeleton columns={4} innerKey="secret-policies" className="bg-mineshaft-700" />
|
||||
)}
|
||||
{policies?.map((policy) => (
|
||||
<SecretApprovalPolicyRow
|
||||
workspaceId={workspaceId}
|
||||
policy={policy}
|
||||
key={policy._id}
|
||||
members={members}
|
||||
onEdit={() => handlePopUpOpen("secretPolicyForm", policy)}
|
||||
onDelete={() => handlePopUpOpen("deletePolicy", policy)}
|
||||
/>
|
||||
))}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<SecretPolicyForm
|
||||
workspaceId={workspaceId}
|
||||
isOpen={popUp.secretPolicyForm.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("secretPolicyForm", isOpen)}
|
||||
members={members}
|
||||
editValues={popUp.secretPolicyForm.data as TSecretApprovalPolicy}
|
||||
/>
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deletePolicy.isOpen}
|
||||
deleteKey="remove"
|
||||
title="Do you want to remove this polciy?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deletePolicy", isOpen)}
|
||||
onDeleteApproved={handleDeletePolicy}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { useState } from "react";
|
||||
import { faCheckCircle, faPencil, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
IconButton,
|
||||
Input,
|
||||
Td,
|
||||
Tooltip,
|
||||
Tr
|
||||
} from "@app/components/v2";
|
||||
import { useUpdateSecretApprovalPolicy } from "@app/hooks/api";
|
||||
import { TSecretApprovalPolicy } from "@app/hooks/api/types";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||
|
||||
type Props = {
|
||||
policy: TSecretApprovalPolicy;
|
||||
members?: TWorkspaceUser[];
|
||||
workspaceId: string;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
export const SecretApprovalPolicyRow = ({
|
||||
policy,
|
||||
members = [],
|
||||
workspaceId,
|
||||
onEdit,
|
||||
onDelete
|
||||
}: Props) => {
|
||||
const [selectedApprovers, setSelectedApprovers] = useState<string[]>([]);
|
||||
const { mutate: updateSecretApprovalPolicy, isLoading } = useUpdateSecretApprovalPolicy();
|
||||
|
||||
return (
|
||||
<Tr>
|
||||
<Td>{policy.environment}</Td>
|
||||
<Td>{policy.secretPath || "*"}</Td>
|
||||
<Td>
|
||||
<DropdownMenu
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
updateSecretApprovalPolicy(
|
||||
{
|
||||
workspaceId,
|
||||
id: policy._id,
|
||||
approvers: selectedApprovers
|
||||
},
|
||||
{
|
||||
onSettled: () => {
|
||||
setSelectedApprovers([]);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
setSelectedApprovers(policy.approvers);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownMenuTrigger asChild disabled={isLoading}>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={policy.approvers?.length ? `${policy.approvers.length} selected` : "None"}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
||||
align="start"
|
||||
>
|
||||
<DropdownMenuLabel>Select members that must approve changes</DropdownMenuLabel>
|
||||
{members?.map(({ _id, user }) => {
|
||||
const isChecked = selectedApprovers.includes(_id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
setSelectedApprovers((state) =>
|
||||
isChecked ? state.filter((el) => el !== _id) : [...state, _id]
|
||||
);
|
||||
}}
|
||||
key={`create-policy-members-${_id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{user.email}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</Td>
|
||||
<Td>{policy.approvals}</Td>
|
||||
<Td>
|
||||
<div className="flex items-center justify-end space-x-4">
|
||||
<Tooltip content="Edit">
|
||||
<IconButton variant="plain" ariaLabel="edit" onClick={onEdit}>
|
||||
<FontAwesomeIcon icon={faPencil} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Delete">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
size="lg"
|
||||
ariaLabel="edit"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,239 @@
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
Input,
|
||||
Modal,
|
||||
ModalContent,
|
||||
Select,
|
||||
SelectItem
|
||||
} from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useCreateSecretApprovalPolicy, useUpdateSecretApprovalPolicy } from "@app/hooks/api";
|
||||
import { TSecretApprovalPolicy } from "@app/hooks/api/types";
|
||||
import { TWorkspaceUser } from "@app/hooks/api/users/types";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
members?: TWorkspaceUser[];
|
||||
workspaceId: string;
|
||||
editValues?: TSecretApprovalPolicy;
|
||||
};
|
||||
|
||||
const formSchema = z.object({
|
||||
environment: z.string(),
|
||||
secretPath: z.string().optional(),
|
||||
approvals: z.number().min(1),
|
||||
approvers: z.string().array().optional()
|
||||
});
|
||||
|
||||
type TFormSchema = z.infer<typeof formSchema>;
|
||||
|
||||
export const SecretPolicyForm = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
members = [],
|
||||
workspaceId,
|
||||
editValues
|
||||
}: Props) => {
|
||||
const {
|
||||
control,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: editValues
|
||||
});
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { createNotification } = useNotificationContext();
|
||||
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
useEffect(() => {
|
||||
if (!isOpen) reset({});
|
||||
}, [isOpen]);
|
||||
|
||||
const isEditMode = Boolean(editValues);
|
||||
|
||||
const { mutateAsync: createSecretApprovalPolicy } = useCreateSecretApprovalPolicy();
|
||||
const { mutateAsync: updateSecretApprovalPolicy } = useUpdateSecretApprovalPolicy();
|
||||
|
||||
const handleCreatePolicy = async (data: TFormSchema) => {
|
||||
try {
|
||||
await createSecretApprovalPolicy({
|
||||
...data,
|
||||
workspaceId
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully created policy"
|
||||
});
|
||||
onToggle(false);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to create policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdatePolicy = async (data: TFormSchema) => {
|
||||
if (!editValues?._id) return;
|
||||
try {
|
||||
await updateSecretApprovalPolicy({
|
||||
id: editValues?._id,
|
||||
...data,
|
||||
secretPath: data.secretPath ?? "-",
|
||||
workspaceId
|
||||
});
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully updated policy"
|
||||
});
|
||||
onToggle(false);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "failed to update policy"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
if (isEditMode) {
|
||||
await handleUpdatePolicy(data);
|
||||
} else {
|
||||
await handleCreatePolicy(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onToggle}>
|
||||
<ModalContent title={isEditMode ? "Edit policy" : "Create policy"}>
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="environment"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Environment"
|
||||
isRequired
|
||||
className="mt-4"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Select
|
||||
value={value}
|
||||
onValueChange={(val) => onChange(val)}
|
||||
className="w-full border border-mineshaft-500"
|
||||
>
|
||||
{environments.map((sourceEnvironment) => (
|
||||
<SelectItem
|
||||
value={sourceEnvironment.slug}
|
||||
key={`azure-key-vault-environment-${sourceEnvironment.slug}`}
|
||||
>
|
||||
{sourceEnvironment.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="secretPath"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="approvers"
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Approvals Required"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Input
|
||||
isReadOnly
|
||||
value={value?.length ? `${value.length} selected` : "None"}
|
||||
className="text-left"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
style={{ width: "var(--radix-dropdown-menu-trigger-width)" }}
|
||||
align="start"
|
||||
>
|
||||
<DropdownMenuLabel>Select members that must approve changes</DropdownMenuLabel>
|
||||
{members.map(({ _id, user }) => {
|
||||
const isChecked = value?.includes(_id);
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={(evt) => {
|
||||
evt.preventDefault();
|
||||
onChange(
|
||||
isChecked
|
||||
? value?.filter((el) => el !== _id)
|
||||
: [...(value || []), _id]
|
||||
);
|
||||
}}
|
||||
key={`create-policy-members-${_id}`}
|
||||
iconPos="right"
|
||||
icon={isChecked && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
>
|
||||
{user.email}
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
name="approvals"
|
||||
defaultValue={1}
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Approvals Required"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input {...field} type="number" />
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="flex mt-8 space-x-4 items-center">
|
||||
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={() => onToggle(false)} variant="outline_bg">
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretApprovalPolicyList } from "./SecretApprovalPolicyList";
|
||||
1
frontend/src/views/SecretApprovalPage/index.tsx
Normal file
1
frontend/src/views/SecretApprovalPage/index.tsx
Normal file
@@ -0,0 +1 @@
|
||||
export { SecretApprovalPage } from "./SecretApprovalPage";
|
||||
Reference in New Issue
Block a user