Merge pull request #4312 from Infisical/secret-sidebar-details-refactor

improvement(frontend): improve UX and design of secret sidebar/table row
This commit is contained in:
Scott Wilson
2025-08-07 17:53:30 -07:00
committed by GitHub
11 changed files with 861 additions and 893 deletions
@@ -125,7 +125,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
if (isPending) {
return (
<div className="flex items-center justify-center py-4">
<Spinner size="xs" />
<Spinner className="text-mineshaft-400" />
</div>
);
}
+19 -3
View File
@@ -13,6 +13,7 @@ export type DrawerContentProps = DialogPrimitive.DialogContentProps & {
subTitle?: ReactNode;
footerContent?: ReactNode;
onClose?: () => void;
cardBodyClassName?: string;
} & VariantProps<typeof drawerContentVariation>;
const drawerContentVariation = cva(
@@ -32,7 +33,17 @@ const drawerContentVariation = cva(
export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
(
{ children, title, subTitle, className, footerContent, direction = "right", onClose, ...props },
{
children,
title,
subTitle,
className,
footerContent,
direction = "right",
onClose,
cardBodyClassName,
...props
},
forwardedRef
) => (
<DialogPrimitive.Portal>
@@ -47,11 +58,16 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
>
<Card isRounded={false} className="dark h-full w-full">
{title && (
<CardTitle subTitle={subTitle} className="px-4">
<CardTitle subTitle={subTitle} className="mb-0 px-4">
{title}
</CardTitle>
)}
<CardBody className="flex-grow overflow-y-auto overflow-x-hidden px-4 dark:[color-scheme:dark]">
<CardBody
className={twMerge(
"flex-grow overflow-y-auto overflow-x-hidden px-4 pt-4 dark:[color-scheme:dark]",
cardBodyClassName
)}
>
{children}
</CardBody>
{footerContent && <CardFooter>{footerContent}</CardFooter>}{" "}
+2 -1
View File
@@ -88,7 +88,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
path: el.secretPath,
secretMetadata: el.secretMetadata,
isRotatedSecret: el.isRotatedSecret,
rotationId: el.rotationId
rotationId: el.rotationId,
reminder: el.reminder
};
if (el.type === SecretType.Personal) {
+3
View File
@@ -1,4 +1,5 @@
import { ProjectPermissionActions } from "@app/context";
import { Reminder } from "@app/hooks/api/reminders/types";
import { PendingAction } from "../secretFolders/types";
import type { WsTag } from "../tags/types";
@@ -69,6 +70,7 @@ export type SecretV3RawSanitized = {
rotationId?: string;
isPending?: boolean;
pendingAction?: PendingAction;
reminder?: Reminder;
};
export type SecretV3Raw = {
@@ -94,6 +96,7 @@ export type SecretV3Raw = {
isRotatedSecret?: boolean;
rotationId?: string;
secretReminderRecipients?: SecretReminderRecipient[];
reminder?: Reminder;
};
export type SecretV3RawResponse = {
@@ -12,11 +12,9 @@ import {
DropdownMenuTrigger,
FontAwesomeSymbol,
FormControl,
GenericFieldLabel,
IconButton,
Input,
Modal,
ModalContent,
ModalTrigger,
Popover,
PopoverContent,
PopoverTrigger,
@@ -41,16 +39,14 @@ import { AnimatePresence, motion } from "framer-motion";
import { memo, useCallback, useEffect, useRef } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import {
hasSecretReference,
SecretReferenceTree
} from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
import { PendingAction } from "@app/hooks/api/secretFolders/types";
import { format } from "date-fns";
import { CreateReminderForm } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CreateReminderForm";
import {
FontAwesomeSpriteName,
formSchema,
@@ -113,7 +109,8 @@ export const SecretItem = memo(
colWidth
}: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"editSecret"
"editSecret",
"reminder"
] as const);
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
@@ -186,7 +183,7 @@ export const SecretItem = memo(
name: "tags"
});
const isOverriden =
const isOverridden =
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
const hasTagsApplied = Boolean(fields.length);
@@ -272,7 +269,7 @@ export const SecretItem = memo(
}, [isSecValueCopied]);
const handleOverrideClick = () => {
if (isOverriden) {
if (isOverridden) {
// override need not be flagged delete if it was never saved in server
// meaning a new unsaved personal secret but user toggled back later
const isUnsavedOverride = !secret.idOverride;
@@ -327,7 +324,7 @@ export const SecretItem = memo(
const copyTokenToClipboard = () => {
const [overrideValue, value] = getValues(["value", "valueOverride"]);
if (isOverriden) {
if (isOverridden) {
navigator.clipboard.writeText(value as string);
} else {
navigator.clipboard.writeText(overrideValue as string);
@@ -335,7 +332,7 @@ export const SecretItem = memo(
setIsSecValueCopied.on();
};
const isInAutoSaveMode = isDirty && !isSubmitting && !isOverriden;
const isInAutoSaveMode = isDirty && !isSubmitting && !isOverridden;
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
@@ -398,7 +395,7 @@ export const SecretItem = memo(
isReadOnly={isReadOnly || isRotatedSecret}
autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain"
isDisabled={isOverriden}
isDisabled={isOverridden}
placeholder={error?.message}
isError={Boolean(error)}
onKeyUp={() => trigger("key")}
@@ -413,14 +410,14 @@ export const SecretItem = memo(
tabIndex={0}
role="button"
>
{secretValueHidden && !isOverriden && !isPending && (
{secretValueHidden && !isOverridden && !isPending && (
<Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
>
<FontAwesomeIcon className="pr-2" size="sm" icon={faEyeSlash} />
</Tooltip>
)}
{isOverriden ? (
{isOverridden ? (
<Controller
name="valueOverride"
key="value-overriden"
@@ -445,7 +442,7 @@ export const SecretItem = memo(
isReadOnly={isReadOnlySecret}
key="secret-value"
isVisible={isVisible && (!secretValueHidden || isPending)}
canEditButNotView={secretValueHidden && !isOverriden && !isPending}
canEditButNotView={secretValueHidden && !isOverridden && !isPending}
environment={environment}
secretPath={secretPath}
{...field}
@@ -462,15 +459,15 @@ export const SecretItem = memo(
key="actions"
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
>
<Tooltip content="Copy secret">
<IconButton
isDisabled={secret.secretValueHidden}
ariaLabel="copy-value"
variant="plain"
size="sm"
className="w-0 overflow-hidden p-0 group-hover:w-5"
onClick={copyTokenToClipboard}
>
<IconButton
isDisabled={secret.secretValueHidden}
ariaLabel="copy-value"
variant="plain"
size="sm"
className="w-0 overflow-hidden p-0 group-hover:w-5"
onClick={copyTokenToClipboard}
>
<Tooltip content="Copy secret">
<FontAwesomeSymbol
className="h-3.5 w-3"
symbolName={
@@ -479,8 +476,8 @@ export const SecretItem = memo(
: FontAwesomeSpriteName.ClipboardCopy
}
/>
</IconButton>
</Tooltip>
</Tooltip>
</IconButton>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
@@ -491,41 +488,47 @@ export const SecretItem = memo(
})}
>
{(isAllowed) => (
<Modal>
<ModalTrigger asChild>
<IconButton
className="w-0 overflow-hidden p-0 group-hover:w-5"
variant="plain"
size="md"
ariaLabel="reference-tree"
isDisabled={!isAllowed || !hasSecretReference(secret?.value)}
>
<Tooltip
content={
hasSecretReference(secret?.value)
? "Secret Reference Tree"
: "Secret does not contain references"
}
>
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.SecretReferenceTree}
/>
</Tooltip>
</IconButton>
</ModalTrigger>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()}
<IconButton
className={twMerge(
"w-0 overflow-hidden p-0 group-hover:w-5",
secret.reminder && "w-5 text-primary"
)}
onClick={() => handlePopUpOpen("reminder")}
variant="plain"
size="md"
ariaLabel="Secret reminder"
isDisabled={!isAllowed || isOverridden}
>
<Tooltip
className="max-w-2xl"
content={
isOverridden ? (
"Unavailable with override"
) : secret.reminder ? (
<div className="flex flex-col gap-y-1">
<GenericFieldLabel label="Reminder Date">
{secret.reminder.nextReminderDate
? format(
new Date(secret.reminder.nextReminderDate),
"h:mm aa - MMM d yyyy"
)
: undefined}
</GenericFieldLabel>
<GenericFieldLabel label="Message">
{secret.reminder.message}
</GenericFieldLabel>
</div>
) : (
"Set Secret Reminder"
)
}
>
<SecretReferenceTree
secretPath={secretPath}
environment={environment}
secretKey={secret?.key}
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Reminder}
/>
</ModalContent>
</Modal>
</Tooltip>
</IconButton>
)}
</ProjectPermissionCan>
<DropdownMenu>
@@ -539,7 +542,7 @@ export const SecretItem = memo(
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild disabled={!isAllowed}>
<DropdownMenuTrigger asChild disabled={!isAllowed || isOverridden}>
<IconButton
ariaLabel="tags"
variant="plain"
@@ -548,9 +551,9 @@ export const SecretItem = memo(
"w-0 overflow-hidden p-0 group-hover:w-5 data-[state=open]:w-5",
hasTagsApplied && "w-5 text-primary"
)}
isDisabled={!isAllowed}
isDisabled={!isAllowed || isOverridden}
>
<Tooltip content="Tags">
<Tooltip content={isOverridden ? "Unavailable with override" : "Tags"}>
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Tags}
@@ -617,8 +620,6 @@ export const SecretItem = memo(
secretName,
secretTags: selectedTagSlugs
})}
renderTooltip
allowedLabel="Override"
>
{(isAllowed) => (
<IconButton
@@ -629,13 +630,15 @@ export const SecretItem = memo(
onClick={handleOverrideClick}
className={twMerge(
"w-0 overflow-hidden p-0 group-hover:w-5",
isOverriden && "w-5 text-primary"
isOverridden && "w-5 text-primary"
)}
>
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Override}
className="h-3.5 w-3.5"
/>
<Tooltip content={`${isOverridden ? "Remove" : "Add"} Override`}>
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Override}
className="h-3.5 w-3.5"
/>
</Tooltip>
</IconButton>
)}
</ProjectPermissionCan>
@@ -650,7 +653,7 @@ export const SecretItem = memo(
})}
>
{(isAllowed) => (
<PopoverTrigger asChild disabled={!isAllowed}>
<PopoverTrigger asChild disabled={!isAllowed || isOverridden}>
<IconButton
className={twMerge(
"w-0 overflow-hidden p-0 group-hover:w-5",
@@ -659,9 +662,11 @@ export const SecretItem = memo(
variant="plain"
size="md"
ariaLabel="add-comment"
isDisabled={!isAllowed}
isDisabled={!isAllowed || isOverridden}
>
<Tooltip content="Comment">
<Tooltip
content={isOverridden ? "Unavailable with override" : "Comment"}
>
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Comment}
@@ -851,12 +856,13 @@ export const SecretItem = memo(
animate={{ x: 0, opacity: 1 }}
exit={{ x: 10, opacity: 0 }}
>
<Tooltip content="More">
<Tooltip content={isOverridden ? "Unavailable with override" : "More"}>
<IconButton
ariaLabel="more"
variant="plain"
size="md"
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
isDisabled={isOverridden}
onClick={() => onDetailViewSecret(secret)}
>
<FontAwesomeSymbol
@@ -874,7 +880,13 @@ export const SecretItem = memo(
secretTags: selectedTagSlugs
})}
renderTooltip
allowedLabel={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"}
allowedLabel={
isOverridden
? "Unavailable with override"
: isRotatedSecret
? "Cannot Delete Rotated Secret"
: "Delete"
}
>
{(isAllowed) => (
<IconButton
@@ -884,7 +896,7 @@ export const SecretItem = memo(
size="md"
className="p-0 opacity-0 group-hover:opacity-100"
onClick={() => onDeleteSecret(secret)}
isDisabled={!isAllowed || isRotatedSecret}
isDisabled={!isAllowed || isRotatedSecret || isOverridden}
>
<FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Trash}
@@ -959,6 +971,15 @@ export const SecretItem = memo(
</AnimatePresence>
</div>
</div>
<CreateReminderForm
isOpen={popUp.reminder.isOpen}
onOpenChange={() => handlePopUpToggle("reminder")}
workspaceId={currentWorkspace.id}
environment={environment}
secretPath={secretPath}
secretId={secret?.id}
reminder={secret.reminder}
/>
<DeleteActionModal
isOpen={popUp.editSecret.isOpen}
deleteKey="confirm"
@@ -1,6 +1,7 @@
/* eslint-disable no-nested-ternary */
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faBell,
faCheck,
faClock,
faClone,
@@ -11,7 +12,6 @@ import {
faEllipsis,
faKey,
faLock,
faProjectDiagram,
faShare,
faTags,
faTrash
@@ -83,7 +83,7 @@ export enum FontAwesomeSpriteName {
ReplicatedSecretKey = "secret-replicated",
ShareSecret = "share-secret",
KeyLock = "key-lock",
SecretReferenceTree = "secret-reference-tree",
Reminder = "secret-reminder",
Trash = "trash"
}
@@ -103,6 +103,6 @@ export const FontAwesomeSpriteSymbols = [
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock },
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree },
{ icon: faBell, symbol: FontAwesomeSpriteName.Reminder },
{ icon: faTrash, symbol: FontAwesomeSpriteName.Trash }
];