From e201e80a066da8cdad2479fdaea975413c92da1c Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Thu, 21 Mar 2024 20:20:59 +0530 Subject: [PATCH] feat(ui): completed ui for dynamic secret --- frontend/package-lock.json | 31 +- frontend/package.json | 1 + .../SecretMainPage/SecretMainPage.store.tsx | 1 + .../views/SecretMainPage/SecretMainPage.tsx | 27 +- .../components/ActionBar/ActionBar.tsx | 31 +- .../CreateDynamicSecretForm.tsx | 105 +++++ .../SqlDatabaseInputForm.tsx | 346 +++++++++++++++++ .../CreateDynamicSecretForm/index.tsx | 1 + .../CreateDynamicSecretLease.tsx | 185 +++++++++ .../DynamicSecretLease.tsx | 213 +++++++++++ .../DynamicSecretListView.tsx | 259 +++++++++++++ .../EditDynamicSecretForm.tsx | 61 +++ .../EditDynamicSecretSqlProviderForm.tsx | 361 ++++++++++++++++++ .../EditDynamicSecretForm/index.tsx | 1 + .../RenewDynamicSecretLease.tsx | 107 ++++++ .../DynamicSecretListView/index.tsx | 1 + 16 files changed, 1713 insertions(+), 18 deletions(-) create mode 100644 frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx create mode 100644 frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx create mode 100644 frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/index.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/CreateDynamicSecretLease.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretLease.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/DynamicSecretListView.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretForm.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/EditDynamicSecretSqlProviderForm.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/EditDynamicSecretForm/index.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/RenewDynamicSecretLease.tsx create mode 100644 frontend/src/views/SecretMainPage/components/DynamicSecretListView/index.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5e30edfc3..0d8eaa794 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "frontend", "dependencies": { "@casl/ability": "^6.5.0", "@casl/react": "^3.1.0", @@ -66,6 +67,7 @@ "jwt-decode": "^3.1.2", "lottie-react": "^2.4.0", "markdown-it": "^13.0.1", + "ms": "^2.1.3", "next": "^12.3.4", "nprogress": "^0.2.0", "picomatch": "^2.3.1", @@ -11455,6 +11457,11 @@ } } }, + "node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/decode-named-character-reference": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", @@ -13759,9 +13766,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -15126,9 +15133,9 @@ } }, "node_modules/ip": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.1.tgz", + "integrity": "sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==", "dev": true }, "node_modules/ipaddr.js": { @@ -17518,9 +17525,9 @@ } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/multipipe": { "version": "1.0.2", @@ -21287,12 +21294,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 6a1666f05..def2a968b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -74,6 +74,7 @@ "jwt-decode": "^3.1.2", "lottie-react": "^2.4.0", "markdown-it": "^13.0.1", + "ms": "^2.1.3", "next": "^12.3.4", "nprogress": "^0.2.0", "picomatch": "^2.3.1", diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.store.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.store.tsx index a92960b31..e5a214fb0 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.store.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.store.tsx @@ -31,6 +31,7 @@ const createSelectedSecretStore: StateCreator = (set) => ({ export enum PopUpNames { CreateSecretForm = "create-secret-form" } + type PopUpState = { popUp: Record; popUpActions: { diff --git a/frontend/src/views/SecretMainPage/SecretMainPage.tsx b/frontend/src/views/SecretMainPage/SecretMainPage.tsx index a06ceda9d..6d82b160d 100644 --- a/frontend/src/views/SecretMainPage/SecretMainPage.tsx +++ b/frontend/src/views/SecretMainPage/SecretMainPage.tsx @@ -17,6 +17,7 @@ import { } from "@app/context"; import { usePopUp } from "@app/hooks"; import { + useGetDynamicSecrets, useGetImportedSecretsSingleEnv, useGetProjectFolders, useGetProjectSecrets, @@ -31,6 +32,7 @@ import { import { ProjectIndexSecretsSection } from "../SecretOverviewPage/components/ProjectIndexSecretsSection"; import { ActionBar } from "./components/ActionBar"; import { CreateSecretForm } from "./components/CreateSecretForm"; +import { DynamicSecretListView } from "./components/DynamicSecretListView"; import { FolderListView } from "./components/FolderListView"; import { PitDrawer } from "./components/PitDrawer"; import { SecretDropzone } from "./components/SecretDropzone"; @@ -103,12 +105,14 @@ export const SecretMainPage = () => { enabled: canReadSecret } }); + // fetch folders const { data: folders, isLoading: isFoldersLoading } = useGetProjectFolders({ projectId: workspaceId, environment, path: secretPath }); + // fetch secret imports const { data: secretImports, @@ -133,6 +137,13 @@ export const SecretMainPage = () => { enabled: canReadSecret } }); + + const { data: dynamicSecrets, isLoading: isDynamicSecretLoading } = useGetDynamicSecrets({ + projectId: workspaceId, + environment, + path: secretPath + }); + // fech tags const { data: tags } = useGetWsTags(canReadSecret ? workspaceId : ""); @@ -163,7 +174,9 @@ export const SecretMainPage = () => { isPaused: !canDoReadRollback }); - const isNotEmtpy = Boolean(secrets?.length || folders?.length || secretImports?.length); + const isNotEmtpy = Boolean( + secrets?.length || folders?.length || secretImports?.length || dynamicSecrets?.length + ); const handleSortToggle = () => setSortDir((state) => (state === SortDir.ASC ? SortDir.DESC : SortDir.ASC)); @@ -213,7 +226,8 @@ export const SecretMainPage = () => { // loading screen when u have permission const loadingOnAccess = - canReadSecret && (isSecretsLoading || isSecretImportsLoading || isFoldersLoading); + canReadSecret && + (isSecretsLoading || isSecretImportsLoading || isFoldersLoading || isDynamicSecretLoading); // loading screen when you don't have permission but as folder's is viewable need to wait for that const loadingOnDenied = !canReadSecret && isFoldersLoading; if (loadingOnAccess || loadingOnDenied) { @@ -298,6 +312,15 @@ export const SecretMainPage = () => { secretPath={secretPath} sortDir={sortDir} /> + {canReadSecret && ( + + )} {canReadSecret && ( { const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ "addFolder", + "addDynamicSecret", "addSecretImport", "bulkDeleteSecrets", "misc", @@ -311,7 +314,6 @@ export const ActionBar = ({ )} - handlePopUpToggle("misc", isOpen)} @@ -347,6 +349,26 @@ export const ActionBar = ({ )} + + {(isAllowed) => ( + + )} + handlePopUpClose("addSecretImport")} onTogglePopUp={(isOpen) => handlePopUpToggle("addSecretImport", isOpen)} /> + handlePopUpToggle("addDynamicSecret", isOpen)} + workspaceId={workspaceId} + environment={environment} + secretPath={secretPath} + /> handlePopUpToggle("addFolder", isOpen)} diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx new file mode 100644 index 000000000..8f2ab811d --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/CreateDynamicSecretForm.tsx @@ -0,0 +1,105 @@ +import { useState } from "react"; +import { faDatabase } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { AnimatePresence, motion } from "framer-motion"; + +import { Modal, ModalContent } from "@app/components/v2"; +import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types"; + +import { SqlDatabaseInputForm } from "./SqlDatabaseInputForm"; + +type Props = { + isOpen?: boolean; + onToggle: (isOpen: boolean) => void; + workspaceId: string; + environment: string; + secretPath: string; +}; + +enum WizardSteps { + SelectProvider = "select-provider", + ProviderInputs = "provider-inputs" +} + +export const CreateDynamicSecretForm = ({ + isOpen, + onToggle, + workspaceId, + environment, + secretPath +}: Props) => { + const [wizardStep, setWizardStep] = useState(WizardSteps.SelectProvider); + const [selectedProvider, setSelectedProvider] = useState(null); + + const handleFormReset = (state: boolean = false) => { + onToggle(state); + setWizardStep(WizardSteps.SelectProvider); + setSelectedProvider(null); + }; + + return ( + handleFormReset(state)}> + + + {wizardStep === WizardSteps.SelectProvider && ( + +
Pick a service to connect
+
+
{ + setSelectedProvider(DynamicSecretProviders.SqlDatabase); + setWizardStep(WizardSteps.ProviderInputs); + }} + onKeyDown={(evt) => { + if (evt.key === "Enter") { + setSelectedProvider(DynamicSecretProviders.SqlDatabase); + setWizardStep(WizardSteps.ProviderInputs); + } + }} + > + +
+ SQL +
+ Database +
+
+
+
+ )} + {wizardStep === WizardSteps.ProviderInputs && + selectedProvider === DynamicSecretProviders.SqlDatabase && ( + + + + )} +
+
+
+ ); +}; diff --git a/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx new file mode 100644 index 000000000..5eb6086d3 --- /dev/null +++ b/frontend/src/views/SecretMainPage/components/ActionBar/CreateDynamicSecretForm/SqlDatabaseInputForm.tsx @@ -0,0 +1,346 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import ms from "ms"; +import { z } from "zod"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { TtlFormLabel } from "@app/components/features"; +import { + Button, + FormControl, + Input, + SecretInput, + Select, + SelectItem, + TextArea +} from "@app/components/v2"; +import { useCreateDynamicSecret } from "@app/hooks/api"; +import { DynamicSecretProviders, SqlProviders } from "@app/hooks/api/dynamicSecret/types"; + +const formSchema = z.object({ + provider: z.object({ + client: z.nativeEnum(SqlProviders), + host: z.string().toLowerCase().min(1), + port: z.number(), + database: z.string().min(1), + username: z.string().min(1), + password: z.string().min(1), + creationStatement: z.string().min(1), + revocationStatement: z.string().min(1), + renewStatement: z.string().min(1), + ca: z.string().optional() + }), + defaultTTL: z.string().superRefine((val, ctx) => { + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + maxTTL: z + .string() + .optional() + .superRefine((val, ctx) => { + if (!val) return; + const valMs = ms(val); + if (valMs < 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" }); + // a day + if (valMs > 24 * 60 * 60 * 1000) + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" }); + }), + slug: z.string().toLowerCase() +}); +type TForm = z.infer; + +type Props = { + onCompleted: () => void; + onCancel: () => void; + secretPath: string; + projectId: string; + environment: string; +}; + +export const SqlDatabaseInputForm = ({ + onCompleted, + onCancel, + environment, + secretPath, + projectId +}: Props) => { + const { + control, + formState: { isSubmitting }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema) + }); + const { createNotification } = useNotificationContext(); + const createDynamicSecret = useCreateDynamicSecret(); + + const handleCreateDynamicSecret = async ({ slug, maxTTL, provider, defaultTTL }: TForm) => { + // wait till previous request is finished + if (createDynamicSecret.isLoading) return; + try { + await createDynamicSecret.mutateAsync({ + provider: { type: DynamicSecretProviders.SqlDatabase, inputs: provider }, + maxTTL, + slug, + path: secretPath, + defaultTTL, + projectId, + environment + }); + onCompleted(); + } catch (err) { + createNotification({ + type: "error", + text: "Failed to create dynamic secret" + }); + } + }; + + return ( +
+
+
+
+ ( + + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+ ( + } + isError={Boolean(error?.message)} + errorText={error?.message} + > + + + )} + /> +
+
+
+
Configuration
+
+ ( + + + + )} + /> +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + +