From f15b30ff85488c2b8e50700414a97573eb6c3243 Mon Sep 17 00:00:00 2001 From: carlosmonastyrski Date: Thu, 10 Apr 2025 13:08:52 -0300 Subject: [PATCH] Improve referenced secret deletion message component --- .../src/server/routes/v1/dashboard-router.ts | 1 + .../secret-import/secret-import-service.ts | 2 +- .../DeleteActionModal/DeleteActionModal.tsx | 11 +- frontend/src/hooks/api/dashboard/types.ts | 1 + .../CollapsibleSecretImports.tsx | 252 +++++++----------- .../SecretListView/SecretListView.tsx | 20 +- 6 files changed, 129 insertions(+), 158 deletions(-) diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 001b11820..a23aa49e1 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -578,6 +578,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { importedBy: z .object({ envName: z.string(), + envSlug: z.string(), folders: z .object({ folderName: z.string(), diff --git a/backend/src/services/secret-import/secret-import-service.ts b/backend/src/services/secret-import/secret-import-service.ts index b6356bcfd..cc966e943 100644 --- a/backend/src/services/secret-import/secret-import-service.ts +++ b/backend/src/services/secret-import/secret-import-service.ts @@ -823,7 +823,7 @@ export const secretImportServiceFactory = ({ }); const importedBy = await secretImportDAL.getFolderIsImportedBy(secretPath, folder.envId, environment, projectId); - + console.log({ importedBy }); const deepPaths: { path: string; folderId: string }[] = []; await Promise.all( diff --git a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx index 299b23bb5..a2b69eaba 100644 --- a/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx +++ b/frontend/src/components/v2/DeleteActionModal/DeleteActionModal.tsx @@ -16,7 +16,9 @@ type Props = { subTitle?: string; onDeleteApproved: () => Promise; buttonText?: string; + formContent?: ReactNode; children?: ReactNode; + deletionMessage?: ReactNode; }; export const DeleteActionModal = ({ @@ -28,6 +30,8 @@ export const DeleteActionModal = ({ title, subTitle = "This action is irreversible.", buttonText = "Delete", + formContent, + deletionMessage, children }: Props): JSX.Element => { const [inputData, setInputData] = useState(""); @@ -79,6 +83,7 @@ export const DeleteActionModal = ({ } onClose={onClose} > + {formContent}
{ evt.preventDefault(); @@ -88,7 +93,11 @@ export const DeleteActionModal = ({ - Type {deleteKey} to perform this action + {deletionMessage || ( + <> + Type {deleteKey} to perform this action + + )} } className="mb-0" diff --git a/frontend/src/hooks/api/dashboard/types.ts b/frontend/src/hooks/api/dashboard/types.ts index 4024394a6..750bfc826 100644 --- a/frontend/src/hooks/api/dashboard/types.ts +++ b/frontend/src/hooks/api/dashboard/types.ts @@ -43,6 +43,7 @@ export type DashboardProjectSecretsDetailsResponse = { totalCount: number; importedBy?: { envName: string; + envSlug: string; folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; }[]; }; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx index 8efcd3ac4..6be011587 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports.tsx @@ -1,24 +1,24 @@ -import React, { useMemo, useState } from "react"; -import { - faChevronLeft, - faChevronRight, - faFolder, - faKey, - faWarning -} from "@fortawesome/free-solid-svg-icons"; +import React, { useMemo } from "react"; +import { faFileImport, faKey, faWarning } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2"; +import { useWorkspace } from "@app/context"; + interface FlatItem { type: "folder" | "secret"; path: string; secretKey?: string; reference: string; id: string; + envName: string; + envSlug: string; } interface CollapsibleSecretImportsProps { importedBy?: { envName: string; + envSlug: string; folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; }[]; } @@ -26,22 +26,49 @@ interface CollapsibleSecretImportsProps { export const CollapsibleSecretImports: React.FC = ({ importedBy = [] }) => { - const [currentEnvIndex, setCurrentEnvIndex] = useState(0); + const { currentWorkspace } = useWorkspace(); - const truncatePath = (path: string, maxLength = 45): string => - path.length <= maxLength ? path : `...${path.slice(-(maxLength - 3))}`; + const truncatePath = (path: string, maxLength = 24): string => { + if (path.length <= maxLength) return path; - const groupedItems = useMemo(() => { - return importedBy.map((env) => { - const envItems: FlatItem[] = []; + const endPortion = path.slice(-(maxLength - 3)); + const slashIndex = endPortion.indexOf("/"); + if (slashIndex !== -1) { + return `...${endPortion.slice(slashIndex)}`; + } + + return `...${endPortion}`; + }; + + const handlePathClick = (item: FlatItem) => { + let pathToNavigate; + if (item.type === "folder") { + pathToNavigate = item.path; + } else { + const lastSlashIndex = item.path.lastIndexOf("/"); + pathToNavigate = lastSlashIndex > 0 ? item.path.substring(0, lastSlashIndex) : "/"; + } + const encodedPath = encodeURIComponent(pathToNavigate); + window.open( + `/secret-manager/${currentWorkspace.id}/secrets/${item.envSlug}?secretPath=${encodedPath}`, + "_blank" + ); + }; + + const flattenedItems = useMemo(() => { + const items: FlatItem[] = []; + + importedBy.forEach((env) => { env.folders.forEach((folder) => { if (folder.folderImported) { - envItems.push({ + items.push({ type: "folder", path: folder.folderName, id: `folder-${env.envName}-${folder.folderName}`, - reference: "Imported" + reference: "Imported", + envName: env.envName, + envSlug: env.envSlug }); } @@ -50,160 +77,83 @@ export const CollapsibleSecretImports: React.FC = const secretPath = folder.folderName === "/" ? `/${secret}` : `${folder.folderName}/${secret}`; - envItems.push({ + items.push({ type: "secret", path: secretPath, secretKey: secret, id: `secret-${env.envName}-${secretPath}`, - reference: "Referenced" + reference: "Referenced", + envName: env.envName, + envSlug: env.envSlug }); }); } }); + }); - return { - envName: env.envName, - items: envItems.sort((a, b) => { - const aPath = a.path.startsWith("/") ? a.path : `/${a.path}`; - const bPath = b.path.startsWith("/") ? b.path : `/${b.path}`; + return items.sort((a, b) => { + const envCompare = a.envName.localeCompare(b.envName); + if (envCompare !== 0) return envCompare; - const aSegments = aPath.split("/").filter(Boolean).length; - const bSegments = bPath.split("/").filter(Boolean).length; + const aPath = a.path.startsWith("/") ? a.path : `/${a.path}`; + const bPath = b.path.startsWith("/") ? b.path : `/${b.path}`; - if (aSegments !== bSegments) { - return aSegments - bSegments; - } + const aSegments = aPath.split("/").filter(Boolean).length; + const bSegments = bPath.split("/").filter(Boolean).length; - return aPath.localeCompare(bPath); - }) - }; + if (aSegments !== bSegments) { + return aSegments - bSegments; + } + + return aPath.localeCompare(bPath); }); }, [importedBy]); - const totalEnvironments = groupedItems.length; - const currentItems = totalEnvironments > 0 ? groupedItems[currentEnvIndex]?.items || [] : []; - - const handlePrevEnvironment = () => { - if (currentEnvIndex > 0) { - setCurrentEnvIndex(currentEnvIndex - 1); - } - }; - - const handleNextEnvironment = () => { - if (currentEnvIndex < totalEnvironments - 1) { - setCurrentEnvIndex(currentEnvIndex + 1); - } - }; - - const renderPaginationButton = ( - direction: "prev" | "next", - onClick: () => void, - disabled: boolean - ) => { - const icon = direction === "prev" ? faChevronLeft : faChevronRight; - const label = direction === "prev" ? "Previous environment" : "Next environment"; - - return ( - - ); - }; - return ( -
-
-
-
-
-
-
-

- Warning: The following resources will be affected by this change -

-
    -
  • Deleting will remove it from all folders where it's imported
  • -
  • - Any secrets referencing this will display their reference syntax (like{" "} - - {"{env.secretPath.key}"} - - ) instead of actual values -
  • -
  • Secrets referencing this will not be automatically deleted
  • -
-
+
+
+
+
+
+
+

+ The following resources will be affected by this change +

+
- {totalEnvironments > 0 && ( -
-

- Environment: {groupedItems[currentEnvIndex]?.envName} -

-
- )} - -
- - - - - - - - - - {currentItems.map((item) => ( - - - - - - ))} - {currentItems.length === 0 && ( - - - - )} - -
TypePathUsage
- {truncatePath(item.path)}{item.reference}
- No items in this environment -
-
- - {totalEnvironments > 1 && ( -
-
- Environment {currentEnvIndex + 1} of {totalEnvironments} -
-
- {renderPaginationButton("prev", handlePrevEnvironment, currentEnvIndex === 0)} - {renderPaginationButton( - "next", - handleNextEnvironment, - currentEnvIndex === totalEnvironments - 1 - )} -
-
- )} +
+ + + + + + + + + {flattenedItems.map((item) => ( + handlePathClick(item)} + className="cursor-pointer hover:bg-mineshaft-700" + title={`Navigate to ${item.path}`} + > + + + + + + ))} + +
TypeEnvironmentPathUsage
+ {item.envName}{truncatePath(item.path)}{item.reference}
); diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx index e55c70c64..20ba64e29 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretListView/SecretListView.tsx @@ -31,6 +31,7 @@ type Props = { isProtectedBranch?: boolean; importedBy?: { envName: string; + envSlug: string; folders: { folderName: string; secrets?: string[]; folderImported: boolean }[]; }[]; }; @@ -363,11 +364,20 @@ export const SecretListView = ({ onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)} onDeleteApproved={handleSecretDelete} buttonText="Delete Secret" - > - {importedBy && importedBy.length > 0 && ( - - )} - + formContent={ + importedBy && + importedBy.length > 0 && + } + deletionMessage={ + <> + Type the secret key{" "} + + "{(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}" + {" "} + below to perform this action + + } + />