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
@@ -2,6 +2,7 @@ import { ForbiddenError } from "@casl/ability";
import { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
import { RemindersSchema } from "@app/db/schemas/reminders";
import { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema";
@@ -628,7 +629,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().optional()
}).nullish()
})
.array()
.optional(),
@@ -706,7 +710,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImports>> | undefined;
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"] | undefined;
let secrets:
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
})[]
| undefined;
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
let secretRotations:
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
@@ -904,7 +912,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
});
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = (
const rawSecrets = (
await server.services.secret.getSecretsRaw({
actorId: req.permission.id,
actor: req.permission.type,
@@ -925,6 +933,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
includeMetadataInSearch: true
})
).secrets;
const reminders = await server.services.reminder.getRemindersForDashboard(
rawSecrets.map((secret) => secret.id)
);
secrets = rawSecrets.map((secret) => ({
...secret,
reminder: reminders[secret.id] ?? null
}));
}
}
} catch (error) {
+26 -1
View File
@@ -124,10 +124,35 @@ export const reminderDALFactory = (db: TDbClient) => {
return reminders[0] || null;
};
const findSecretReminders = async (secretIds: string[], tx?: Knex) => {
const rawReminders = await (tx || db)(TableName.Reminder)
.whereIn(`${TableName.Reminder}.secretId`, secretIds)
.leftJoin(TableName.ReminderRecipient, `${TableName.Reminder}.id`, `${TableName.ReminderRecipient}.reminderId`)
.select(selectAllTableCols(TableName.Reminder))
.select(db.ref("userId").withSchema(TableName.ReminderRecipient));
const reminders = sqlNestRelationships({
data: rawReminders,
key: "id",
parentMapper: (el) => ({
_id: el.id,
...RemindersSchema.parse(el)
}),
childrenMapper: [
{
key: "userId",
label: "recipients" as const,
mapper: ({ userId }) => userId
}
]
});
return reminders;
};
return {
...reminderOrm,
findSecretDailyReminders,
findUpcomingReminders,
findSecretReminder
findSecretReminder,
findSecretReminders
};
};
@@ -372,6 +372,21 @@ export const reminderServiceFactory = ({
};
};
const getRemindersForDashboard: TReminderServiceFactory["getRemindersForDashboard"] = async (secretIds) => {
// scott we don't need to check permissions/secret existence because these are the
// secrets from the dashboard that have already gone through these checks
const reminders = await reminderDAL.findSecretReminders(secretIds);
const reminderMap: Record<string, (typeof reminders)[number]> = {};
reminders.forEach((reminder) => {
if (reminder.secretId) reminderMap[reminder.secretId] = reminder;
});
return reminderMap;
};
return {
createReminder,
getReminder,
@@ -379,6 +394,7 @@ export const reminderServiceFactory = ({
deleteReminder,
deleteReminderBySecretId,
batchCreateReminders,
createReminderInternal
createReminderInternal,
getRemindersForDashboard
};
};
@@ -103,4 +103,6 @@ export interface TReminderServiceFactory {
id: string;
created: boolean;
}>;
getRemindersForDashboard: (secretIds: string[]) => Promise<Record<string, TReminder & { recipients: string[] }>>;
}
@@ -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 = {
@@ -1,15 +1,16 @@
import { useEffect, useMemo } from "react";
import { useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
import {
faArrowRotateRight,
faCheckCircle,
faClock,
faCopy,
faDesktop,
faEyeSlash,
faPlus,
faProjectDiagram,
faSearch,
faServer,
faShare,
faTag,
@@ -26,6 +27,7 @@ import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import { hasSecretReference, SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails";
import {
Button,
Drawer,
@@ -39,30 +41,26 @@ import {
FormLabel,
IconButton,
Input,
Modal,
ModalContent,
Switch,
Tag,
TextArea,
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { getProjectBaseURL } from "@app/helpers/project";
import { usePopUp, useToggle } from "@app/hooks";
import { usePopUp } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { useGetReminder } from "@app/hooks/api/reminders";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { camelCaseToSpaces } from "@app/lib/fn/string";
import { CreateReminderForm } from "./CreateReminderForm";
import { HIDDEN_SECRET_VALUE } from "./SecretItem";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -102,7 +100,7 @@ export const SecretDetailSidebar = ({
handleSubmit,
setValue,
reset,
formState: { isDirty, isSubmitting }
formState: { isDirty }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
values: secret,
@@ -110,12 +108,12 @@ export const SecretDetailSidebar = ({
});
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
"secretAccessUpgradePlan"
"secretAccessUpgradePlan",
"secretReferenceTree"
] as const);
const { permission } = useProjectPermission();
const { currentWorkspace } = useWorkspace();
const { data: reminderData } = useGetReminder(secret?.id);
const tagFields = useFieldArray({
control,
@@ -188,25 +186,6 @@ export const SecretDetailSidebar = ({
secretKey
});
const handleOverrideClick = () => {
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;
setValue(
"overrideAction",
isUnsavedOverride ? secret?.overrideAction : SecretActionType.Deleted,
{
shouldDirty: !isUnsavedOverride
}
);
setValue("valueOverride", secret?.valueOverride, { shouldDirty: !isUnsavedOverride });
} else {
setValue("overrideAction", SecretActionType.Modified, { shouldDirty: true });
setValue("valueOverride", "", { shouldDirty: true });
}
};
const handleTagSelect = (tag: WsTag) => {
if (selectedTagsGroupById?.[tag.id]) {
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
@@ -222,8 +201,6 @@ export const SecretDetailSidebar = ({
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
};
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
useEffect(() => {
setValue(
"reminderRecipients",
@@ -289,34 +266,8 @@ export const SecretDetailSidebar = ({
}
};
const getDaysUntilReminder = useMemo(() => {
return (): string => {
const now = new Date();
now.setHours(0, 0, 0, 0);
const target = new Date(reminderData?.nextReminderDate || "");
target.setHours(0, 0, 0, 0);
const diffTime = target.getTime() - now.getTime();
const daysRemaining = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
return `Days until next reminder: ${daysRemaining}`;
};
}, [reminderData]);
return (
<>
<CreateReminderForm
isOpen={createReminderFormOpen}
onOpenChange={() => {
setCreateReminderFormOpen.toggle();
}}
workspaceId={currentWorkspace.id}
environment={environment}
secretPath={secretPath}
secretId={secret?.id}
reminder={reminderData}
/>
<UpgradePlanModal
isOpen={popUp.secretAccessUpgradePlan.isOpen}
onOpenChange={(isUpgradeModalOpen) =>
@@ -324,28 +275,45 @@ export const SecretDetailSidebar = ({
}
text="Secret access analysis is only available on Infisical's Pro plan and above."
/>
<Modal
isOpen={popUp.secretReferenceTree.isOpen}
onOpenChange={(isSecretRefOpen) =>
handlePopUpToggle("secretReferenceTree", isSecretRefOpen)
}
>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()}
>
<SecretReferenceTree
secretPath={secretPath}
environment={environment}
secretKey={popUp.secretReferenceTree.data}
/>
</ModalContent>
</Modal>
<Drawer
onOpenChange={(state) => {
onOpenChange={async (state) => {
if (isOpen && isDirty) {
if (
// eslint-disable-next-line no-alert
window.confirm(
"You have edited the secret. Are you sure you want to reset the change?"
)
) {
onToggle(false);
reset();
} else return;
await handleSubmit(handleFormSubmit)();
}
onToggle(state);
}}
isOpen={isOpen}
>
<DrawerContent title={`Secret – ${secret?.key}`} className="thin-scrollbar">
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
<div className="flex h-full flex-col">
<div className="flex flex-row">
<div className="w-full">
<DrawerContent
title={`Secret – ${secret?.key}`}
className="thin-scrollbar h-full"
cardBodyClassName="pb-0"
>
<form
onSubmit={handleSubmit(handleFormSubmit)}
className="flex h-full flex-1 flex-col gap-y-4"
>
<span className="text-sm text-bunker-300">
Changes will automatically be applied for commit
</span>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
@@ -361,9 +329,8 @@ export const SecretDetailSidebar = ({
key="secret-value"
control={control}
render={({ field }) => (
<div className="flex items-center gap-2">
<FormControl
className="flex-1"
className="mb-0"
helperText={
cannotReadSecretValue ? (
<div className="flex space-x-2">
@@ -372,22 +339,22 @@ export const SecretDetailSidebar = ({
className="mt-0.5 text-yellow-400"
/>
<span>
The value of this secret is hidden because you do not have the
read secret value permission.
The value of this secret is hidden because you do not have the read
secret value permission.
</span>
</div>
) : undefined
}
label="Value"
>
<div className="flex items-center gap-2">
<div className="flex items-start gap-x-2">
<InfisicalSecretInput
isReadOnly={isReadOnly || !isAllowed || secret?.isRotatedSecret}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden}
containerClassName="text-bunker-300 w-full hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
containerClassName="text-bunker-300 w-full hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
{...field}
autoFocus={false}
/>
@@ -397,9 +364,7 @@ export const SecretDetailSidebar = ({
? "This project does not allow secret sharing."
: "You don't have permission to view the secret value."
}
isDisabled={
!secret?.secretValueHidden && currentWorkspace.secretSharing
}
isDisabled={!secret?.secretValueHidden && currentWorkspace.secretSharing}
>
<Button
isDisabled={
@@ -420,15 +385,11 @@ export const SecretDetailSidebar = ({
</Tooltip>
</div>
</FormControl>
</div>
)}
/>
)}
</ProjectPermissionCan>
</div>
</div>
<div className="mb-2 rounded border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div className="mb-4 px-4">
<div className="rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
<Controller
control={control}
name="skipMultilineEncoding"
@@ -467,58 +428,7 @@ export const SecretDetailSidebar = ({
)}
/>
</div>
<div
className={`mb-4 w-full border-t border-mineshaft-600 ${isOverridden ? "block" : "hidden"}`}
/>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<div className="flex items-center justify-between px-4 pb-4">
<span className="w-max text-sm text-mineshaft-300">
Override with a personal value
<Tooltip
content="Override the secret value with a personal value that does not get shared with other users and machines."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip>
</span>
<Switch
isDisabled={!isAllowed}
id="personal-override"
onCheckedChange={handleOverrideClick}
isChecked={isOverridden}
className="justify-start"
/>
</div>
)}
</ProjectPermissionCan>
{isOverridden && (
<Controller
name="valueOverride"
control={control}
render={({ field }) => (
<FormControl label="Override Value" className="px-4">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>
</FormControl>
)}
/>
)}
</div>
<div className="mb-4 mt-2 flex flex-col rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div className="flex flex-col rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
<div
className={`flex justify-between px-4 text-mineshaft-100 ${tagFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
>
@@ -530,7 +440,7 @@ export const SecretDetailSidebar = ({
<div>
<FormControl>
<div
className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
className={`flex flex-wrap gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
>
{tagFields.fields.map(({ tagColor, id: formId, slug }) => (
<Tag
@@ -631,7 +541,6 @@ export const SecretDetailSidebar = ({
<div
className={`mb-4 w-full border-t border-mineshaft-600 ${tagFields.fields.length > 0 || metadataFormFields.fields.length > 0 ? "block" : "hidden"}`}
/>
<div
className={`flex justify-between px-4 text-mineshaft-100 ${metadataFormFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
>
@@ -715,51 +624,21 @@ export const SecretDetailSidebar = ({
label="Comments & Notes"
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
className="mb-0 h-[125px] pb-0"
>
<TextArea
className="border border-mineshaft-600 bg-bunker-800 text-sm"
className="mb-0 !resize-none border border-mineshaft-600 bg-mineshaft-900 text-sm"
readOnly={isReadOnly}
rows={5}
placeholder="add a comment or note to this secret..."
rows={4}
{...field}
/>
</FormControl>
)}
/>
<FormControl>
{reminderData && reminderData.nextReminderDate ? (
<div className="flex items-center justify-between px-2">
<div className="flex items-center space-x-2">
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
<span className="text-sm text-bunker-300">{getDaysUntilReminder()}</span>
</div>
<div>
<Button
className="px-2 py-1"
variant="outline_bg"
onClick={() => setCreateReminderFormOpen.on()}
>
Update
</Button>
</div>
</div>
) : (
<div className="ml-1 flex items-center space-x-2">
<Button
className="w-full px-2 py-2 font-normal"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faClock} />}
onClick={() => setCreateReminderFormOpen.on()}
isDisabled={cannotEditSecret}
>
Create Reminder
</Button>
</div>
)}
</FormControl>
<div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
<div className="mb-2 pl-1">Version History</div>
<div className="thin-scrollbar flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
<div className="dark flex max-h-[24rem] flex-1 cursor-default flex-col text-sm text-bunker-300">
<div className="mb-0.5 text-mineshaft-400">Version History</div>
<div className="thin-scrollbar flex flex-1 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
{secretVersion?.map(
({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
<div className="flex flex-row" key={id}>
@@ -781,9 +660,7 @@ export const SecretDetailSidebar = ({
<div className="flex flex-row">
<div className="flex w-fit flex-row text-sm">
Modified by:
<Tooltip
content={getModifiedByName(actor.actorType, actor.name)}
>
<Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
onClick={() =>
@@ -925,9 +802,7 @@ export const SecretDetailSidebar = ({
variant="outline_bg"
size="sm"
className="h-8 w-8 rounded-md"
onClick={() =>
setValue("value", secretValue, { shouldDirty: true })
}
onClick={() => setValue("value", secretValue, { shouldDirty: true })}
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
@@ -939,8 +814,8 @@ export const SecretDetailSidebar = ({
)}
</div>
</div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2 mt-4">
<div className="dark flex flex-col text-sm text-bunker-300">
<div className="mb-0.5 text-mineshaft-400">
Access List
<Tooltip
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret."
@@ -949,24 +824,8 @@ export const SecretDetailSidebar = ({
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip>
</div>
{isPending && (
<Button className="w-full px-2 py-1" variant="outline_bg" isDisabled>
Analyze Access
</Button>
)}
{!isPending && secretAccessList === undefined && (
<Button
className="w-full px-2 py-1"
variant="outline_bg"
onClick={() => {
handlePopUpOpen("secretAccessUpgradePlan");
}}
>
Analyze Access
</Button>
)}
{!isPending && secretAccessList && (
<div className="mb-4 flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
{secretAccessList ? (
<div className="flex flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 dark:[color-scheme:dark]">
{secretAccessList.users.length > 0 && (
<div className="pb-3">
<div className="mb-2 font-bold">Users</div>
@@ -974,6 +833,7 @@ export const SecretDetailSidebar = ({
{secretAccessList.users.map((user) => (
<div className="rounded-md bg-bunker-500">
<Tooltip
side="left"
content={user.allowedActions
.map((action) => camelCaseToSpaces(action))
.join(", ")}
@@ -987,7 +847,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id,
membershipId: user.membershipId
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-mineshaft-100"
>
{user.name}
</Link>
@@ -1004,6 +864,7 @@ export const SecretDetailSidebar = ({
{secretAccessList.identities.map((identity) => (
<div className="rounded-md bg-bunker-500">
<Tooltip
side="left"
content={identity.allowedActions
.map(
(action) =>
@@ -1020,7 +881,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id,
identityId: identity.id
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-mineshaft-100"
>
{identity.name}
</Link>
@@ -1037,6 +898,7 @@ export const SecretDetailSidebar = ({
{secretAccessList.groups.map((group) => (
<div className="rounded-md bg-bunker-500">
<Tooltip
side="left"
content={group.allowedActions
.map(
(action) =>
@@ -1050,7 +912,7 @@ export const SecretDetailSidebar = ({
params={{
groupId: group.id
}}
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-primary"
className="text-secondary/80 rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 py-0.5 text-sm hover:text-mineshaft-100"
>
{group.name}
</Link>
@@ -1061,33 +923,41 @@ export const SecretDetailSidebar = ({
</div>
)}
</div>
)}
</div>
<div className="flex flex-col space-y-4">
<div className="mb-4 flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
) : (
<Button
isFullWidth
type="submit"
className="w-full"
leftIcon={<FontAwesomeIcon icon={faSearch} />}
variant="outline_bg"
isDisabled={isSubmitting || !isDirty || !isAllowed}
isLoading={isSubmitting}
isDisabled={isPending}
isLoading={isPending}
onClick={() => handlePopUpOpen("secretAccessUpgradePlan")}
>
Apply Changes
Analyze Access
</Button>
)}
</ProjectPermissionCan>
<div className="flex items-center gap-2">
<Tooltip content="Copy Secret ID">
</div>
<div className="mt-auto flex items-center space-x-2 pb-4">
<Tooltip
content={
hasSecretReference(secret?.value)
? undefined
: "Secret does not contain any references."
}
className="z-[100] text-center"
>
<div className="flex-1">
<Button
className="w-full"
variant="outline_bg"
isDisabled={cannotReadSecretValue || !hasSecretReference(secret?.value)}
leftIcon={<FontAwesomeIcon icon={faProjectDiagram} />}
onClick={() => handlePopUpOpen("secretReferenceTree", secretKey)}
>
Secret Reference Tree
</Button>
</div>
</Tooltip>
<Tooltip content="Copy Secret ID" className="z-[100]">
<IconButton
variant="outline_bg"
ariaLabel="Copy Secret ID"
@@ -1114,12 +984,12 @@ export const SecretDetailSidebar = ({
})}
>
{(isAllowed) => (
<Tooltip content="Delete Secret">
<Tooltip content="Delete Secret" align="end" className="z-[100]">
<IconButton
colorSchema="danger"
variant="outline_bg"
ariaLabel="Delete Secret"
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
className="h-min border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
isDisabled={!isAllowed}
onClick={onDeleteSecret}
>
@@ -1129,9 +999,6 @@ export const SecretDetailSidebar = ({
)}
</ProjectPermissionCan>
</div>
</div>
</div>
</div>
</form>
</DrawerContent>
</Drawer>
@@ -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,7 +459,6 @@ 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"
@@ -471,6 +467,7 @@ export const SecretItem = memo(
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>
</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"
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="reference-tree"
isDisabled={!isAllowed || !hasSecretReference(secret?.value)}
ariaLabel="Secret reminder"
isDisabled={!isAllowed || isOverridden}
>
<Tooltip
className="max-w-2xl"
content={
hasSecretReference(secret?.value)
? "Secret Reference Tree"
: "Secret does not contain references"
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"
)
}
>
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.SecretReferenceTree}
symbolName={FontAwesomeSpriteName.Reminder}
/>
</Tooltip>
</IconButton>
</ModalTrigger>
<ModalContent
title="Secret Reference Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()}
>
<SecretReferenceTree
secretPath={secretPath}
environment={environment}
secretKey={secret?.key}
/>
</ModalContent>
</Modal>
)}
</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"
)}
>
<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={isOverridden ? "Unavailable with override" : "Comment"}
>
<Tooltip content="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 }
];