feat(secrets-ui): improve warning message table

This commit is contained in:
carlosmonastyrski
2025-04-29 14:19:52 -03:00
parent e959ed7fab
commit c47d76a6c7
6 changed files with 70 additions and 46 deletions
@@ -171,7 +171,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.object({ .object({
name: z.string(), name: z.string(),
destination: z.string(), destination: z.string(),
environment: z.string() environment: z.string(),
id: z.string(),
path: z.string()
}) })
.array() .array()
.optional(), .optional(),
@@ -509,7 +511,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
const usedBySecretSyncs: { name: string; destination: string; environment: string }[] = []; const usedBySecretSyncs: { name: string; destination: string; environment: string; id: string; path: string }[] =
[];
for await (const environment of environments) { for await (const environment of environments) {
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath( const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
{ projectId, secretPath, environment }, { projectId, secretPath, environment },
@@ -519,7 +522,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
usedBySecretSyncs.push({ usedBySecretSyncs.push({
name: sync.name, name: sync.name,
destination: sync.destination, destination: sync.destination,
environment environment,
id: sync.id,
path: sync.folder?.path || "/"
}); });
}); });
} }
@@ -640,7 +645,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.object({ .object({
name: z.string(), name: z.string(),
destination: z.string(), destination: z.string(),
environment: z.string() environment: z.string(),
id: z.string(),
path: z.string()
}) })
.array() .array()
.optional(), .optional(),
@@ -945,7 +952,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
const usedBySecretSyncs = secretSyncs.map((sync) => ({ const usedBySecretSyncs = secretSyncs.map((sync) => ({
name: sync.name, name: sync.name,
destination: sync.destination, destination: sync.destination,
environment: sync.environment?.name || environment environment: sync.environment?.name || environment,
id: sync.id,
path: sync.folder?.path || "/"
})); }));
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
+10 -2
View File
@@ -25,10 +25,18 @@ export type DashboardProjectSecretsOverviewResponse = {
totalUniqueFoldersInPage: number; totalUniqueFoldersInPage: number;
totalUniqueSecretImportsInPage: number; totalUniqueSecretImportsInPage: number;
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
usedBySecretSyncs?: { name: string; destination: string; environment: string }[]; usedBySecretSyncs?: UsedBySecretSyncs[];
totalUniqueSecretRotationsInPage: number; totalUniqueSecretRotationsInPage: number;
}; };
export type UsedBySecretSyncs = {
name: string;
destination: string;
environment: string;
id: string;
path: string;
};
export type DashboardProjectSecretsDetailsResponse = { export type DashboardProjectSecretsDetailsResponse = {
imports?: TSecretImport[]; imports?: TSecretImport[];
folders?: TSecretFolder[]; folders?: TSecretFolder[];
@@ -44,7 +52,7 @@ export type DashboardProjectSecretsDetailsResponse = {
totalSecretRotationCount?: number; totalSecretRotationCount?: number;
totalCount: number; totalCount: number;
importedBy?: ProjectSecretsImportedBy[]; importedBy?: ProjectSecretsImportedBy[];
usedBySecretSyncs?: { name: string; destination: string; environment: string }[]; usedBySecretSyncs?: UsedBySecretSyncs[];
}; };
export type ProjectSecretsImportedBy = { export type ProjectSecretsImportedBy = {
@@ -15,7 +15,7 @@ import {
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import { ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types"; import { ProjectSecretsImportedBy, UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { import {
SecretType, SecretType,
SecretV3RawSanitized, SecretV3RawSanitized,
@@ -38,7 +38,7 @@ type Props = {
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>; [EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
}; };
importedBy?: ProjectSecretsImportedBy[] | null; importedBy?: ProjectSecretsImportedBy[] | null;
usedBySecretSyncs?: { name: string; destination: string; environment: string }[]; usedBySecretSyncs?: UsedBySecretSyncs[];
secretsToDeleteKeys: string[]; secretsToDeleteKeys: string[];
}; };
@@ -261,10 +261,10 @@ export const SelectionPanel = ({
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)} onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
onDeleteApproved={handleBulkDelete} onDeleteApproved={handleBulkDelete}
formContent={ formContent={
importedBy && ((usedBySecretSyncsFiltered && usedBySecretSyncsFiltered.length > 0) ||
importedBy.some((element) => element.folders.length > 0) && ( (importedBy && importedBy.some((element) => element.folders.length > 0))) && (
<CollapsibleSecretImports <CollapsibleSecretImports
importedBy={importedBy} importedBy={importedBy || []}
secretsToDelete={secretsToDeleteKeys} secretsToDelete={secretsToDeleteKeys}
usedBySecretSyncs={usedBySecretSyncsFiltered} usedBySecretSyncs={usedBySecretSyncsFiltered}
/> />
@@ -69,6 +69,7 @@ import {
dashboardKeys, dashboardKeys,
fetchDashboardProjectSecretsByKeys fetchDashboardProjectSecretsByKeys
} from "@app/hooks/api/dashboard/queries"; } from "@app/hooks/api/dashboard/queries";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries"; import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
@@ -113,7 +114,7 @@ type Props = {
onVisibilityToggle: () => void; onVisibilityToggle: () => void;
onToggleRowType: (rowType: RowType) => void; onToggleRowType: (rowType: RowType) => void;
onClickRollbackMode: () => void; onClickRollbackMode: () => void;
usedBySecretSyncs?: { environment: string; name: string; destination: string }[]; usedBySecretSyncs?: UsedBySecretSyncs[];
importedBy?: { importedBy?: {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
@@ -3,8 +3,9 @@ import React, { useMemo } from "react";
import { faFileImport, faKey, faSync, faWarning } from "@fortawesome/free-solid-svg-icons"; import { faFileImport, faKey, faSync, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2"; import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
enum ItemType { enum ItemType {
Folder = "Folder", Folder = "Folder",
@@ -19,6 +20,8 @@ interface FlatItem {
reference: string; reference: string;
id: string; id: string;
environment: { name: string; slug: string }; environment: { name: string; slug: string };
tooltipText?: string;
destination?: string;
} }
interface CollapsibleSecretImportsProps { interface CollapsibleSecretImportsProps {
@@ -30,13 +33,7 @@ interface CollapsibleSecretImportsProps {
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
usedBySecretSyncs?: usedBySecretSyncs?: UsedBySecretSyncs[] | null;
| {
name: string;
destination: string;
environment: string;
}[]
| null;
secretsToDelete: string[]; secretsToDelete: string[];
onlyReferences?: boolean; onlyReferences?: boolean;
} }
@@ -65,7 +62,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
const handlePathClick = (item: FlatItem) => { const handlePathClick = (item: FlatItem) => {
if (item.type === ItemType.SecretSync) { if (item.type === ItemType.SecretSync) {
window.open( window.open(
`/secret-manager/${currentWorkspace.id}/integrations?selectedTab=secret-syncs`, `/secret-manager/${currentWorkspace.id}/integrations/secret-syncs/${item.destination}/${item.id}`,
"_blank", "_blank",
"noopener,noreferrer" "noopener,noreferrer"
); );
@@ -128,10 +125,12 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
usedBySecretSyncs?.forEach((syncItem) => { usedBySecretSyncs?.forEach((syncItem) => {
items.push({ items.push({
type: ItemType.SecretSync, type: ItemType.SecretSync,
path: syncItem.destination, destination: syncItem.destination,
id: `secret-sync-${syncItem.name}-${syncItem.destination}`, path: syncItem.path,
id: syncItem.id,
reference: "Secret Sync", reference: "Secret Sync",
environment: { name: syncItem.environment, slug: "" } environment: { name: syncItem.environment, slug: "" },
tooltipText: `Currently used by Secret Sync: ${syncItem.name}`
}); });
}); });
@@ -222,28 +221,34 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
className="cursor-pointer hover:bg-mineshaft-700" className="cursor-pointer hover:bg-mineshaft-700"
title={ title={
item.type === ItemType.SecretSync item.type === ItemType.SecretSync
? "Navigate to Secret Syncs" ? "Navigate to Secret Sync"
: `Navigate to ${item.path}` : `Navigate to ${item.path}`
} }
> >
<Td> <Td>
<FontAwesomeIcon <Tooltip
icon={ className="max-w-md"
item.type === ItemType.Secret content={item.tooltipText}
? faKey isDisabled={!item.tooltipText}
: item.type === ItemType.Folder >
? faFileImport <FontAwesomeIcon
: faSync icon={
} item.type === ItemType.Secret
className={`h-4 w-4 ${ ? faKey
item.type === ItemType.Secret : item.type === ItemType.Folder
? "text-gray-400" ? faFileImport
: item.type === ItemType.Folder : faSync
? "text-green-700" }
: "text-blue-500" className={`h-4 w-4 ${
}`} item.type === ItemType.Secret
aria-hidden="true" ? "text-gray-400"
/> : item.type === ItemType.Folder
? "text-green-700"
: "text-blue-500"
}`}
aria-hidden="true"
/>
</Tooltip>
</Td> </Td>
<Td className="px-4">{item.environment.name}</Td> <Td className="px-4">{item.environment.name}</Td>
<Td className="truncate px-4">{truncatePath(item.path)}</Td> <Td className="truncate px-4">{truncatePath(item.path)}</Td>
@@ -8,6 +8,7 @@ import { DeleteActionModal } from "@app/components/v2";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api"; import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries"; import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries"; import { secretKeys } from "@app/hooks/api/secrets/queries";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
@@ -29,7 +30,7 @@ type Props = {
tags?: WsTag[]; tags?: WsTag[];
isVisible?: boolean; isVisible?: boolean;
isProtectedBranch?: boolean; isProtectedBranch?: boolean;
usedBySecretSyncs?: { environment: string; name: string; destination: string }[]; usedBySecretSyncs?: UsedBySecretSyncs[];
importedBy?: { importedBy?: {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
@@ -385,8 +386,8 @@ export const SecretListView = ({
onDeleteApproved={handleSecretDelete} onDeleteApproved={handleSecretDelete}
buttonText="Delete Secret" buttonText="Delete Secret"
formContent={ formContent={
importedBy && ((importedBy && importedBy.length > 0) ||
importedBy.length > 0 && ( (usedBySecretSyncs && usedBySecretSyncs?.length > 0)) && (
<CollapsibleSecretImports <CollapsibleSecretImports
importedBy={importedBy} importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs} usedBySecretSyncs={usedBySecretSyncs}