Merge pull request #3142 from Infisical/sidebar-update

improve sidebars
This commit is contained in:
Daniel Hougaard
2025-02-25 13:20:53 +04:00
committed by GitHub
6 changed files with 613 additions and 376 deletions
@@ -20,7 +20,7 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
return ( return (
<DropdownMenuPrimitive.Portal> <DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content <DropdownMenuPrimitive.Content
sideOffset={10} sideOffset={-8}
{...props} {...props}
ref={forwardedRef} ref={forwardedRef}
className={twMerge( className={twMerge(
@@ -20,12 +20,13 @@ export const MenuIconButton = <T extends ElementType = "button">({
ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => { ComponentPropsWithRef<T> & { lottieIconMode?: "reverse" | "forward" }): JSX.Element => {
const iconRef = useRef<DotLottie | null>(null); const iconRef = useRef<DotLottie | null>(null);
return ( return (
<div className={!isSelected ? "hover:px-1" : ""}>
<Item <Item
type="button" type="button"
role="menuitem" role="menuitem"
className={twMerge( className={twMerge(
"group relative flex w-full cursor-pointer flex-col items-center justify-center rounded p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700", "group relative flex w-full cursor-pointer flex-col items-center justify-center rounded my-1 p-2 font-inter text-sm text-bunker-100 transition-all duration-150 hover:bg-mineshaft-700",
isSelected && "bg-bunker-800 hover:bg-mineshaft-600", isSelected && "bg-bunker-800 hover:bg-mineshaft-600 rounded-none",
isDisabled && "cursor-not-allowed hover:bg-transparent", isDisabled && "cursor-not-allowed hover:bg-transparent",
className className
)} )}
@@ -37,7 +38,7 @@ export const MenuIconButton = <T extends ElementType = "button">({
<div <div
className={`${ className={`${
isSelected ? "opacity-100" : "opacity-0" isSelected ? "opacity-100" : "opacity-0"
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`} } absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
/> />
{icon && ( {icon && (
<div className="my-auto mb-2 h-6 w-6"> <div className="my-auto mb-2 h-6 w-6">
@@ -59,5 +60,6 @@ export const MenuIconButton = <T extends ElementType = "button">({
{children} {children}
</div> </div>
</Item> </Item>
</div>
); );
}; };
@@ -84,6 +84,10 @@ export const MinimizedOrgSidebar = () => {
const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL);
const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {});
const { subscription } = useSubscription(); const { subscription } = useSubscription();
const [open, setOpen] = useState(false);
const [openSupport, setOpenSupport] = useState(false);
const [openUser, setOpenUser] = useState(false);
const [openOrg, setOpenOrg] = useState(false);
const { user } = useUser(); const { user } = useUser();
const { mutateAsync } = useGetOrgTrialUrl(); const { mutateAsync } = useGetOrgTrialUrl();
@@ -160,23 +164,29 @@ export const MinimizedOrgSidebar = () => {
> >
<nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]"> <nav className="items-between flex h-full flex-col justify-between overflow-y-auto dark:[color-scheme:dark]">
<div> <div>
<div className="flex cursor-pointer items-center p-2 pt-4 hover:bg-mineshaft-700"> <div className="flex items-center hover:bg-mineshaft-700">
<DropdownMenu modal> <DropdownMenu open={openOrg} onOpenChange={setOpenOrg} modal>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-1 transition-all"> onMouseEnter={() => setOpenOrg(true)}
onMouseLeave={() => setOpenOrg(false)}
asChild
>
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-3 pb-5 pt-6 transition-all">
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary"> <div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary">
{currentOrg?.name.charAt(0)} {currentOrg?.name.charAt(0)}
</div> </div>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent <DropdownMenuContent
onMouseEnter={() => setOpenOrg(true)}
onMouseLeave={() => setOpenOrg(false)}
align="start" align="start"
side="right" side="right"
className="p-1 shadow-mineshaft-600 drop-shadow-md" className="mt-6 cursor-default p-1 shadow-mineshaft-600 drop-shadow-md"
style={{ minWidth: "320px" }} style={{ minWidth: "220px" }}
> >
<div className="px-2 py-1"> <div className="px-0.5 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700"> <div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/5 to-mineshaft-800 p-1 transition-all duration-300">
<div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black"> <div className="mr-2 flex h-8 w-8 items-center justify-center rounded-md bg-primary text-black">
{currentOrg?.name.charAt(0)} {currentOrg?.name.charAt(0)}
</div> </div>
@@ -241,7 +251,7 @@ export const MinimizedOrgSidebar = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
<div className="space-y-1 px-1"> <div className="space-y-1">
<Link to="/organization/secret-manager/overview"> <Link to="/organization/secret-manager/overview">
{({ isActive }) => ( {({ isActive }) => (
<MenuIconButton <MenuIconButton
@@ -251,7 +261,7 @@ export const MinimizedOrgSidebar = () => {
} }
icon="sliding-carousel" icon="sliding-carousel"
> >
Secret Manager Secrets
</MenuIconButton> </MenuIconButton>
)} )}
</Link> </Link>
@@ -264,7 +274,7 @@ export const MinimizedOrgSidebar = () => {
} }
icon="note" icon="note"
> >
Cert Manager PKI
</MenuIconButton> </MenuIconButton>
)} )}
</Link> </Link>
@@ -296,31 +306,41 @@ export const MinimizedOrgSidebar = () => {
<Link to="/organization/secret-scanning"> <Link to="/organization/secret-scanning">
{({ isActive }) => ( {({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="secret-scan"> <MenuIconButton isSelected={isActive} icon="secret-scan">
Secret Scanning Scanner
</MenuIconButton> </MenuIconButton>
)} )}
</Link> </Link>
<Link to="/organization/secret-sharing"> <Link to="/organization/secret-sharing">
{({ isActive }) => ( {({ isActive }) => (
<MenuIconButton isSelected={isActive} icon="lock-closed"> <MenuIconButton isSelected={isActive} icon="lock-closed">
Secret Sharing Share
</MenuIconButton> </MenuIconButton>
)} )}
</Link> </Link>
<div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} /> <div className="my-1 w-full bg-mineshaft-500" style={{ height: "1px" }} />
<DropdownMenu> <DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
asChild
>
<div className="w-full"> <div className="w-full">
<MenuIconButton <MenuIconButton
lottieIconMode="reverse" lottieIconMode="reverse"
icon="settings-cog" icon="settings-cog"
isSelected={isMoreSelected} isSelected={isMoreSelected}
> >
Org Controls Admin
</MenuIconButton> </MenuIconButton>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" side="right" className="p-1"> <DropdownMenuContent
onMouseEnter={() => setOpen(true)}
onMouseLeave={() => setOpen(false)}
align="start"
side="right"
className="p-1"
>
<DropdownMenuLabel>Organization Options</DropdownMenuLabel> <DropdownMenuLabel>Organization Options</DropdownMenuLabel>
<Link to="/organization/access-management"> <Link to="/organization/access-management">
<DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}> <DropdownMenuItem icon={<FontAwesomeIcon className="w-3" icon={faUsers} />}>
@@ -364,14 +384,24 @@ export const MinimizedOrgSidebar = () => {
: "mb-4" : "mb-4"
} flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`} } flex w-full cursor-default flex-col items-center px-1 text-sm text-mineshaft-400`}
> >
<DropdownMenu> <DropdownMenu open={openSupport} onOpenChange={setOpenSupport}>
<DropdownMenuTrigger className="w-full"> <DropdownMenuTrigger
onMouseEnter={() => setOpenSupport(true)}
onMouseLeave={() => setOpenSupport(false)}
className="w-full"
>
<MenuIconButton> <MenuIconButton>
<FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" /> <FontAwesomeIcon icon={faInfoCircle} className="mb-3 text-lg" />
Support Support
</MenuIconButton> </MenuIconButton>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent
onMouseEnter={() => setOpenSupport(true)}
onMouseLeave={() => setOpenSupport(false)}
align="end"
side="right"
className="p-1"
>
{INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => ( {INFISICAL_SUPPORT_OPTIONS.map(([icon, text, url]) => (
<DropdownMenuItem key={url as string}> <DropdownMenuItem key={url as string}>
<a <a
@@ -419,17 +449,28 @@ export const MinimizedOrgSidebar = () => {
</button> </button>
</Tooltip> </Tooltip>
)} )}
<DropdownMenu> <DropdownMenu open={openUser} onOpenChange={setOpenUser}>
<DropdownMenuTrigger className="w-full" asChild> <DropdownMenuTrigger
onMouseEnter={() => setOpenUser(true)}
onMouseLeave={() => setOpenUser(false)}
className="w-full"
asChild
>
<div> <div>
<MenuIconButton icon="user">User</MenuIconButton> <MenuIconButton icon="user">User</MenuIconButton>
</div> </div>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="start" className="p-1"> <DropdownMenuContent
<div className="px-2 py-1"> onMouseEnter={() => setOpenUser(true)}
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 p-1 transition-all duration-150 hover:bg-mineshaft-700"> onMouseLeave={() => setOpenUser(false)}
<div className="p-2"> side="right"
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" /> align="end"
className="p-1"
>
<div className="cursor-default px-1 py-1">
<div className="flex w-full items-center justify-center rounded-md border border-mineshaft-600 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800 p-1 px-2 transition-all duration-150">
<div className="p-1 pr-3">
<FontAwesomeIcon icon={faUser} className="text-xl text-mineshaft-400" />
</div> </div>
<div className="flex flex-grow flex-col text-white"> <div className="flex flex-grow flex-col text-white">
<div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize"> <div className="max-w-36 truncate text-ellipsis text-sm font-medium capitalize">
@@ -477,11 +518,11 @@ export const MinimizedOrgSidebar = () => {
</DropdownMenuItem> </DropdownMenuItem>
</Link> </Link>
)} )}
<div className="mt-1 border-t border-mineshaft-600 pt-1">
<Link to="/organization/admin"> <Link to="/organization/admin">
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600"> <DropdownMenuItem>Organization Admin Console</DropdownMenuItem>
Organization Admin Console
</DropdownMenuItem>
</Link> </Link>
</div>
<div className="mt-1 h-1 border-t border-mineshaft-600" /> <div className="mt-1 h-1 border-t border-mineshaft-600" />
<DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}> <DropdownMenuItem onClick={logOutUser} icon={<FontAwesomeIcon icon={faSignOut} />}>
Log Out Log Out
@@ -120,7 +120,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
isError={Boolean(error)} isError={Boolean(error)}
errorText={error?.message} errorText={error?.message}
> >
<Input {...field} placeholder="API Key" type="text" /> <Input {...field} placeholder="API Key" type="text" autoComplete="off" autoCorrect="off" spellCheck="false" />
</FormControl> </FormControl>
)} )}
/> />
@@ -155,7 +155,7 @@ export const ShareSecretForm = ({ isPublic, value }: Props) => {
errorText={error?.message} errorText={error?.message}
isOptional isOptional
> >
<Input {...field} placeholder="Password" type="password" /> <Input {...field} placeholder="Password" type="password" autoComplete="new-password" autoCorrect="off" spellCheck="false" aria-autocomplete="none" data-form-type="other" />
</FormControl> </FormControl>
)} )}
/> />
@@ -59,7 +59,12 @@ export const PitDrawer = ({
onClick={() => onSelectSnapshot(id)} onClick={() => onSelectSnapshot(id)}
> >
<div className="flex w-full justify-between"> <div className="flex w-full justify-between">
<div>{formatDistance(new Date(createdAt), new Date())}</div> <div>
{(() => {
const distance = formatDistance(new Date(createdAt), new Date());
return distance.charAt(0).toUpperCase() + distance.slice(1) + " ago";
})()}
</div>
<div>{getButtonLabel(i === 0 && index === 0, snapshotId === id)}</div> <div>{getButtonLabel(i === 0 && index === 0, snapshotId === id)}</div>
</div> </div>
</Button> </Button>
@@ -70,7 +75,7 @@ export const PitDrawer = ({
<Button <Button
className="mt-8 px-4 py-3 text-sm" className="mt-8 px-4 py-3 text-sm"
isFullWidth isFullWidth
variant="star" variant="outline_bg"
isLoading={isFetchingNextPage} isLoading={isFetchingNextPage}
isDisabled={isFetchingNextPage || !hasNextPage} isDisabled={isFetchingNextPage || !hasNextPage}
onClick={fetchNextPage} onClick={fetchNextPage}
@@ -1,11 +1,11 @@
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 } from "@fortawesome/free-regular-svg-icons"; import { faCircleQuestion, faEye } from "@fortawesome/free-regular-svg-icons";
import { import {
faArrowRotateRight,
faCheckCircle, faCheckCircle,
faCircle,
faCircleDot,
faClock, faClock,
faEyeSlash,
faPlus, faPlus,
faShare, faShare,
faTag, faTag,
@@ -236,12 +236,11 @@ export const SecretDetailSidebar = ({
}} }}
isOpen={isOpen} isOpen={isOpen}
> >
<DrawerContent title="Secret"> <DrawerContent title={`Secret – ${secret?.key}`} className="thin-scrollbar">
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full"> <form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<FormControl label="Key"> <div className="flex flex-row">
<Input isDisabled {...register("key")} /> <div className="w-full">
</FormControl>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { a={subject(ProjectPermissionSub.Secrets, {
@@ -273,7 +272,29 @@ export const SecretDetailSidebar = ({
/> />
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
<div className="mb-2 border-b border-mineshaft-600 pb-4"> </div>
<div className="ml-1 mt-1.5 flex items-center">
<Button
className="w-full px-2 py-[0.43rem] font-normal"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share
</Button>
</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">
<Controller
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { a={subject(ProjectPermissionSub.Secrets, {
@@ -284,23 +305,69 @@ export const SecretDetailSidebar = ({
})} })}
> >
{(isAllowed) => ( {(isAllowed) => (
<div className="flex items-center justify-between">
<span className="w-max text-sm text-mineshaft-300">
Multi-line encoding
<Tooltip
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip>
</span>
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center justify-between"
/>
</div>
)}
</ProjectPermissionCan>
)}
/>
</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 <Switch
isDisabled={!isAllowed} isDisabled={!isAllowed}
id="personal-override" id="personal-override"
onCheckedChange={handleOverrideClick} onCheckedChange={handleOverrideClick}
isChecked={isOverridden} isChecked={isOverridden}
> className="justify-start"
Override with a personal value />
</Switch> </div>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div>
{isOverridden && ( {isOverridden && (
<Controller <Controller
name="valueOverride" name="valueOverride"
control={control} control={control}
render={({ field }) => ( render={({ field }) => (
<FormControl label="Value Override"> <FormControl label="Override Value" className="px-4">
<InfisicalSecretInput <InfisicalSecretInput
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
environment={environment} environment={environment}
@@ -312,7 +379,129 @@ export const SecretDetailSidebar = ({
)} )}
/> />
)} )}
<FormControl label="Metadata"> </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 justify-between px-4 text-mineshaft-100 ${tagFields.fields.length > 0 ? "flex-col" : "flex-row"}`}
>
<div
className={`text-sm text-mineshaft-300 ${tagFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
>
Tags
</div>
<div>
<FormControl>
<div
className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}
>
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{slug}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add"
variant="outline_bg"
size="xs"
className="rounded-md"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="start" side="right" className="z-[100]">
<DropdownMenuLabel className="pl-2">
Add tags to this secret
</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<div className="p-2">
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</div>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
</div>
</div>
<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"}`}
>
<div
className={`text-sm text-mineshaft-300 ${metadataFormFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}
>
Metadata
</div>
<FormControl>
<div className="flex flex-col space-y-2"> <div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => ( {metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2"> <div key={metadataFieldId} className="flex items-end space-x-2">
@@ -364,114 +553,32 @@ export const SecretDetailSidebar = ({
</IconButton> </IconButton>
</div> </div>
))} ))}
<div className="mt-2"> <div className={`${metadataFormFields.fields.length > 0 ? "pt-2" : ""}`}>
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</FormControl>
<FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{slug}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton <IconButton
ariaLabel="add" ariaLabel="Add Key"
variant="outline_bg" variant="outline_bg"
size="xs" size="xs"
className="rounded-md" className="rounded-md"
isDisabled={!isAllowed} onClick={() => metadataFormFields.append({ key: "", value: "" })}
> >
<FontAwesomeIcon icon={faPlus} /> <FontAwesomeIcon icon={faPlus} />
</IconButton> </IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div> </div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<DropdownMenuItem asChild>
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div> </div>
</FormControl> </FormControl>
<FormControl label="Reminder"> </div>
</div>
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 bg-bunker-800 text-sm"
{...register("comment")}
readOnly={isReadOnly}
rows={5}
/>
</FormControl>
<FormControl>
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? ( {secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
<div className="ml-1 mt-2 flex items-center justify-between"> <div className="flex items-center justify-between px-2">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<FontAwesomeIcon className="text-primary-500" icon={faClock} /> <FontAwesomeIcon className="text-primary-500" icon={faClock} />
<span className="text-sm text-bunker-300"> <span className="text-sm text-bunker-300">
@@ -490,9 +597,9 @@ export const SecretDetailSidebar = ({
</div> </div>
</div> </div>
) : ( ) : (
<div className="ml-1 mt-2 flex items-center space-x-2"> <div className="ml-1 flex items-center space-x-2">
<Button <Button
className="w-full px-2 py-1" className="w-full px-2 py-2 font-normal"
variant="outline_bg" variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faClock} />} leftIcon={<FontAwesomeIcon icon={faClock} />}
onClick={() => setCreateReminderFormOpen.on()} onClick={() => setCreateReminderFormOpen.on()}
@@ -503,92 +610,147 @@ export const SecretDetailSidebar = ({
</div> </div>
)} )}
</FormControl> </FormControl>
<FormControl label="Comments & Notes"> <div className="mb-4flex-grow dark cursor-default text-sm text-bunker-300">
<TextArea <div className="mb-2 pl-1">Version History</div>
className="border border-mineshaft-600 text-sm" <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]">
{...register("comment")} {secretVersion?.map(({ createdAt, secretValue, version, id }) => (
readOnly={isReadOnly} <div className="flex flex-row">
rows={5} <div key={id} className="flex w-full flex-col space-y-1">
/> <div className="flex items-center">
</FormControl> <div className="w-10">
<div className="my-2 mb-4 border-b border-mineshaft-600 pb-4"> <div className="w-fit rounded-md border border-mineshaft-600 bg-mineshaft-700 px-1 text-sm text-mineshaft-300">
<Controller v{version}
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Multi line encoding
<Tooltip
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</Switch>
)}
</ProjectPermissionCan>
)}
/>
</div> </div>
<div className="ml-1 flex items-center space-x-4">
<Button
className="w-full px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share Secret
</Button>
</div>
<div className="dark mb-4 mt-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, secretValue, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
</div> </div>
<div>{format(new Date(createdAt), "Pp")}</div> <div>{format(new Date(createdAt), "Pp")}</div>
</div> </div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4"> <div className="flex w-full cursor-default">
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div> <div className="relative w-10">
<div className="break-all font-mono">{secretValue}</div> <div className="absolute bottom-0 left-3 top-0 mt-0.5 border-l border-mineshaft-400/60" />
</div>
<div className="flex flex-row">
<div className="h-min w-fit rounded-sm bg-primary-500/10 px-1 text-primary-300/70">
Value:
</div>
<div className="group break-all pl-1 font-mono">
<div className="relative hidden cursor-pointer transition-all duration-200 group-[.show-value]:inline">
<button
type="button"
className="select-none"
onClick={(e) => {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
navigator.clipboard.writeText(secretValue || "");
const target = e.currentTarget;
target.style.borderBottom = "1px dashed";
target.style.paddingBottom = "-1px";
// Create and insert popup
const popup = document.createElement("div");
popup.className =
"w-16 flex justify-center absolute top-6 left-0 text-xs text-primary-100 bg-mineshaft-800 px-1 py-0.5 rounded-md border border-primary-500/50";
popup.textContent = "Copied!";
target.parentElement?.appendChild(popup);
// Remove popup and border after delay
setTimeout(() => {
popup.remove();
target.style.borderBottom = "none";
}, 3000);
}
}}
>
{secretValue}
</button>
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.stopPropagation();
e.currentTarget
.closest(".group")
?.classList.remove("show-value");
}
}}
>
<FontAwesomeIcon icon={faEyeSlash} />
</button>
</div>
<span className="group-[.show-value]:hidden">
{secretValue?.replace(/./g, "*")}
<button
type="button"
className="ml-1 cursor-pointer"
onClick={(e) => {
e.currentTarget.closest(".group")?.classList.add("show-value");
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.currentTarget
.closest(".group")
?.classList.add("show-value");
}
}}
>
<FontAwesomeIcon icon={faEye} />
</button>
</span>
</div>
</div>
</div>
</div>
<div
className={`flex items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}
>
<Tooltip content="Restore Secret Value">
<IconButton
ariaLabel="Restore"
variant="outline_bg"
size="sm"
className="h-8 w-8 rounded-md"
onClick={() => setValue("value", secretValue)}
>
<FontAwesomeIcon icon={faArrowRotateRight} />
</IconButton>
</Tooltip>
</div> </div>
</div> </div>
))} ))}
</div> </div>
</div> </div>
<div className="dark mb-4 flex-grow text-sm text-bunker-300"> <div className="dark mb-4 flex-grow text-sm text-bunker-300">
<div className="mb-2"> <div className="mb-2 mt-4">
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."
className="z-[100]" className="z-[100]"
> >
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" /> <FontAwesomeIcon icon={faCircleQuestion} className="ml-2" />
</Tooltip> </Tooltip>
</div> </div>
{isPending && ( {isPending && (
@@ -608,14 +770,22 @@ export const SecretDetailSidebar = ({
</Button> </Button>
)} )}
{!isPending && secretAccessList && ( {!isPending && secretAccessList && (
<div className="flex max-h-72 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]"> <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>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{secretAccessList.users.map((user) => ( {secretAccessList.users.map((user) => (
<div className="rounded-md bg-bunker-500 px-1"> <div className="rounded-md bg-bunker-500">
<Tooltip content={user.allowedActions.join(", ")} className="z-[100]"> <Tooltip
content={user.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
className="z-[100]"
>
<Link <Link
to={ to={
`/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const `/${ProjectType.SecretManager}/$projectId/members/$membershipId` as const
@@ -624,7 +794,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id, projectId: currentWorkspace.id,
membershipId: user.membershipId membershipId: user.membershipId
}} }}
className="text-secondary/80 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-primary"
> >
{user.name} {user.name}
</Link> </Link>
@@ -639,9 +809,14 @@ export const SecretDetailSidebar = ({
<div className="mb-2 font-bold">Identities</div> <div className="mb-2 font-bold">Identities</div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{secretAccessList.identities.map((identity) => ( {secretAccessList.identities.map((identity) => (
<div className="rounded-md bg-bunker-500 px-1"> <div className="rounded-md bg-bunker-500">
<Tooltip <Tooltip
content={identity.allowedActions.join(", ")} content={identity.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
className="z-[100]" className="z-[100]"
> >
<Link <Link
@@ -652,7 +827,7 @@ export const SecretDetailSidebar = ({
projectId: currentWorkspace.id, projectId: currentWorkspace.id,
identityId: identity.id identityId: identity.id
}} }}
className="text-secondary/80 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-primary"
> >
{identity.name} {identity.name}
</Link> </Link>
@@ -667,9 +842,14 @@ export const SecretDetailSidebar = ({
<div className="mb-2 font-bold">Groups</div> <div className="mb-2 font-bold">Groups</div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{secretAccessList.groups.map((group) => ( {secretAccessList.groups.map((group) => (
<div className="rounded-md bg-bunker-500 px-1"> <div className="rounded-md bg-bunker-500">
<Tooltip <Tooltip
content={group.allowedActions.join(", ")} content={group.allowedActions
.map(
(action) =>
action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()
)
.join(", ")}
className="z-[100]" className="z-[100]"
> >
<Link <Link
@@ -677,7 +857,7 @@ export const SecretDetailSidebar = ({
params={{ params={{
groupId: group.id groupId: group.id
}} }}
className="text-secondary/80 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-primary"
> >
{group.name} {group.name}
</Link> </Link>
@@ -691,7 +871,7 @@ export const SecretDetailSidebar = ({
)} )}
</div> </div>
<div className="flex flex-col space-y-4"> <div className="flex flex-col space-y-4">
<div className="mb-2 flex items-center space-x-4"> <div className="mb-4 flex items-center space-x-4">
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { a={subject(ProjectPermissionSub.Secrets, {
@@ -705,6 +885,7 @@ export const SecretDetailSidebar = ({
<Button <Button
isFullWidth isFullWidth
type="submit" type="submit"
variant="outline_bg"
isDisabled={isSubmitting || !isDirty || !isAllowed} isDisabled={isSubmitting || !isDirty || !isAllowed}
isLoading={isSubmitting} isLoading={isSubmitting}
> >
@@ -722,9 +903,17 @@ export const SecretDetailSidebar = ({
})} })}
> >
{(isAllowed) => ( {(isAllowed) => (
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}> <IconButton
Delete colorSchema="danger"
</Button> ariaLabel="Delete Secret"
className="border border-mineshaft-600 bg-mineshaft-700 hover:border-red-500/70 hover:bg-red-600/20"
isDisabled={!isAllowed}
onClick={onDeleteSecret}
>
<Tooltip content="Delete Secret">
<FontAwesomeIcon icon={faTrash} />
</Tooltip>
</IconButton>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
</div> </div>