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
@@ -685,12 +685,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;
})[];
@@ -1072,7 +1077,17 @@ const Page = () => {
/>
)}
{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) && (
<SecretListView
@@ -733,7 +733,7 @@ export const SecretItem = memo(
{(isAllowed) => (
<IconButton
ariaLabel="override-value"
isDisabled={!isAllowed}
isDisabled={!isAllowed || isRotatedSecret}
variant="plain"
size="sm"
onClick={handleOverrideClick}
@@ -742,7 +742,13 @@ export const SecretItem = memo(
isOverridden && "w-5 text-primary"
)}
>
<Tooltip content={`${isOverridden ? "Remove" : "Add"} Override`}>
<Tooltip
content={
isRotatedSecret
? "Unavailable for rotated secrets"
: `${isOverridden ? "Remove" : "Add"} Override`
}
>
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Override}
className="h-3.5 w-3.5"
@@ -52,6 +52,7 @@ type Props = {
}[];
}[];
colWidth: number;
excludePendingCreates?: boolean;
};
export const SecretListView = ({
@@ -64,7 +65,8 @@ export const SecretListView = ({
isProtectedBranch = false,
usedBySecretSyncs,
importedBy,
colWidth
colWidth,
excludePendingCreates = false
}: Props) => {
const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
@@ -580,27 +582,32 @@ export const SecretListView = ({
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
<FontAwesomeIcon icon={icon} symbol={symbol} key={`font-awesome-svg-spritie-${symbol}`} />
))}
{secrets.map((secret) => (
<SecretItem
colWidth={colWidth}
environment={environment}
secretPath={secretPath}
tags={wsTags}
isSelected={Boolean(selectedSecrets?.[secret.id])}
onToggleSecretSelect={toggleSelectedSecret}
isVisible={isVisible}
secret={secret}
key={secret.id}
onSaveSecret={handleSaveSecret}
onDeleteSecret={onDeleteSecret}
onDetailViewSecret={onDetailViewSecret}
importedBy={importedBy}
onCreateTag={onCreateTag}
onShareSecret={onShareSecret}
isPending={secret.isPending}
pendingAction={secret.pendingAction}
/>
))}
{secrets
.filter((secret) => {
if (!excludePendingCreates) return true;
return !secret.isPending || secret.pendingAction !== PendingAction.Create;
})
.map((secret) => (
<SecretItem
colWidth={colWidth}
environment={environment}
secretPath={secretPath}
tags={wsTags}
isSelected={Boolean(selectedSecrets?.[secret.id])}
onToggleSecretSelect={toggleSelectedSecret}
isVisible={isVisible}
secret={secret}
key={secret.id}
onSaveSecret={handleSaveSecret}
onDeleteSecret={onDeleteSecret}
onDetailViewSecret={onDetailViewSecret}
importedBy={importedBy}
onCreateTag={onCreateTag}
onShareSecret={onShareSecret}
isPending={secret.isPending}
pendingAction={secret.pendingAction}
/>
))}
<DeleteActionModal
isOpen={popUp.deleteSecret.isOpen}
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 { ProjectPermissionSecretRotationActions } from "@app/context/ProjectPermissionContext/types";
import { SECRET_ROTATION_MAP } from "@app/helpers/secretRotationsV2";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { TSecretRotationV2 } from "@app/hooks/api/secretRotationsV2";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { SecretListView } from "../SecretListView";
import { SecretRotationSecretRow } from "./SecretRotationSecretRow";
type Props = {
@@ -28,6 +31,23 @@ type Props = {
onRotate: () => 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 (
<>
<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">
<FontAwesomeIcon icon={faRotate} />
</div>
@@ -198,6 +242,20 @@ export const SecretRotationItem = ({
</motion.div>
</AnimatePresence>
</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}>
<ModalContent
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 { 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}
/>
))}
<EditSecretRotationV2Modal