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 { z } from "zod";
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas"; 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 { EventType, UserAgentType } from "@app/ee/services/audit-log/audit-log-types";
import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionSecretActions } from "@app/ee/services/permission/project-permission";
import { SecretRotationV2Schema } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-union-schema"; 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(), 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().optional()
}).nullish()
}) })
.array() .array()
.optional(), .optional(),
@@ -706,7 +710,11 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImports>> | undefined; let imports: Awaited<ReturnType<typeof server.services.secretImport.getImports>> | undefined;
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | 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 dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
let secretRotations: let secretRotations:
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>> | Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
@@ -904,7 +912,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}); });
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
secrets = ( const rawSecrets = (
await server.services.secret.getSecretsRaw({ await server.services.secret.getSecretsRaw({
actorId: req.permission.id, actorId: req.permission.id,
actor: req.permission.type, actor: req.permission.type,
@@ -925,6 +933,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
includeMetadataInSearch: true includeMetadataInSearch: true
}) })
).secrets; ).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) { } catch (error) {
+26 -1
View File
@@ -124,10 +124,35 @@ export const reminderDALFactory = (db: TDbClient) => {
return reminders[0] || null; 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 { return {
...reminderOrm, ...reminderOrm,
findSecretDailyReminders, findSecretDailyReminders,
findUpcomingReminders, 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 { return {
createReminder, createReminder,
getReminder, getReminder,
@@ -379,6 +394,7 @@ export const reminderServiceFactory = ({
deleteReminder, deleteReminder,
deleteReminderBySecretId, deleteReminderBySecretId,
batchCreateReminders, batchCreateReminders,
createReminderInternal createReminderInternal,
getRemindersForDashboard
}; };
}; };
@@ -103,4 +103,6 @@ export interface TReminderServiceFactory {
id: string; id: string;
created: boolean; created: boolean;
}>; }>;
getRemindersForDashboard: (secretIds: string[]) => Promise<Record<string, TReminder & { recipients: string[] }>>;
} }
@@ -125,7 +125,7 @@ export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Prop
if (isPending) { if (isPending) {
return ( return (
<div className="flex items-center justify-center py-4"> <div className="flex items-center justify-center py-4">
<Spinner size="xs" /> <Spinner className="text-mineshaft-400" />
</div> </div>
); );
} }
+19 -3
View File
@@ -13,6 +13,7 @@ export type DrawerContentProps = DialogPrimitive.DialogContentProps & {
subTitle?: ReactNode; subTitle?: ReactNode;
footerContent?: ReactNode; footerContent?: ReactNode;
onClose?: () => void; onClose?: () => void;
cardBodyClassName?: string;
} & VariantProps<typeof drawerContentVariation>; } & VariantProps<typeof drawerContentVariation>;
const drawerContentVariation = cva( const drawerContentVariation = cva(
@@ -32,7 +33,17 @@ const drawerContentVariation = cva(
export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>( 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 forwardedRef
) => ( ) => (
<DialogPrimitive.Portal> <DialogPrimitive.Portal>
@@ -47,11 +58,16 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
> >
<Card isRounded={false} className="dark h-full w-full"> <Card isRounded={false} className="dark h-full w-full">
{title && ( {title && (
<CardTitle subTitle={subTitle} className="px-4"> <CardTitle subTitle={subTitle} className="mb-0 px-4">
{title} {title}
</CardTitle> </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} {children}
</CardBody> </CardBody>
{footerContent && <CardFooter>{footerContent}</CardFooter>}{" "} {footerContent && <CardFooter>{footerContent}</CardFooter>}{" "}
+2 -1
View File
@@ -88,7 +88,8 @@ export const mergePersonalSecrets = (rawSecrets: SecretV3Raw[]) => {
path: el.secretPath, path: el.secretPath,
secretMetadata: el.secretMetadata, secretMetadata: el.secretMetadata,
isRotatedSecret: el.isRotatedSecret, isRotatedSecret: el.isRotatedSecret,
rotationId: el.rotationId rotationId: el.rotationId,
reminder: el.reminder
}; };
if (el.type === SecretType.Personal) { if (el.type === SecretType.Personal) {
+3
View File
@@ -1,4 +1,5 @@
import { ProjectPermissionActions } from "@app/context"; import { ProjectPermissionActions } from "@app/context";
import { Reminder } from "@app/hooks/api/reminders/types";
import { PendingAction } from "../secretFolders/types"; import { PendingAction } from "../secretFolders/types";
import type { WsTag } from "../tags/types"; import type { WsTag } from "../tags/types";
@@ -69,6 +70,7 @@ export type SecretV3RawSanitized = {
rotationId?: string; rotationId?: string;
isPending?: boolean; isPending?: boolean;
pendingAction?: PendingAction; pendingAction?: PendingAction;
reminder?: Reminder;
}; };
export type SecretV3Raw = { export type SecretV3Raw = {
@@ -94,6 +96,7 @@ export type SecretV3Raw = {
isRotatedSecret?: boolean; isRotatedSecret?: boolean;
rotationId?: string; rotationId?: string;
secretReminderRecipients?: SecretReminderRecipient[]; secretReminderRecipients?: SecretReminderRecipient[];
reminder?: Reminder;
}; };
export type SecretV3RawResponse = { 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 { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons"; import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
import { import {
faArrowRotateRight, faArrowRotateRight,
faCheckCircle, faCheckCircle,
faClock,
faCopy, faCopy,
faDesktop, faDesktop,
faEyeSlash, faEyeSlash,
faPlus, faPlus,
faProjectDiagram,
faSearch,
faServer, faServer,
faShare, faShare,
faTag, faTag,
@@ -26,6 +27,7 @@ import { twMerge } from "tailwind-merge";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { createNotification } from "@app/components/notifications"; import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions"; import { ProjectPermissionCan } from "@app/components/permissions";
import { hasSecretReference, SecretReferenceTree } from "@app/components/secrets/SecretReferenceDetails";
import { import {
Button, Button,
Drawer, Drawer,
@@ -39,30 +41,26 @@ import {
FormLabel, FormLabel,
IconButton, IconButton,
Input, Input,
Modal,
ModalContent,
Switch, Switch,
Tag, Tag,
TextArea, TextArea,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission, useWorkspace } from "@app/context";
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { getProjectBaseURL } from "@app/helpers/project"; import { getProjectBaseURL } from "@app/helpers/project";
import { usePopUp, useToggle } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api"; import { useGetSecretVersion } from "@app/hooks/api";
import { ActorType } from "@app/hooks/api/auditLogs/enums"; import { ActorType } from "@app/hooks/api/auditLogs/enums";
import { useGetReminder } from "@app/hooks/api/reminders";
import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries"; import { useGetSecretAccessList } from "@app/hooks/api/secrets/queries";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types"; import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { camelCaseToSpaces } from "@app/lib/fn/string"; import { camelCaseToSpaces } from "@app/lib/fn/string";
import { CreateReminderForm } from "./CreateReminderForm";
import { HIDDEN_SECRET_VALUE } from "./SecretItem"; import { HIDDEN_SECRET_VALUE } from "./SecretItem";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils"; import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
@@ -102,7 +100,7 @@ export const SecretDetailSidebar = ({
handleSubmit, handleSubmit,
setValue, setValue,
reset, reset,
formState: { isDirty, isSubmitting } formState: { isDirty }
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
values: secret, values: secret,
@@ -110,12 +108,12 @@ export const SecretDetailSidebar = ({
}); });
const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([ const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp([
"secretAccessUpgradePlan" "secretAccessUpgradePlan",
"secretReferenceTree"
] as const); ] as const);
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { data: reminderData } = useGetReminder(secret?.id);
const tagFields = useFieldArray({ const tagFields = useFieldArray({
control, control,
@@ -188,25 +186,6 @@ export const SecretDetailSidebar = ({
secretKey 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) => { const handleTagSelect = (tag: WsTag) => {
if (selectedTagsGroupById?.[tag.id]) { if (selectedTagsGroupById?.[tag.id]) {
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id); const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
@@ -222,8 +201,6 @@ export const SecretDetailSidebar = ({
await onSaveSecret(secret, { ...secret, ...data }, () => reset()); await onSaveSecret(secret, { ...secret, ...data }, () => reset());
}; };
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
useEffect(() => { useEffect(() => {
setValue( setValue(
"reminderRecipients", "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 ( return (
<> <>
<CreateReminderForm
isOpen={createReminderFormOpen}
onOpenChange={() => {
setCreateReminderFormOpen.toggle();
}}
workspaceId={currentWorkspace.id}
environment={environment}
secretPath={secretPath}
secretId={secret?.id}
reminder={reminderData}
/>
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.secretAccessUpgradePlan.isOpen} isOpen={popUp.secretAccessUpgradePlan.isOpen}
onOpenChange={(isUpgradeModalOpen) => onOpenChange={(isUpgradeModalOpen) =>
@@ -324,28 +275,45 @@ export const SecretDetailSidebar = ({
} }
text="Secret access analysis is only available on Infisical's Pro plan and above." 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 <Drawer
onOpenChange={(state) => { onOpenChange={async (state) => {
if (isOpen && isDirty) { if (isOpen && isDirty) {
if ( await handleSubmit(handleFormSubmit)();
// 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;
} }
onToggle(state); onToggle(state);
}} }}
isOpen={isOpen} isOpen={isOpen}
> >
<DrawerContent title={`Secret – ${secret?.key}`} className="thin-scrollbar"> <DrawerContent
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full"> title={`Secret – ${secret?.key}`}
<div className="flex h-full flex-col"> className="thin-scrollbar h-full"
<div className="flex flex-row"> cardBodyClassName="pb-0"
<div className="w-full"> >
<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 <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { a={subject(ProjectPermissionSub.Secrets, {
@@ -361,9 +329,8 @@ export const SecretDetailSidebar = ({
key="secret-value" key="secret-value"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<div className="flex items-center gap-2">
<FormControl <FormControl
className="flex-1" className="mb-0"
helperText={ helperText={
cannotReadSecretValue ? ( cannotReadSecretValue ? (
<div className="flex space-x-2"> <div className="flex space-x-2">
@@ -372,22 +339,22 @@ export const SecretDetailSidebar = ({
className="mt-0.5 text-yellow-400" className="mt-0.5 text-yellow-400"
/> />
<span> <span>
The value of this secret is hidden because you do not have the The value of this secret is hidden because you do not have the read
read secret value permission. secret value permission.
</span> </span>
</div> </div>
) : undefined ) : undefined
} }
label="Value" label="Value"
> >
<div className="flex items-center gap-2"> <div className="flex items-start gap-x-2">
<InfisicalSecretInput <InfisicalSecretInput
isReadOnly={isReadOnly || !isAllowed || secret?.isRotatedSecret} isReadOnly={isReadOnly || !isAllowed || secret?.isRotatedSecret}
environment={environment} environment={environment}
secretPath={secretPath} secretPath={secretPath}
key="secret-value" key="secret-value"
isDisabled={isOverridden} 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} {...field}
autoFocus={false} autoFocus={false}
/> />
@@ -397,9 +364,7 @@ export const SecretDetailSidebar = ({
? "This project does not allow secret sharing." ? "This project does not allow secret sharing."
: "You don't have permission to view the secret value." : "You don't have permission to view the secret value."
} }
isDisabled={ isDisabled={!secret?.secretValueHidden && currentWorkspace.secretSharing}
!secret?.secretValueHidden && currentWorkspace.secretSharing
}
> >
<Button <Button
isDisabled={ isDisabled={
@@ -420,15 +385,11 @@ export const SecretDetailSidebar = ({
</Tooltip> </Tooltip>
</div> </div>
</FormControl> </FormControl>
</div>
)} )}
/> />
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> <div className="rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4">
</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">
<Controller <Controller
control={control} control={control}
name="skipMultilineEncoding" name="skipMultilineEncoding"
@@ -467,58 +428,7 @@ export const SecretDetailSidebar = ({
)} )}
/> />
</div> </div>
<div <div className="flex flex-col rounded-md border border-mineshaft-600 bg-mineshaft-900 p-4 px-0 pb-0">
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 <div
className={`flex justify-between px-4 text-mineshaft-100 ${tagFields.fields.length > 0 ? "flex-col" : "flex-row"}`} 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> <div>
<FormControl> <FormControl>
<div <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 }) => ( {tagFields.fields.map(({ tagColor, id: formId, slug }) => (
<Tag <Tag
@@ -631,7 +541,6 @@ export const SecretDetailSidebar = ({
<div <div
className={`mb-4 w-full border-t border-mineshaft-600 ${tagFields.fields.length > 0 || metadataFormFields.fields.length > 0 ? "block" : "hidden"}`} className={`mb-4 w-full border-t border-mineshaft-600 ${tagFields.fields.length > 0 || metadataFormFields.fields.length > 0 ? "block" : "hidden"}`}
/> />
<div <div
className={`flex justify-between px-4 text-mineshaft-100 ${metadataFormFields.fields.length > 0 ? "flex-col" : "flex-row"}`} 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" label="Comments & Notes"
isError={Boolean(error?.message)} isError={Boolean(error?.message)}
errorText={error?.message} errorText={error?.message}
className="mb-0" className="mb-0 h-[125px] pb-0"
> >
<TextArea <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} readOnly={isReadOnly}
rows={5} placeholder="add a comment or note to this secret..."
rows={4}
{...field} {...field}
/> />
</FormControl> </FormControl>
)} )}
/> />
<FormControl> <div className="dark flex max-h-[24rem] flex-1 cursor-default flex-col text-sm text-bunker-300">
{reminderData && reminderData.nextReminderDate ? ( <div className="mb-0.5 text-mineshaft-400">Version History</div>
<div className="flex items-center justify-between px-2"> <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]">
<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]">
{secretVersion?.map( {secretVersion?.map(
({ createdAt, secretValue, secretValueHidden, version, id, actor }) => ( ({ createdAt, secretValue, secretValueHidden, version, id, actor }) => (
<div className="flex flex-row" key={id}> <div className="flex flex-row" key={id}>
@@ -781,9 +660,7 @@ export const SecretDetailSidebar = ({
<div className="flex flex-row"> <div className="flex flex-row">
<div className="flex w-fit flex-row text-sm"> <div className="flex w-fit flex-row text-sm">
Modified by: Modified by:
<Tooltip <Tooltip content={getModifiedByName(actor.actorType, actor.name)}>
content={getModifiedByName(actor.actorType, actor.name)}
>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div <div
onClick={() => onClick={() =>
@@ -925,9 +802,7 @@ export const SecretDetailSidebar = ({
variant="outline_bg" variant="outline_bg"
size="sm" size="sm"
className="h-8 w-8 rounded-md" className="h-8 w-8 rounded-md"
onClick={() => onClick={() => setValue("value", secretValue, { shouldDirty: true })}
setValue("value", secretValue, { shouldDirty: true })
}
> >
<FontAwesomeIcon icon={faArrowRotateRight} /> <FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton> </IconButton>
@@ -939,8 +814,8 @@ export const SecretDetailSidebar = ({
)} )}
</div> </div>
</div> </div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300"> <div className="dark flex flex-col text-sm text-bunker-300">
<div className="mb-2 mt-4"> <div className="mb-0.5 text-mineshaft-400">
Access List Access List
<Tooltip <Tooltip
content="Lists all users, machine identities, and groups that have been granted any permission level (read, create, edit, or delete) for this secret." 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" /> <FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip> </Tooltip>
</div> </div>
{isPending && ( {secretAccessList ? (
<Button className="w-full px-2 py-1" variant="outline_bg" isDisabled> <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]">
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.users.length > 0 && ( {secretAccessList.users.length > 0 && (
<div className="pb-3"> <div className="pb-3">
<div className="mb-2 font-bold">Users</div> <div className="mb-2 font-bold">Users</div>
@@ -974,6 +833,7 @@ export const SecretDetailSidebar = ({
{secretAccessList.users.map((user) => ( {secretAccessList.users.map((user) => (
<div className="rounded-md bg-bunker-500"> <div className="rounded-md bg-bunker-500">
<Tooltip <Tooltip
side="left"
content={user.allowedActions content={user.allowedActions
.map((action) => camelCaseToSpaces(action)) .map((action) => camelCaseToSpaces(action))
.join(", ")} .join(", ")}
@@ -987,7 +847,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id, projectId: currentWorkspace.id,
membershipId: user.membershipId 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} {user.name}
</Link> </Link>
@@ -1004,6 +864,7 @@ export const SecretDetailSidebar = ({
{secretAccessList.identities.map((identity) => ( {secretAccessList.identities.map((identity) => (
<div className="rounded-md bg-bunker-500"> <div className="rounded-md bg-bunker-500">
<Tooltip <Tooltip
side="left"
content={identity.allowedActions content={identity.allowedActions
.map( .map(
(action) => (action) =>
@@ -1020,7 +881,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id, projectId: currentWorkspace.id,
identityId: identity.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} {identity.name}
</Link> </Link>
@@ -1037,6 +898,7 @@ export const SecretDetailSidebar = ({
{secretAccessList.groups.map((group) => ( {secretAccessList.groups.map((group) => (
<div className="rounded-md bg-bunker-500"> <div className="rounded-md bg-bunker-500">
<Tooltip <Tooltip
side="left"
content={group.allowedActions content={group.allowedActions
.map( .map(
(action) => (action) =>
@@ -1050,7 +912,7 @@ export const SecretDetailSidebar = ({
params={{ params={{
groupId: group.id 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} {group.name}
</Link> </Link>
@@ -1061,33 +923,41 @@ export const SecretDetailSidebar = ({
</div> </div>
)} )}
</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 <Button
isFullWidth className="w-full"
type="submit" leftIcon={<FontAwesomeIcon icon={faSearch} />}
variant="outline_bg" variant="outline_bg"
isDisabled={isSubmitting || !isDirty || !isAllowed} isDisabled={isPending}
isLoading={isSubmitting} isLoading={isPending}
onClick={() => handlePopUpOpen("secretAccessUpgradePlan")}
> >
Apply Changes Analyze Access
</Button> </Button>
)} )}
</ProjectPermissionCan> </div>
<div className="flex items-center gap-2"> <div className="mt-auto flex items-center space-x-2 pb-4">
<Tooltip content="Copy Secret ID"> <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 <IconButton
variant="outline_bg" variant="outline_bg"
ariaLabel="Copy Secret ID" ariaLabel="Copy Secret ID"
@@ -1114,12 +984,12 @@ export const SecretDetailSidebar = ({
})} })}
> >
{(isAllowed) => ( {(isAllowed) => (
<Tooltip content="Delete Secret"> <Tooltip content="Delete Secret" align="end" className="z-[100]">
<IconButton <IconButton
colorSchema="danger" colorSchema="danger"
variant="outline_bg" variant="outline_bg"
ariaLabel="Delete Secret" 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} isDisabled={!isAllowed}
onClick={onDeleteSecret} onClick={onDeleteSecret}
> >
@@ -1129,9 +999,6 @@ export const SecretDetailSidebar = ({
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>
</div>
</div>
</div>
</form> </form>
</DrawerContent> </DrawerContent>
</Drawer> </Drawer>
@@ -12,11 +12,9 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
FontAwesomeSymbol, FontAwesomeSymbol,
FormControl, FormControl,
GenericFieldLabel,
IconButton, IconButton,
Input, Input,
Modal,
ModalContent,
ModalTrigger,
Popover, Popover,
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
@@ -41,16 +39,14 @@ import { AnimatePresence, motion } from "framer-motion";
import { memo, useCallback, useEffect, useRef } from "react"; import { memo, useCallback, useEffect, useRef } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form"; import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import {
hasSecretReference,
SecretReferenceTree
} from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons"; import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
import { PendingAction } from "@app/hooks/api/secretFolders/types"; 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 { import {
FontAwesomeSpriteName, FontAwesomeSpriteName,
formSchema, formSchema,
@@ -113,7 +109,8 @@ export const SecretItem = memo(
colWidth colWidth
}: Props) => { }: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"editSecret" "editSecret",
"reminder"
] as const); ] as const);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
@@ -186,7 +183,7 @@ export const SecretItem = memo(
name: "tags" name: "tags"
}); });
const isOverriden = const isOverridden =
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified; overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
const hasTagsApplied = Boolean(fields.length); const hasTagsApplied = Boolean(fields.length);
@@ -272,7 +269,7 @@ export const SecretItem = memo(
}, [isSecValueCopied]); }, [isSecValueCopied]);
const handleOverrideClick = () => { const handleOverrideClick = () => {
if (isOverriden) { if (isOverridden) {
// override need not be flagged delete if it was never saved in server // override need not be flagged delete if it was never saved in server
// meaning a new unsaved personal secret but user toggled back later // meaning a new unsaved personal secret but user toggled back later
const isUnsavedOverride = !secret.idOverride; const isUnsavedOverride = !secret.idOverride;
@@ -327,7 +324,7 @@ export const SecretItem = memo(
const copyTokenToClipboard = () => { const copyTokenToClipboard = () => {
const [overrideValue, value] = getValues(["value", "valueOverride"]); const [overrideValue, value] = getValues(["value", "valueOverride"]);
if (isOverriden) { if (isOverridden) {
navigator.clipboard.writeText(value as string); navigator.clipboard.writeText(value as string);
} else { } else {
navigator.clipboard.writeText(overrideValue as string); navigator.clipboard.writeText(overrideValue as string);
@@ -335,7 +332,7 @@ export const SecretItem = memo(
setIsSecValueCopied.on(); setIsSecValueCopied.on();
}; };
const isInAutoSaveMode = isDirty && !isSubmitting && !isOverriden; const isInAutoSaveMode = isDirty && !isSubmitting && !isOverridden;
return ( return (
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
@@ -398,7 +395,7 @@ export const SecretItem = memo(
isReadOnly={isReadOnly || isRotatedSecret} isReadOnly={isReadOnly || isRotatedSecret}
autoCapitalization={currentWorkspace?.autoCapitalization} autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain" variant="plain"
isDisabled={isOverriden} isDisabled={isOverridden}
placeholder={error?.message} placeholder={error?.message}
isError={Boolean(error)} isError={Boolean(error)}
onKeyUp={() => trigger("key")} onKeyUp={() => trigger("key")}
@@ -413,14 +410,14 @@ export const SecretItem = memo(
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
{secretValueHidden && !isOverriden && !isPending && ( {secretValueHidden && !isOverridden && !isPending && (
<Tooltip <Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`} 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} /> <FontAwesomeIcon className="pr-2" size="sm" icon={faEyeSlash} />
</Tooltip> </Tooltip>
)} )}
{isOverriden ? ( {isOverridden ? (
<Controller <Controller
name="valueOverride" name="valueOverride"
key="value-overriden" key="value-overriden"
@@ -445,7 +442,7 @@ export const SecretItem = memo(
isReadOnly={isReadOnlySecret} isReadOnly={isReadOnlySecret}
key="secret-value" key="secret-value"
isVisible={isVisible && (!secretValueHidden || isPending)} isVisible={isVisible && (!secretValueHidden || isPending)}
canEditButNotView={secretValueHidden && !isOverriden && !isPending} canEditButNotView={secretValueHidden && !isOverridden && !isPending}
environment={environment} environment={environment}
secretPath={secretPath} secretPath={secretPath}
{...field} {...field}
@@ -462,7 +459,6 @@ export const SecretItem = memo(
key="actions" key="actions"
className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2" className="flex h-full flex-shrink-0 self-start transition-all group-hover:gap-x-2"
> >
<Tooltip content="Copy secret">
<IconButton <IconButton
isDisabled={secret.secretValueHidden} isDisabled={secret.secretValueHidden}
ariaLabel="copy-value" ariaLabel="copy-value"
@@ -471,6 +467,7 @@ export const SecretItem = memo(
className="w-0 overflow-hidden p-0 group-hover:w-5" className="w-0 overflow-hidden p-0 group-hover:w-5"
onClick={copyTokenToClipboard} onClick={copyTokenToClipboard}
> >
<Tooltip content="Copy secret">
<FontAwesomeSymbol <FontAwesomeSymbol
className="h-3.5 w-3" className="h-3.5 w-3"
symbolName={ symbolName={
@@ -479,8 +476,8 @@ export const SecretItem = memo(
: FontAwesomeSpriteName.ClipboardCopy : FontAwesomeSpriteName.ClipboardCopy
} }
/> />
</IconButton>
</Tooltip> </Tooltip>
</IconButton>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { a={subject(ProjectPermissionSub.Secrets, {
@@ -491,41 +488,47 @@ export const SecretItem = memo(
})} })}
> >
{(isAllowed) => ( {(isAllowed) => (
<Modal>
<ModalTrigger asChild>
<IconButton <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" variant="plain"
size="md" size="md"
ariaLabel="reference-tree" ariaLabel="Secret reminder"
isDisabled={!isAllowed || !hasSecretReference(secret?.value)} isDisabled={!isAllowed || isOverridden}
> >
<Tooltip <Tooltip
className="max-w-2xl"
content={ content={
hasSecretReference(secret?.value) isOverridden ? (
? "Secret Reference Tree" "Unavailable with override"
: "Secret does not contain references" ) : 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 <FontAwesomeSymbol
className="h-3.5 w-3.5" className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.SecretReferenceTree} symbolName={FontAwesomeSpriteName.Reminder}
/> />
</Tooltip> </Tooltip>
</IconButton> </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> </ProjectPermissionCan>
<DropdownMenu> <DropdownMenu>
@@ -539,7 +542,7 @@ export const SecretItem = memo(
})} })}
> >
{(isAllowed) => ( {(isAllowed) => (
<DropdownMenuTrigger asChild disabled={!isAllowed}> <DropdownMenuTrigger asChild disabled={!isAllowed || isOverridden}>
<IconButton <IconButton
ariaLabel="tags" ariaLabel="tags"
variant="plain" 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", "w-0 overflow-hidden p-0 group-hover:w-5 data-[state=open]:w-5",
hasTagsApplied && "w-5 text-primary" hasTagsApplied && "w-5 text-primary"
)} )}
isDisabled={!isAllowed} isDisabled={!isAllowed || isOverridden}
> >
<Tooltip content="Tags"> <Tooltip content={isOverridden ? "Unavailable with override" : "Tags"}>
<FontAwesomeSymbol <FontAwesomeSymbol
className="h-3.5 w-3.5" className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Tags} symbolName={FontAwesomeSpriteName.Tags}
@@ -617,8 +620,6 @@ export const SecretItem = memo(
secretName, secretName,
secretTags: selectedTagSlugs secretTags: selectedTagSlugs
})} })}
renderTooltip
allowedLabel="Override"
> >
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
@@ -629,13 +630,15 @@ export const SecretItem = memo(
onClick={handleOverrideClick} onClick={handleOverrideClick}
className={twMerge( className={twMerge(
"w-0 overflow-hidden p-0 group-hover:w-5", "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 <FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Override} symbolName={FontAwesomeSpriteName.Override}
className="h-3.5 w-3.5" className="h-3.5 w-3.5"
/> />
</Tooltip>
</IconButton> </IconButton>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
@@ -650,7 +653,7 @@ export const SecretItem = memo(
})} })}
> >
{(isAllowed) => ( {(isAllowed) => (
<PopoverTrigger asChild disabled={!isAllowed}> <PopoverTrigger asChild disabled={!isAllowed || isOverridden}>
<IconButton <IconButton
className={twMerge( className={twMerge(
"w-0 overflow-hidden p-0 group-hover:w-5", "w-0 overflow-hidden p-0 group-hover:w-5",
@@ -659,9 +662,11 @@ export const SecretItem = memo(
variant="plain" variant="plain"
size="md" size="md"
ariaLabel="add-comment" ariaLabel="add-comment"
isDisabled={!isAllowed} isDisabled={!isAllowed || isOverridden}
>
<Tooltip
content={isOverridden ? "Unavailable with override" : "Comment"}
> >
<Tooltip content="Comment">
<FontAwesomeSymbol <FontAwesomeSymbol
className="h-3.5 w-3.5" className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Comment} symbolName={FontAwesomeSpriteName.Comment}
@@ -851,12 +856,13 @@ export const SecretItem = memo(
animate={{ x: 0, opacity: 1 }} animate={{ x: 0, opacity: 1 }}
exit={{ x: 10, opacity: 0 }} exit={{ x: 10, opacity: 0 }}
> >
<Tooltip content="More"> <Tooltip content={isOverridden ? "Unavailable with override" : "More"}>
<IconButton <IconButton
ariaLabel="more" ariaLabel="more"
variant="plain" variant="plain"
size="md" size="md"
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100" className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
isDisabled={isOverridden}
onClick={() => onDetailViewSecret(secret)} onClick={() => onDetailViewSecret(secret)}
> >
<FontAwesomeSymbol <FontAwesomeSymbol
@@ -874,7 +880,13 @@ export const SecretItem = memo(
secretTags: selectedTagSlugs secretTags: selectedTagSlugs
})} })}
renderTooltip renderTooltip
allowedLabel={isRotatedSecret ? "Cannot Delete Rotated Secret" : "Delete"} allowedLabel={
isOverridden
? "Unavailable with override"
: isRotatedSecret
? "Cannot Delete Rotated Secret"
: "Delete"
}
> >
{(isAllowed) => ( {(isAllowed) => (
<IconButton <IconButton
@@ -884,7 +896,7 @@ export const SecretItem = memo(
size="md" size="md"
className="p-0 opacity-0 group-hover:opacity-100" className="p-0 opacity-0 group-hover:opacity-100"
onClick={() => onDeleteSecret(secret)} onClick={() => onDeleteSecret(secret)}
isDisabled={!isAllowed || isRotatedSecret} isDisabled={!isAllowed || isRotatedSecret || isOverridden}
> >
<FontAwesomeSymbol <FontAwesomeSymbol
symbolName={FontAwesomeSpriteName.Trash} symbolName={FontAwesomeSpriteName.Trash}
@@ -959,6 +971,15 @@ export const SecretItem = memo(
</AnimatePresence> </AnimatePresence>
</div> </div>
</div> </div>
<CreateReminderForm
isOpen={popUp.reminder.isOpen}
onOpenChange={() => handlePopUpToggle("reminder")}
workspaceId={currentWorkspace.id}
environment={environment}
secretPath={secretPath}
secretId={secret?.id}
reminder={secret.reminder}
/>
<DeleteActionModal <DeleteActionModal
isOpen={popUp.editSecret.isOpen} isOpen={popUp.editSecret.isOpen}
deleteKey="confirm" deleteKey="confirm"
@@ -1,6 +1,7 @@
/* eslint-disable no-nested-ternary */ /* eslint-disable no-nested-ternary */
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons"; import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faBell,
faCheck, faCheck,
faClock, faClock,
faClone, faClone,
@@ -11,7 +12,6 @@ import {
faEllipsis, faEllipsis,
faKey, faKey,
faLock, faLock,
faProjectDiagram,
faShare, faShare,
faTags, faTags,
faTrash faTrash
@@ -83,7 +83,7 @@ export enum FontAwesomeSpriteName {
ReplicatedSecretKey = "secret-replicated", ReplicatedSecretKey = "secret-replicated",
ShareSecret = "share-secret", ShareSecret = "share-secret",
KeyLock = "key-lock", KeyLock = "key-lock",
SecretReferenceTree = "secret-reference-tree", Reminder = "secret-reminder",
Trash = "trash" Trash = "trash"
} }
@@ -103,6 +103,6 @@ export const FontAwesomeSpriteSymbols = [
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey }, { icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret }, { icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock }, { icon: faLock, symbol: FontAwesomeSpriteName.KeyLock },
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree }, { icon: faBell, symbol: FontAwesomeSpriteName.Reminder },
{ icon: faTrash, symbol: FontAwesomeSpriteName.Trash } { icon: faTrash, symbol: FontAwesomeSpriteName.Trash }
]; ];