mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 00:28:31 +00:00
improve sidebars
This commit is contained in:
+40
-38
@@ -20,44 +20,46 @@ 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 (
|
||||||
<Item
|
<div className={!isSelected ? "hover:px-1" : ""}>
|
||||||
type="button"
|
<Item
|
||||||
role="menuitem"
|
type="button"
|
||||||
className={twMerge(
|
role="menuitem"
|
||||||
"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",
|
className={twMerge(
|
||||||
isSelected && "bg-bunker-800 hover:bg-mineshaft-600",
|
"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",
|
||||||
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
isSelected && "bg-bunker-800 hover:bg-mineshaft-600 rounded-none",
|
||||||
className
|
isDisabled && "cursor-not-allowed hover:bg-transparent",
|
||||||
)}
|
className
|
||||||
onMouseEnter={() => iconRef.current?.play()}
|
)}
|
||||||
onMouseLeave={() => iconRef.current?.stop()}
|
onMouseEnter={() => iconRef.current?.play()}
|
||||||
ref={inputRef}
|
onMouseLeave={() => iconRef.current?.stop()}
|
||||||
{...props}
|
ref={inputRef}
|
||||||
>
|
{...props}
|
||||||
<div
|
|
||||||
className={`${
|
|
||||||
isSelected ? "opacity-100" : "opacity-0"
|
|
||||||
} absolute -left-[0.28rem] h-full w-1 rounded-md bg-primary transition-all duration-150`}
|
|
||||||
/>
|
|
||||||
{icon && (
|
|
||||||
<div className="my-auto mb-2 h-6 w-6">
|
|
||||||
<DotLottieReact
|
|
||||||
dotLottieRefCallback={(el) => {
|
|
||||||
iconRef.current = el;
|
|
||||||
}}
|
|
||||||
src={`/lotties/${icon}.json`}
|
|
||||||
loop
|
|
||||||
className="h-full w-full"
|
|
||||||
mode={lottieIconMode}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="flex-grow justify-center break-words text-center"
|
|
||||||
style={{ fontSize: "10px" }}
|
|
||||||
>
|
>
|
||||||
{children}
|
<div
|
||||||
</div>
|
className={`${
|
||||||
</Item>
|
isSelected ? "opacity-100" : "opacity-0"
|
||||||
|
} absolute left-0 h-full w-1 bg-primary transition-all duration-150`}
|
||||||
|
/>
|
||||||
|
{icon && (
|
||||||
|
<div className="my-auto mb-2 h-6 w-6">
|
||||||
|
<DotLottieReact
|
||||||
|
dotLottieRefCallback={(el) => {
|
||||||
|
iconRef.current = el;
|
||||||
|
}}
|
||||||
|
src={`/lotties/${icon}.json`}
|
||||||
|
loop
|
||||||
|
className="h-full w-full"
|
||||||
|
mode={lottieIconMode}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="flex-grow justify-center break-words text-center"
|
||||||
|
style={{ fontSize: "10px" }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Item>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+40
-32
@@ -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,25 @@ 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 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-1 transition-all">
|
<div className="flex w-full items-center justify-center rounded-md border border-none border-mineshaft-600 p-3 pt-6 pb-5 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="p-1 shadow-mineshaft-600 drop-shadow-md cursor-default mt-6"
|
||||||
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 p-1 transition-all duration-300 bg-gradient-to-tr from-primary-500/5 to-mineshaft-800">
|
||||||
<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 +247,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 +257,7 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
}
|
}
|
||||||
icon="sliding-carousel"
|
icon="sliding-carousel"
|
||||||
>
|
>
|
||||||
Secret Manager
|
Secrets
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -264,7 +270,7 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
}
|
}
|
||||||
icon="note"
|
icon="note"
|
||||||
>
|
>
|
||||||
Cert Manager
|
PKI
|
||||||
</MenuIconButton>
|
</MenuIconButton>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -296,31 +302,31 @@ 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 +370,14 @@ 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 +425,17 @@ 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 onMouseEnter={() => setOpenUser(true)} onMouseLeave={() => setOpenUser(false)} side="right" align="end" className="p-1">
|
||||||
<div className="px-2 py-1">
|
<div className="px-1 py-1 cursor-default">
|
||||||
<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 p-1 px-2 transition-all duration-150 bg-gradient-to-tr from-primary-500/10 to-mineshaft-800">
|
||||||
<div className="p-2">
|
<div className="p-1 pr-3">
|
||||||
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400" />
|
<FontAwesomeIcon icon={faUser} className="text-mineshaft-400 text-xl" />
|
||||||
</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 +483,13 @@ export const MinimizedOrgSidebar = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<Link to="/organization/admin">
|
<div className="pt-1 mt-1 border-t border-mineshaft-600">
|
||||||
<DropdownMenuItem className="mt-1 border-t border-mineshaft-600">
|
<Link to="/organization/admin">
|
||||||
Organization Admin Console
|
<DropdownMenuItem>
|
||||||
</DropdownMenuItem>
|
Organization Admin Console
|
||||||
</Link>
|
</DropdownMenuItem>
|
||||||
|
</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>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
+7
-2
@@ -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}
|
||||||
|
|||||||
+389
-306
@@ -1,11 +1,13 @@
|
|||||||
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,
|
faCircle,
|
||||||
faCircleDot,
|
faCircleDot,
|
||||||
faClock,
|
faClock,
|
||||||
|
faEyeSlash,
|
||||||
faPlus,
|
faPlus,
|
||||||
faShare,
|
faShare,
|
||||||
faTag,
|
faTag,
|
||||||
@@ -236,44 +238,103 @@ 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">
|
{/* <FormControl label="Key">
|
||||||
<Input isDisabled {...register("key")} />
|
<Input isDisabled {...register("key")} />
|
||||||
</FormControl>
|
</FormControl> */}
|
||||||
<ProjectPermissionCan
|
<div className="flex flex-row">
|
||||||
I={ProjectPermissionActions.Edit}
|
<div className="w-full">
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
<ProjectPermissionCan
|
||||||
environment,
|
I={ProjectPermissionActions.Edit}
|
||||||
secretPath,
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
secretName: secretKey,
|
environment,
|
||||||
secretTags: selectTagSlugs
|
secretPath,
|
||||||
})}
|
secretName: secretKey,
|
||||||
>
|
secretTags: selectTagSlugs
|
||||||
{(isAllowed) => (
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
name="value"
|
||||||
|
key="secret-value"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControl label="Value">
|
||||||
|
<InfisicalSecretInput
|
||||||
|
isReadOnly={isReadOnly}
|
||||||
|
environment={environment}
|
||||||
|
secretPath={secretPath}
|
||||||
|
key="secret-value"
|
||||||
|
isDisabled={isOverridden || !isAllowed}
|
||||||
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||||
|
{...field}
|
||||||
|
autoFocus={false}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
</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 p-4 px-0 border rounded border-mineshaft-600 bg-mineshaft-900 pb-0">
|
||||||
|
<div className="mb-4 px-4">
|
||||||
<Controller
|
<Controller
|
||||||
name="value"
|
|
||||||
key="secret-value"
|
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field }) => (
|
name="skipMultilineEncoding"
|
||||||
<FormControl label="Value">
|
render={({ field: { value, onChange, onBlur } }) => (
|
||||||
<InfisicalSecretInput
|
<ProjectPermissionCan
|
||||||
isReadOnly={isReadOnly}
|
I={ProjectPermissionActions.Edit}
|
||||||
environment={environment}
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
secretPath={secretPath}
|
environment,
|
||||||
key="secret-value"
|
secretPath,
|
||||||
isDisabled={isOverridden || !isAllowed}
|
secretName: secretKey,
|
||||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
secretTags: selectTagSlugs
|
||||||
{...field}
|
})}
|
||||||
autoFocus={false}
|
>
|
||||||
/>
|
{(isAllowed) => (
|
||||||
</FormControl>
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="w-max text-mineshaft-300 text-sm">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" size="md" />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
id="skipmultiencoding-option"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
onBlur={onBlur}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
className="items-center justify-between"
|
||||||
|
>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
</ProjectPermissionCan>
|
<div className={`w-full border-t border-mineshaft-600 mb-4 ${isOverridden ? "block" : "hidden"}`}></div>
|
||||||
<div className="mb-2 border-b border-mineshaft-600 pb-4">
|
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
@@ -284,194 +345,219 @@ export const SecretDetailSidebar = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Switch
|
|
||||||
isDisabled={!isAllowed}
|
<div className="flex items-center justify-between pb-4 px-4">
|
||||||
id="personal-override"
|
<span className="w-max text-mineshaft-300 text-sm">Override with a personal value
|
||||||
onCheckedChange={handleOverrideClick}
|
<Tooltip
|
||||||
isChecked={isOverridden}
|
content="Override the secret value with a personal value that does not get shared with other users and machines."
|
||||||
>
|
className="z-[100]"
|
||||||
Override with a personal value
|
>
|
||||||
</Switch>
|
<FontAwesomeIcon icon={faCircleQuestion} className="ml-2" size="md" />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
<Switch
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
id="personal-override"
|
||||||
|
onCheckedChange={handleOverrideClick}
|
||||||
|
isChecked={isOverridden}
|
||||||
|
className="justify-start"
|
||||||
|
>
|
||||||
|
</Switch>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</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>
|
||||||
{isOverridden && (
|
<div className="flex flex-col bg-mineshaft-900 rounded-md p-4 px-0 pb-0 mb-4 mt-2 border border-mineshaft-600">
|
||||||
<Controller
|
<div className={`flex justify-between text-mineshaft-100 px-4 ${tagFields.fields.length > 0 ? "flex-col " : "flex-row "}`}>
|
||||||
name="valueOverride"
|
<div className={`text-sm text-mineshaft-300 ${tagFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}>Tags</div>
|
||||||
control={control}
|
<div>
|
||||||
render={({ field }) => (
|
<FormControl>
|
||||||
<FormControl label="Value Override">
|
<div className={`grid auto-cols-min grid-flow-col gap-2 overflow-hidden ${tagFields.fields.length > 0 ? "pt-2" : ""}`}>
|
||||||
<InfisicalSecretInput
|
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
|
||||||
isReadOnly={isReadOnly}
|
<Tag
|
||||||
environment={environment}
|
className="flex w-min items-center space-x-2"
|
||||||
secretPath={secretPath}
|
key={formId}
|
||||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
onClose={() => {
|
||||||
{...field}
|
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>
|
</FormControl>
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FormControl label="Metadata">
|
|
||||||
<div className="flex flex-col space-y-2">
|
|
||||||
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
|
||||||
<div key={metadataFieldId} className="flex items-end space-x-2">
|
|
||||||
<div className="flex-grow">
|
|
||||||
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`secretMetadata.${i}.key`}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-0"
|
|
||||||
>
|
|
||||||
<Input {...field} className="max-h-8" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow">
|
|
||||||
{i === 0 && (
|
|
||||||
<FormLabel
|
|
||||||
label="Value"
|
|
||||||
className="text-xs text-mineshaft-400"
|
|
||||||
isOptional
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`secretMetadata.${i}.value`}
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
isError={Boolean(error?.message)}
|
|
||||||
errorText={error?.message}
|
|
||||||
className="mb-0"
|
|
||||||
>
|
|
||||||
<Input {...field} className="max-h-8" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete key"
|
|
||||||
className="bottom-0.5 max-h-8"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => metadataFormFields.remove(i)}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faTrash} />
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="mt-2">
|
|
||||||
<Button
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
variant="outline_bg"
|
|
||||||
onClick={() => metadataFormFields.append({ key: "", value: "" })}
|
|
||||||
>
|
|
||||||
Add Key
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
<div className={`w-full border-t border-mineshaft-600 mb-4 ${tagFields.fields.length > 0 || metadataFormFields.fields.length > 0 ? "block" : "hidden"}`}></div>
|
||||||
<FormControl label="Tags" className="">
|
<div className={`flex justify-between text-mineshaft-100 px-4 ${metadataFormFields.fields.length > 0 ? "flex-col " : "flex-row "}`}>
|
||||||
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
|
<div className={`text-sm text-mineshaft-300 ${metadataFormFields.fields.length > 0 ? "mb-2" : "mt-0.5"}`}>Metadata</div>
|
||||||
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
|
<FormControl>
|
||||||
<Tag
|
<div className="flex flex-col space-y-2">
|
||||||
className="flex w-min items-center space-x-2"
|
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
|
||||||
key={formId}
|
<div key={metadataFieldId} className="flex items-end space-x-2">
|
||||||
onClose={() => {
|
<div className="flex-grow">
|
||||||
if (cannotEditSecret) {
|
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
|
||||||
createNotification({ type: "error", text: "Access denied" });
|
<Controller
|
||||||
return;
|
control={control}
|
||||||
}
|
name={`secretMetadata.${i}.key`}
|
||||||
const tag = tags?.find(({ id: tagId }) => id === tagId);
|
render={({ field, fieldState: { error } }) => (
|
||||||
if (tag) handleTagSelect(tag);
|
<FormControl
|
||||||
}}
|
isError={Boolean(error?.message)}
|
||||||
>
|
errorText={error?.message}
|
||||||
<div
|
className="mb-0"
|
||||||
className="h-3 w-3 rounded-full"
|
>
|
||||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
<Input {...field} className="max-h-8" />
|
||||||
/>
|
</FormControl>
|
||||||
<div className="text-sm">{slug}</div>
|
)}
|
||||||
</Tag>
|
/>
|
||||||
))}
|
</div>
|
||||||
<DropdownMenu>
|
<div className="flex-grow">
|
||||||
<ProjectPermissionCan
|
{i === 0 && (
|
||||||
I={ProjectPermissionActions.Edit}
|
<FormLabel
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
label="Value"
|
||||||
environment,
|
className="text-xs text-mineshaft-400"
|
||||||
secretPath,
|
isOptional
|
||||||
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="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>
|
<Controller
|
||||||
</DropdownMenuItem>
|
control={control}
|
||||||
);
|
name={`secretMetadata.${i}.value`}
|
||||||
})}
|
render={({ field, fieldState: { error } }) => (
|
||||||
<ProjectPermissionCan
|
<FormControl
|
||||||
I={ProjectPermissionActions.Create}
|
isError={Boolean(error?.message)}
|
||||||
a={ProjectPermissionSub.Tags}
|
errorText={error?.message}
|
||||||
>
|
className="mb-0"
|
||||||
{(isAllowed) => (
|
>
|
||||||
<DropdownMenuItem asChild>
|
<Input {...field} className="max-h-8" />
|
||||||
<Button
|
</FormControl>
|
||||||
size="xs"
|
)}
|
||||||
className="w-full"
|
/>
|
||||||
colorSchema="primary"
|
</div>
|
||||||
variant="outline_bg"
|
<IconButton
|
||||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
ariaLabel="delete key"
|
||||||
onClick={onCreateTag}
|
className="bottom-0.5 max-h-8"
|
||||||
isDisabled={!isAllowed}
|
variant="outline_bg"
|
||||||
>
|
onClick={() => metadataFormFields.remove(i)}
|
||||||
Create a tag
|
>
|
||||||
</Button>
|
<FontAwesomeIcon icon={faTrash} />
|
||||||
</DropdownMenuItem>
|
</IconButton>
|
||||||
)}
|
</div>
|
||||||
</ProjectPermissionCan>
|
))}
|
||||||
</DropdownMenuContent>
|
<div className={`${metadataFormFields.fields.length > 0 ? "pt-2" : ""}`}>
|
||||||
</DropdownMenu>
|
<IconButton ariaLabel="Add Key" variant="outline_bg" size="xs" className="rounded-md" onClick={() => metadataFormFields.append({ key: "", value: "" })}>
|
||||||
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormControl label="Comments & Notes">
|
||||||
|
<TextArea
|
||||||
|
className="border border-mineshaft-600 text-sm bg-bunker-800"
|
||||||
|
{...register("comment")}
|
||||||
|
readOnly={isReadOnly}
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl label="Reminder">
|
<FormControl>
|
||||||
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
|
||||||
<div className="ml-1 mt-2 flex items-center justify-between">
|
<div className="px-2 flex items-center justify-between">
|
||||||
<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 +576,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 +589,94 @@ export const SecretDetailSidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl label="Comments & Notes">
|
<div className="dark mb-4flex-grow text-sm text-bunker-300 cursor-default">
|
||||||
<TextArea
|
<div className="mb-2 pl-1">Version History</div>
|
||||||
className="border border-mineshaft-600 text-sm"
|
<div className="flex h-48 flex-col space-y-2 overflow-y-auto thin-scrollbar 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 }, i) => (
|
||||||
readOnly={isReadOnly}
|
<div className="flex flex-row">
|
||||||
rows={5}
|
<div key={id} className="flex flex-col space-y-1 w-full">
|
||||||
/>
|
<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="text-mineshaft-300 text-sm bg-mineshaft-700 border border-mineshaft-600 rounded-md px-1 w-fit">
|
||||||
<Controller
|
v{version}
|
||||||
control={control}
|
</div>
|
||||||
name="skipMultilineEncoding"
|
</div>
|
||||||
render={({ field: { value, onChange, onBlur } }) => (
|
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||||
<ProjectPermissionCan
|
</div>
|
||||||
I={ProjectPermissionActions.Edit}
|
<div className="flex w-full cursor-default">
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
<div className="w-10 relative">
|
||||||
environment,
|
<div className="absolute left-3 top-0 bottom-0 border-l mt-0.5 border-mineshaft-400/60"></div>
|
||||||
secretPath,
|
</div>
|
||||||
secretName: secretKey,
|
<div className="flex flex-row">
|
||||||
secretTags: selectTagSlugs
|
<div className="rounded-sm bg-primary-500/10 text-primary-300/70 px-1 w-fit h-min">Value:</div>
|
||||||
})}
|
<div className="pl-1 break-all font-mono group">
|
||||||
>
|
<div className="hidden group-[.show-value]:inline transition-all duration-200 cursor-pointer relative">
|
||||||
{(isAllowed) => (
|
<span
|
||||||
<Switch
|
className="select-none"
|
||||||
id="skipmultiencoding-option"
|
onClick={(e) => {
|
||||||
onCheckedChange={(isChecked) => onChange(isChecked)}
|
navigator.clipboard.writeText(secretValue || '');
|
||||||
isChecked={value}
|
const target = e.currentTarget;
|
||||||
onBlur={onBlur}
|
target.style.borderBottom = '1px dashed';
|
||||||
isDisabled={!isAllowed}
|
target.style.paddingBottom = '-1px';
|
||||||
className="items-center"
|
|
||||||
>
|
// Create and insert popup
|
||||||
Multi line encoding
|
const popup = document.createElement('div');
|
||||||
<Tooltip
|
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';
|
||||||
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
|
popup.textContent = 'Copied!';
|
||||||
className="z-[100]"
|
target.parentElement?.appendChild(popup);
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
|
// Remove popup and border after delay
|
||||||
</Tooltip>
|
setTimeout(() => {
|
||||||
</Switch>
|
popup.remove();
|
||||||
)}
|
target.style.borderBottom = 'none';
|
||||||
</ProjectPermissionCan>
|
}, 3000);
|
||||||
)}
|
}}
|
||||||
/>
|
>
|
||||||
</div>
|
{secretValue}
|
||||||
<div className="ml-1 flex items-center space-x-4">
|
</span>
|
||||||
<Button
|
<FontAwesomeIcon
|
||||||
className="w-full px-2 py-1"
|
icon={faEyeSlash}
|
||||||
variant="outline_bg"
|
className="ml-1 cursor-pointer"
|
||||||
leftIcon={<FontAwesomeIcon icon={faShare} />}
|
onClick={(e) => {
|
||||||
onClick={() => {
|
e.stopPropagation();
|
||||||
const value = secret?.valueOverride ?? secret?.value;
|
e.currentTarget.closest('.group')?.classList.remove('show-value');
|
||||||
if (value) {
|
}}
|
||||||
handleSecretShare(value);
|
/>
|
||||||
}
|
</div>
|
||||||
}}
|
<span className="group-[.show-value]:hidden">
|
||||||
>
|
{secretValue?.replace(/./g, '*')}
|
||||||
Share Secret
|
<FontAwesomeIcon
|
||||||
</Button>
|
icon={faEye}
|
||||||
</div>
|
className="ml-1 cursor-pointer"
|
||||||
<div className="dark mb-4 mt-4 flex-grow text-sm text-bunker-300">
|
onClick={(e) => {
|
||||||
<div className="mb-2">Version History</div>
|
e.currentTarget.closest('.group')?.classList.add('show-value');
|
||||||
<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">
|
</span>
|
||||||
<div className="flex items-center space-x-2">
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
|
||||||
</div>
|
</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 items-center justify-center ${version === secretVersion.length ? "hidden" : ""}`}>
|
||||||
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
<Tooltip content="Restore Secret Value">
|
||||||
<div className="break-all font-mono">{secretValue}</div>
|
<IconButton ariaLabel="Restore" variant="outline_bg" size="sm" className="rounded-md h-8 w-8" 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 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 +696,14 @@ 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="flex max-h-72 mb-4 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 +712,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 bg-mineshaft-700 border border-mineshaft-600 rounded-md px-1 py-0.5 text-sm hover:text-primary"
|
||||||
>
|
>
|
||||||
{user.name}
|
{user.name}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -639,11 +727,8 @@ 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.map(action => action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()).join(", ")} className="z-[100]">
|
||||||
content={identity.allowedActions.join(", ")}
|
|
||||||
className="z-[100]"
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
to={
|
to={
|
||||||
`/${ProjectType.SecretManager}/$projectId/identities/$identityId` as const
|
`/${ProjectType.SecretManager}/$projectId/identities/$identityId` as const
|
||||||
@@ -652,7 +737,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 bg-mineshaft-700 border border-mineshaft-600 rounded-md px-1 py-0.5 text-sm hover:text-primary"
|
||||||
>
|
>
|
||||||
{identity.name}
|
{identity.name}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -667,17 +752,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.map(action => action.charAt(0).toUpperCase() + action.slice(1).toLowerCase()).join(", ")} className="z-[100]">
|
||||||
content={group.allowedActions.join(", ")}
|
|
||||||
className="z-[100]"
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
to={"/organization/groups/$groupId" as const}
|
to={"/organization/groups/$groupId" as const}
|
||||||
params={{
|
params={{
|
||||||
groupId: group.id
|
groupId: group.id
|
||||||
}}
|
}}
|
||||||
className="text-secondary/80 text-sm hover:text-primary"
|
className="text-secondary/80 bg-mineshaft-700 border border-mineshaft-600 rounded-md px-1 py-0.5 text-sm hover:text-primary"
|
||||||
>
|
>
|
||||||
{group.name}
|
{group.name}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -691,7 +773,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 +787,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 +805,9 @@ export const SecretDetailSidebar = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
<IconButton colorSchema="danger" ariaLabel="Delete Secret" className="bg-mineshaft-700 border border-mineshaft-600 hover:bg-red-600/20 hover:border-red-500/70" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
||||||
Delete
|
<Tooltip content="Delete Secret"><FontAwesomeIcon icon={faTrash} /></Tooltip>
|
||||||
</Button>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user