Merge pull request #4890 from Infisical/feat/ENG-3443

fix: group rotated secrets under rotation row on the UI dashboard
This commit is contained in:
carlosmonastyrski
2025-12-09 23:34:40 -03:00
committed by GitHub
12 changed files with 242 additions and 61 deletions
@@ -214,7 +214,10 @@ export const secretRotationV2DALFactory = (
tx?: Knex tx?: Knex
) => { ) => {
try { try {
const extendedQuery = baseSecretRotationV2Query({ filter, db, tx, options }) const { limit, offset = 0, sort, ...queryOptions } = options || {};
const baseOptions = { ...queryOptions };
const subquery = baseSecretRotationV2Query({ filter, db, tx, options: baseOptions })
.join( .join(
TableName.SecretRotationV2SecretMapping, TableName.SecretRotationV2SecretMapping,
`${TableName.SecretRotationV2SecretMapping}.rotationId`, `${TableName.SecretRotationV2SecretMapping}.rotationId`,
@@ -233,6 +236,7 @@ export const secretRotationV2DALFactory = (
) )
.leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`)
.select( .select(
selectAllTableCols(TableName.SecretRotationV2),
db.ref("id").withSchema(TableName.SecretV2).as("secretId"), db.ref("id").withSchema(TableName.SecretV2).as("secretId"),
db.ref("key").withSchema(TableName.SecretV2).as("secretKey"), db.ref("key").withSchema(TableName.SecretV2).as("secretKey"),
db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"), db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"),
@@ -252,18 +256,31 @@ export const secretRotationV2DALFactory = (
db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"),
db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"),
db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"),
db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue"),
db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.SecretRotationV2}."createdAt" DESC) as rank`)
); );
if (search) { if (search) {
void extendedQuery.where((query) => { void subquery.where((qb) => {
void query void qb
.whereILike(`${TableName.SecretV2}.key`, `%${search}%`) .whereILike(`${TableName.SecretV2}.key`, `%${search}%`)
.orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`); .orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`);
}); });
} }
const secretRotations = await extendedQuery; let secretRotations: Awaited<typeof subquery>;
if (limit !== undefined) {
const rankOffset = offset + 1;
const queryWithLimit = (tx || db)
.with("inner", subquery)
.select("*")
.from("inner")
.where("inner.rank", ">=", rankOffset)
.andWhere("inner.rank", "<", rankOffset + limit);
secretRotations = (await queryWithLimit) as unknown as Awaited<typeof subquery>;
} else {
secretRotations = await subquery;
}
if (!secretRotations.length) return []; if (!secretRotations.length) return [];
@@ -624,7 +624,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secretValueHidden: z.boolean(), secretValueHidden: z.boolean(),
secretPath: z.string().optional(), secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(), secretMetadata: ResourceMetadataSchema.optional(),
tags: SanitizedTagSchema.array().optional() tags: SanitizedTagSchema.array().optional(),
reminder: RemindersSchema.extend({
recipients: z.string().array()
}).nullable()
}) })
.nullable() .nullable()
.array() .array()
@@ -743,6 +746,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations> ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
>[number]["secrets"][number] & { >[number]["secrets"][number] & {
isEmpty: boolean; isEmpty: boolean;
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
} }
> | null)[]; > | null)[];
})[] })[]
@@ -847,27 +851,38 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
); );
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
secretRotations = ( const rawSecretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
await server.services.secretRotationV2.getDashboardSecretRotations( {
{ projectId,
projectId, search,
search, orderBy,
orderBy, orderDirection,
orderDirection, environments: [environment],
environments: [environment], secretPath,
secretPath, limit: remainingLimit,
limit: remainingLimit, offset: adjustedOffset
offset: adjustedOffset },
}, req.permission
req.permission );
)
).map((rotation) => ({ 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, ...rotation,
secrets: rotation.secrets.map((secret) => secrets: rotation.secrets.map((secret) =>
secret secret
? { ? {
...secret, ...secret,
isEmpty: !secret.secretValue isEmpty: !secret.secretValue,
reminder: rotationReminders[secret.id] ?? null
} }
: secret : secret
) )
@@ -948,7 +963,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
search, search,
tagSlugs: tags, tagSlugs: tags,
includeTagsInSearch: true, includeTagsInSearch: true,
includeMetadataInSearch: true includeMetadataInSearch: true,
excludeRotatedSecrets: includeSecretRotations
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
@@ -970,7 +986,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
offset: adjustedOffset, offset: adjustedOffset,
tagSlugs: tags, tagSlugs: tags,
includeTagsInSearch: true, includeTagsInSearch: true,
includeMetadataInSearch: true includeMetadataInSearch: true,
excludeRotatedSecrets: includeSecretRotations
}) })
).secrets; ).secrets;
@@ -416,6 +416,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
tagSlugs?: string[]; tagSlugs?: string[];
includeTagsInSearch?: boolean; includeTagsInSearch?: boolean;
includeMetadataInSearch?: boolean; includeMetadataInSearch?: boolean;
excludeRotatedSecrets?: boolean;
} }
) => { ) => {
try { try {
@@ -481,6 +482,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
); );
} }
if (filters?.excludeRotatedSecrets) {
void query.whereNull(`${TableName.SecretRotationV2SecretMapping}.secretId`);
}
const secrets = await query; const secrets = await query;
// @ts-expect-error not inferred by knex // @ts-expect-error not inferred by knex
@@ -594,6 +599,11 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
void bd.whereIn(`${TableName.SecretTag}.slug`, slugs); void bd.whereIn(`${TableName.SecretTag}.slug`, slugs);
} }
}) })
.where((bd) => {
if (filters?.excludeRotatedSecrets) {
void bd.whereNull(`${TableName.SecretRotationV2SecretMapping}.secretId`);
}
})
.orderBy( .orderBy(
filters?.orderBy === SecretsOrderBy.Name ? "key" : "id", filters?.orderBy === SecretsOrderBy.Name ? "key" : "id",
filters?.orderDirection ?? OrderByDirection.ASC filters?.orderDirection ?? OrderByDirection.ASC
@@ -483,8 +483,8 @@ export const secretV2BridgeServiceFactory = ({
}); });
if (!sharedSecretToModify) if (!sharedSecretToModify)
throw new NotFoundError({ message: `Secret with name ${inputSecret.secretName} not found` }); throw new NotFoundError({ message: `Secret with name ${inputSecret.secretName} not found` });
if (sharedSecretToModify.isRotatedSecret && (inputSecret.newSecretName || inputSecret.secretValue)) if (sharedSecretToModify.isRotatedSecret && inputSecret.newSecretName)
throw new BadRequestError({ message: "Cannot update rotated secret name or value" }); throw new BadRequestError({ message: "Cannot update rotated secret name" });
secretId = sharedSecretToModify.id; secretId = sharedSecretToModify.id;
secret = sharedSecretToModify; secret = sharedSecretToModify;
} }
@@ -888,6 +888,7 @@ export const secretV2BridgeServiceFactory = ({
| "tagSlugs" | "tagSlugs"
| "environment" | "environment"
| "search" | "search"
| "excludeRotatedSecrets"
>) => { >) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -1934,8 +1935,14 @@ export const secretV2BridgeServiceFactory = ({
if (el.isRotatedSecret) { if (el.isRotatedSecret) {
const input = secretsToUpdateGroupByPath[secretPath].find((i) => i.secretKey === el.key); const input = secretsToUpdateGroupByPath[secretPath].find((i) => i.secretKey === el.key);
if (input && (input.newSecretName || input.secretValue)) if (input) {
throw new BadRequestError({ message: `Cannot update rotated secret name or value: ${el.key}` }); if (input.newSecretName) {
delete input.newSecretName;
}
if (input.secretValue !== undefined) {
delete input.secretValue;
}
}
} }
}); });
@@ -2061,8 +2068,11 @@ export const secretV2BridgeServiceFactory = ({
commitChanges, commitChanges,
inputSecrets: secretsToUpdate.map((el) => { inputSecrets: secretsToUpdate.map((el) => {
const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0]; const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0];
const shouldUpdateValue = !originalSecret.isRotatedSecret && typeof el.secretValue !== "undefined";
const shouldUpdateName = !originalSecret.isRotatedSecret && el.newSecretName;
const encryptedValue = const encryptedValue =
typeof el.secretValue !== "undefined" shouldUpdateValue && el.secretValue !== undefined
? { ? {
encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob, encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob,
references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences
@@ -2077,7 +2087,7 @@ export const secretV2BridgeServiceFactory = ({
(value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob
), ),
skipMultilineEncoding: el.skipMultilineEncoding, skipMultilineEncoding: el.skipMultilineEncoding,
key: el.newSecretName || el.secretKey, key: shouldUpdateName ? el.newSecretName : el.secretKey,
tags: el.tagIds, tags: el.tagIds,
secretMetadata: el.secretMetadata, secretMetadata: el.secretMetadata,
...encryptedValue ...encryptedValue
@@ -50,6 +50,7 @@ export type TGetSecretsDTO = {
limit?: number; limit?: number;
search?: string; search?: string;
keys?: string[]; keys?: string[];
excludeRotatedSecrets?: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TGetSecretsMissingReadValuePermissionDTO = Omit< export type TGetSecretsMissingReadValuePermissionDTO = Omit<
@@ -362,6 +363,7 @@ export type TFindSecretsByFolderIdsFilter = {
includeTagsInSearch?: boolean; includeTagsInSearch?: boolean;
includeMetadataInSearch?: boolean; includeMetadataInSearch?: boolean;
keys?: string[]; keys?: string[];
excludeRotatedSecrets?: boolean;
}; };
export type TGetSecretsRawByFolderMappingsDTO = { export type TGetSecretsRawByFolderMappingsDTO = {
@@ -1154,6 +1154,7 @@ export const secretServiceFactory = ({
| "search" | "search"
| "includeTagsInSearch" | "includeTagsInSearch"
| "includeMetadataInSearch" | "includeMetadataInSearch"
| "excludeRotatedSecrets"
>) => { >) => {
const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId);
@@ -214,6 +214,7 @@ export type TGetSecretsRawDTO = {
keys?: string[]; keys?: string[];
includeTagsInSearch?: boolean; includeTagsInSearch?: boolean;
includeMetadataInSearch?: boolean; includeMetadataInSearch?: boolean;
excludeRotatedSecrets?: boolean;
} & TProjectPermission; } & TProjectPermission;
export type TGetSecretAccessListDTO = { export type TGetSecretAccessListDTO = {
@@ -685,12 +685,17 @@ const Page = () => {
setDebouncedSearchFilter(""); setDebouncedSearchFilter("");
}; };
const getMergedSecretsWithPending = () => { const getMergedSecretsWithPending = (
paramSecrets?: (SecretV3RawSanitized | null)[]
): SecretV3RawSanitized[] => {
const sanitizedParamSecrets = paramSecrets?.filter(Boolean) as
| SecretV3RawSanitized[]
| undefined;
if (!isBatchMode || pendingChanges.secrets.length === 0) { if (!isBatchMode || pendingChanges.secrets.length === 0) {
return secrets; return sanitizedParamSecrets || secrets || [];
} }
const mergedSecrets = [...(secrets || [])] as (SecretV3RawSanitized & { const mergedSecrets = [...(sanitizedParamSecrets || secrets || [])] as (SecretV3RawSanitized & {
originalKey?: string; originalKey?: string;
})[]; })[];
@@ -1072,7 +1077,17 @@ const Page = () => {
/> />
)} )}
{canReadSecretRotations && Boolean(secretRotations?.length) && ( {canReadSecretRotations && Boolean(secretRotations?.length) && (
<SecretRotationListView secretRotations={secretRotations} /> <SecretRotationListView
secretRotations={secretRotations}
colWidth={colWidth}
tags={tags}
projectId={projectId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
getMergedSecretsWithPending={getMergedSecretsWithPending}
/>
)} )}
{canReadSecret && Boolean(mergedSecrets?.length) && ( {canReadSecret && Boolean(mergedSecrets?.length) && (
<SecretListView <SecretListView
@@ -733,7 +733,7 @@ export const SecretItem = memo(
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
ariaLabel="override-value" ariaLabel="override-value"
isDisabled={!isAllowed} isDisabled={!isAllowed || isRotatedSecret}
variant="plain" variant="plain"
size="sm" size="sm"
onClick={handleOverrideClick} onClick={handleOverrideClick}
@@ -742,7 +742,13 @@ export const SecretItem = memo(
isOverridden && "w-5 text-primary" isOverridden && "w-5 text-primary"
)} )}
> >
<Tooltip content={`${isOverridden ? "Remove" : "Add"} Override`}> <Tooltip
content={
isRotatedSecret
? "Unavailable for rotated secrets"
: `${isOverridden ? "Remove" : "Add"} Override`
}
>
<FontAwesomeSymbol <FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Override} symbolName={FontAwesomeSpriteName.Override}
className="h-3.5 w-3.5" className="h-3.5 w-3.5"
@@ -52,6 +52,7 @@ type Props = {
}[]; }[];
}[]; }[];
colWidth: number; colWidth: number;
excludePendingCreates?: boolean;
}; };
export const SecretListView = ({ export const SecretListView = ({
@@ -64,7 +65,8 @@ export const SecretListView = ({
isProtectedBranch = false, isProtectedBranch = false,
usedBySecretSyncs, usedBySecretSyncs,
importedBy, importedBy,
colWidth colWidth,
excludePendingCreates = false
}: Props) => { }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -580,27 +582,32 @@ export const SecretListView = ({
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => ( {FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
<FontAwesomeIcon icon={icon} symbol={symbol} key={`font-awesome-svg-spritie-${symbol}`} /> <FontAwesomeIcon icon={icon} symbol={symbol} key={`font-awesome-svg-spritie-${symbol}`} />
))} ))}
{secrets.map((secret) => ( {secrets
<SecretItem .filter((secret) => {
colWidth={colWidth} if (!excludePendingCreates) return true;
environment={environment} return !secret.isPending || secret.pendingAction !== PendingAction.Create;
secretPath={secretPath} })
tags={wsTags} .map((secret) => (
isSelected={Boolean(selectedSecrets?.[secret.id])} <SecretItem
onToggleSecretSelect={toggleSelectedSecret} colWidth={colWidth}
isVisible={isVisible} environment={environment}
secret={secret} secretPath={secretPath}
key={secret.id} tags={wsTags}
onSaveSecret={handleSaveSecret} isSelected={Boolean(selectedSecrets?.[secret.id])}
onDeleteSecret={onDeleteSecret} onToggleSecretSelect={toggleSelectedSecret}
onDetailViewSecret={onDetailViewSecret} isVisible={isVisible}
importedBy={importedBy} secret={secret}
onCreateTag={onCreateTag} key={secret.id}
onShareSecret={onShareSecret} onSaveSecret={handleSaveSecret}
isPending={secret.isPending} onDeleteSecret={onDeleteSecret}
pendingAction={secret.pendingAction} onDetailViewSecret={onDetailViewSecret}
/> importedBy={importedBy}
))} onCreateTag={onCreateTag}
onShareSecret={onShareSecret}
isPending={secret.isPending}
pendingAction={secret.pendingAction}
/>
))}
<DeleteActionModal <DeleteActionModal
isOpen={popUp.deleteSecret.isOpen} isOpen={popUp.deleteSecret.isOpen}
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key} deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
@@ -18,8 +18,11 @@ import { IconButton, Modal, ModalContent, TableContainer, Tag, Tooltip } from "@
import { ProjectPermissionSub } from "@app/context"; import { ProjectPermissionSub } from "@app/context";
import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2"; import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { SecretListView } from "../SecretListView";
import { SecretRotationSecretRow } from "./SecretRotationSecretRow"; import { SecretRotationSecretRow } from "./SecretRotationSecretRow";
type Props = { type Props = {
@@ -28,6 +31,23 @@ type Props = {
onRotate: () => void; onRotate: () => void;
onViewGeneratedCredentials: () => void; onViewGeneratedCredentials: () => void;
onDelete: () => 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 = ({ export const SecretRotationItem = ({
@@ -35,16 +55,40 @@ export const SecretRotationItem = ({
onEdit, onEdit,
onRotate, onRotate,
onViewGeneratedCredentials, onViewGeneratedCredentials,
onDelete onDelete,
projectId,
secretPath = "/",
tags = [],
isProtectedBranch = false,
usedBySecretSyncs,
importedBy,
colWidth,
getMergedSecretsWithPending
}: Props) => { }: Props) => {
const { name, type, environment, folder, secrets, description } = secretRotation; const { name, type, environment, folder, secrets, description } = secretRotation;
const { name: rotationType, image } = SECRET_ROTATION_MAP[type]; const { name: rotationType, image } = SECRET_ROTATION_MAP[type];
const [showSecrets, setShowSecrets] = useState(false); const [showSecrets, setShowSecrets] = useState(false);
const [isExpanded, setIsExpanded] = useState(true);
return ( return (
<> <>
<div className={twMerge("group flex border-b border-mineshaft-600 hover:bg-mineshaft-700")}> <div
className={twMerge(
"group flex cursor-pointer border-b border-mineshaft-600 hover:bg-mineshaft-700"
)}
onClick={() => 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}`}
>
<div className="text- flex w-11 items-center py-2 pl-5 text-mineshaft-400"> <div className="text- flex w-11 items-center py-2 pl-5 text-mineshaft-400">
<FontAwesomeIcon icon={faRotate} /> <FontAwesomeIcon icon={faRotate} />
</div> </div>
@@ -198,6 +242,20 @@ export const SecretRotationItem = ({
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
</div> </div>
{isExpanded && (
<SecretListView
colWidth={colWidth}
secrets={getMergedSecretsWithPending(secretRotation.secrets) || []}
tags={tags}
environment={environment.slug}
projectId={projectId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
excludePendingCreates
/>
)}
<Modal onOpenChange={setShowSecrets} isOpen={showSecrets}> <Modal onOpenChange={setShowSecrets} isOpen={showSecrets}>
<ModalContent <ModalContent
onOpenAutoFocus={(e) => e.preventDefault()} onOpenAutoFocus={(e) => e.preventDefault()}
@@ -3,15 +3,44 @@ import { EditSecretRotationV2Modal } from "@app/components/secret-rotations-v2/E
import { RotateSecretRotationV2Modal } from "@app/components/secret-rotations-v2/RotateSecretRotationV2Modal"; import { RotateSecretRotationV2Modal } from "@app/components/secret-rotations-v2/RotateSecretRotationV2Modal";
import { ViewSecretRotationV2GeneratedCredentialsModal } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials"; import { ViewSecretRotationV2GeneratedCredentialsModal } from "@app/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2"; import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { SecretRotationItem } from "./SecretRotationItem"; import { SecretRotationItem } from "./SecretRotationItem";
type Props = { type Props = {
secretRotations?: TSecretRotationV2[]; 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([ const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"editSecretRotation", "editSecretRotation",
"rotateSecretRotation", "rotateSecretRotation",
@@ -31,6 +60,14 @@ export const SecretRotationListView = ({ secretRotations }: Props) => {
handlePopUpOpen("viewSecretRotationGeneratedCredentials", secretRotation) handlePopUpOpen("viewSecretRotationGeneratedCredentials", secretRotation)
} }
onDelete={() => handlePopUpOpen("deleteSecretRotation", secretRotation)} onDelete={() => handlePopUpOpen("deleteSecretRotation", secretRotation)}
colWidth={colWidth}
tags={tags}
projectId={projectId}
secretPath={secretPath}
isProtectedBranch={isProtectedBranch}
importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
getMergedSecretsWithPending={getMergedSecretsWithPending}
/> />
))} ))}
<EditSecretRotationV2Modal <EditSecretRotationV2Modal