diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 8cf9604a4..cf66cd898 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -624,7 +624,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SanitizedTagSchema.array().optional() + tags: SanitizedTagSchema.array().optional(), + reminder: RemindersSchema.extend({ + recipients: z.string().array() + }).nullable() }) .nullable() .array() @@ -743,6 +746,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { ReturnType >[number]["secrets"][number] & { isEmpty: boolean; + reminder: Awaited>[string] | null; } > | null)[]; })[] @@ -847,27 +851,38 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { ); if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) { - secretRotations = ( - await server.services.secretRotationV2.getDashboardSecretRotations( - { - projectId, - search, - orderBy, - orderDirection, - environments: [environment], - secretPath, - limit: remainingLimit, - offset: adjustedOffset - }, - req.permission - ) - ).map((rotation) => ({ + const rawSecretRotations = await server.services.secretRotationV2.getDashboardSecretRotations( + { + projectId, + search, + orderBy, + orderDirection, + environments: [environment], + secretPath, + limit: remainingLimit, + offset: adjustedOffset + }, + req.permission + ); + + const allRotationSecretIds = rawSecretRotations + .flatMap((rotation) => rotation.secrets) + .filter((secret) => Boolean(secret)) + .map((secret) => secret.id); + + const rotationReminders = + allRotationSecretIds.length > 0 + ? await server.services.reminder.getRemindersForDashboard(allRotationSecretIds) + : {}; + + secretRotations = rawSecretRotations.map((rotation) => ({ ...rotation, secrets: rotation.secrets.map((secret) => secret ? { ...secret, - isEmpty: !secret.secretValue + isEmpty: secret.secretValueHidden, + reminder: rotationReminders[secret.id] ?? null } : secret ) @@ -978,11 +993,26 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { rawSecrets.map((secret) => secret.id) ); - secrets = rawSecrets.map((secret) => ({ + const rotationSecretIds = + includeSecretRotations && secretRotations?.length + ? new Set( + secretRotations.flatMap((rotation) => rotation.secrets.filter(Boolean).map((secret) => secret.id)) + ) + : new Set(); + + const filteredSecrets = rawSecrets.filter((secret) => !rotationSecretIds.has(secret.id)); + + secrets = filteredSecrets.map((secret) => ({ ...secret, isEmpty: !secret.secretValue, reminder: reminders[secret.id] ?? null })); + + if (includeSecretRotations && secretRotations?.length && totalSecretCount && rotationSecretIds.size > 0) { + const filteredCount = rawSecrets.filter((secret) => !rotationSecretIds.has(secret.id)).length; + const originalCount = rawSecrets.length; + totalSecretCount = Math.max(0, totalSecretCount - (originalCount - filteredCount)); + } } } } catch (error) { diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx index 203a3f6eb..441f69f7c 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/SecretDashboardPage.tsx @@ -656,12 +656,17 @@ const Page = () => { setDebouncedSearchFilter(""); }; - const getMergedSecretsWithPending = () => { + const getMergedSecretsWithPending = ( + paramSecrets?: (SecretV3RawSanitized | null)[] + ): SecretV3RawSanitized[] => { + const sanitizedParamSecrets = paramSecrets?.filter(Boolean) as + | SecretV3RawSanitized[] + | undefined; if (!isBatchMode || pendingChanges.secrets.length === 0) { - return secrets; + return sanitizedParamSecrets || secrets || []; } - const mergedSecrets = [...(secrets || [])] as (SecretV3RawSanitized & { + const mergedSecrets = [...(sanitizedParamSecrets || secrets || [])] as (SecretV3RawSanitized & { originalKey?: string; })[]; @@ -1041,7 +1046,17 @@ const Page = () => { /> )} {canReadSecretRotations && Boolean(secretRotations?.length) && ( - + )} {canReadSecret && Boolean(mergedSecrets?.length) && ( ( - + void; onViewGeneratedCredentials: () => void; onDelete: () => void; + projectId: string; + secretPath?: string; + tags?: WsTag[]; + isProtectedBranch?: boolean; + usedBySecretSyncs?: UsedBySecretSyncs[]; + importedBy?: { + environment: { name: string; slug: string }; + folders: { + name: string; + secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[]; + isImported: boolean; + }[]; + }[]; + colWidth: number; + getMergedSecretsWithPending: ( + paramSecrets?: (SecretV3RawSanitized | null)[] + ) => SecretV3RawSanitized[]; }; export const SecretRotationItem = ({ @@ -35,16 +55,40 @@ export const SecretRotationItem = ({ onEdit, onRotate, onViewGeneratedCredentials, - onDelete + onDelete, + projectId, + secretPath = "/", + tags = [], + isProtectedBranch = false, + usedBySecretSyncs, + importedBy, + colWidth, + getMergedSecretsWithPending }: Props) => { const { name, type, environment, folder, secrets, description } = secretRotation; const { name: rotationType, image } = SECRET_ROTATION_MAP[type]; const [showSecrets, setShowSecrets] = useState(false); + const [isExpanded, setIsExpanded] = useState(true); return ( <> -
+
setIsExpanded(!isExpanded)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setIsExpanded(!isExpanded); + } + }} + role="button" + tabIndex={0} + aria-expanded={isExpanded} + aria-label={`${isExpanded ? "Collapse" : "Expand"} rotation secrets for ${name}`} + >
@@ -198,6 +242,19 @@ export const SecretRotationItem = ({
+ {isExpanded && ( + + )} e.preventDefault()} diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationListView.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationListView.tsx index 1682a79df..1de6aa201 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationListView.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/SecretRotationListView/SecretRotationListView.tsx @@ -3,15 +3,44 @@ import { EditSecretRotationV2Modal } from "@app/components/secret-rotations-v2/E import { RotateSecretRotationV2Modal } from "@app/components/secret-rotations-v2/RotateSecretRotationV2Modal"; import { ViewSecretRotationV2GeneratedCredentialsModal } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials"; import { usePopUp } from "@app/hooks"; +import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types"; import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; +import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { SecretRotationItem } from "./SecretRotationItem"; type Props = { secretRotations?: TSecretRotationV2[]; + projectId: string; + secretPath?: string; + tags?: WsTag[]; + isProtectedBranch?: boolean; + usedBySecretSyncs?: UsedBySecretSyncs[]; + importedBy?: { + environment: { name: string; slug: string }; + folders: { + name: string; + secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[]; + isImported: boolean; + }[]; + }[]; + colWidth: number; + getMergedSecretsWithPending: ( + secretParams?: (SecretV3RawSanitized | null)[] + ) => SecretV3RawSanitized[]; }; -export const SecretRotationListView = ({ secretRotations }: Props) => { +export const SecretRotationListView = ({ + secretRotations, + projectId, + secretPath = "/", + tags = [], + isProtectedBranch = false, + usedBySecretSyncs, + importedBy, + colWidth, + getMergedSecretsWithPending +}: Props) => { const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ "editSecretRotation", "rotateSecretRotation", @@ -31,6 +60,14 @@ export const SecretRotationListView = ({ secretRotations }: Props) => { handlePopUpOpen("viewSecretRotationGeneratedCredentials", secretRotation) } onDelete={() => handlePopUpOpen("deleteSecretRotation", secretRotation)} + colWidth={colWidth} + tags={tags} + projectId={projectId} + secretPath={secretPath} + isProtectedBranch={isProtectedBranch} + importedBy={importedBy} + usedBySecretSyncs={usedBySecretSyncs} + getMergedSecretsWithPending={getMergedSecretsWithPending} /> ))}