mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +00:00
feat: added reference tree to overview and secret main page, changed to better graph structure
This commit is contained in:
@@ -11,6 +11,8 @@ import { TSecretV2BridgeDALFactory } from "./secret-v2-bridge-dal";
|
|||||||
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
|
import { TFnSecretBulkDelete, TFnSecretBulkInsert, TFnSecretBulkUpdate } from "./secret-v2-bridge-types";
|
||||||
|
|
||||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
|
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
|
||||||
|
// akhilmhdh: JS regex with global save state in .test
|
||||||
|
const INTERPOLATION_SYNTAX_REG_NON_GLOBAL = /\${([^}]+)}/;
|
||||||
|
|
||||||
export const shouldUseSecretV2Bridge = (version: number) => version === 3;
|
export const shouldUseSecretV2Bridge = (version: number) => version === 3;
|
||||||
|
|
||||||
@@ -497,7 +499,7 @@ export const expandSecretReferencesFactory = ({
|
|||||||
trace
|
trace
|
||||||
};
|
};
|
||||||
|
|
||||||
const shouldExpandMore = INTERPOLATION_SYNTAX_REG.test(referencedSecretValue);
|
const shouldExpandMore = INTERPOLATION_SYNTAX_REG_NON_GLOBAL.test(referencedSecretValue);
|
||||||
if (dto.shouldStackTrace) {
|
if (dto.shouldStackTrace) {
|
||||||
const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null };
|
const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null };
|
||||||
trace?.children.push(stackTraceNode);
|
trace?.children.push(stackTraceNode);
|
||||||
|
|||||||
+1
-44
@@ -7,7 +7,6 @@ import {
|
|||||||
faCircleDot,
|
faCircleDot,
|
||||||
faClock,
|
faClock,
|
||||||
faPlus,
|
faPlus,
|
||||||
faProjectDiagram,
|
|
||||||
faShare,
|
faShare,
|
||||||
faTag
|
faTag
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -29,9 +28,6 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
|
||||||
ModalContent,
|
|
||||||
ModalTrigger,
|
|
||||||
Switch,
|
Switch,
|
||||||
Tag,
|
Tag,
|
||||||
TextArea,
|
TextArea,
|
||||||
@@ -45,7 +41,6 @@ import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
|
|||||||
|
|
||||||
import { CreateReminderForm } from "./CreateReminderForm";
|
import { CreateReminderForm } from "./CreateReminderForm";
|
||||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||||
import { SecretReferenceTree } from "./SecretReferenceDetails";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -65,8 +60,6 @@ type Props = {
|
|||||||
handleSecretShare: (value: string) => void;
|
handleSecretShare: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/g;
|
|
||||||
|
|
||||||
export const SecretDetailSidebar = ({
|
export const SecretDetailSidebar = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onToggle,
|
onToggle,
|
||||||
@@ -176,8 +169,6 @@ export const SecretDetailSidebar = ({
|
|||||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
||||||
const secretReminderNote = watch("reminderNote");
|
const secretReminderNote = watch("reminderNote");
|
||||||
|
|
||||||
const hasReferences = secret?.value?.match(INTERPOLATION_SYNTAX_REG);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CreateReminderForm
|
<CreateReminderForm
|
||||||
@@ -402,7 +393,7 @@ export const SecretDetailSidebar = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="mt-2 ml-1 flex items-center space-x-2">
|
<div className="mt-2 ml-1 flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
className="px-2 py-1"
|
className="w-full px-2 py-1"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
leftIcon={<FontAwesomeIcon icon={faClock} />}
|
||||||
onClick={() => setCreateReminderFormOpen.on()}
|
onClick={() => setCreateReminderFormOpen.on()}
|
||||||
@@ -458,40 +449,6 @@ export const SecretDetailSidebar = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-1 flex items-center space-x-4">
|
<div className="ml-1 flex items-center space-x-4">
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<Modal>
|
|
||||||
<Tooltip
|
|
||||||
className="text-center"
|
|
||||||
content={hasReferences ? "" : "Secret does not contain references"}
|
|
||||||
>
|
|
||||||
<ModalTrigger asChild>
|
|
||||||
<Button
|
|
||||||
variant="outline_bg"
|
|
||||||
className="w-full px-2 py-1"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faProjectDiagram} />}
|
|
||||||
isDisabled={!isAllowed || !hasReferences}
|
|
||||||
>
|
|
||||||
Reference Tree
|
|
||||||
</Button>
|
|
||||||
</ModalTrigger>
|
|
||||||
</Tooltip>
|
|
||||||
<ModalContent
|
|
||||||
title="Secret Reference Tree"
|
|
||||||
subTitle="Visual breakdown of secrets referenced by this secret."
|
|
||||||
>
|
|
||||||
<SecretReferenceTree
|
|
||||||
secretPath={secretPath}
|
|
||||||
environment={environment}
|
|
||||||
secret={secret}
|
|
||||||
/>
|
|
||||||
</ModalContent>
|
|
||||||
</Modal>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full px-2 py-1"
|
className="w-full px-2 py-1"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalTrigger,
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
@@ -36,8 +39,8 @@ import { AnimatePresence, motion } from "framer-motion";
|
|||||||
import { memo, useEffect } from "react";
|
import { memo, useEffect } 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 "../SecretReferenceDetails";
|
||||||
|
|
||||||
import { CreateReminderForm } from "./CreateReminderForm";
|
|
||||||
import {
|
import {
|
||||||
FontAwesomeSpriteName,
|
FontAwesomeSpriteName,
|
||||||
formSchema,
|
formSchema,
|
||||||
@@ -100,9 +103,6 @@ export const SecretItem = memo(
|
|||||||
|
|
||||||
const secretName = watch("key");
|
const secretName = watch("key");
|
||||||
|
|
||||||
const secretReminderRepeatDays = watch("reminderRepeatDays");
|
|
||||||
const secretReminderNote = watch("reminderNote");
|
|
||||||
|
|
||||||
const overrideAction = watch("overrideAction");
|
const overrideAction = watch("overrideAction");
|
||||||
const hasComment = Boolean(watch("comment"));
|
const hasComment = Boolean(watch("comment"));
|
||||||
|
|
||||||
@@ -139,7 +139,6 @@ export const SecretItem = memo(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
||||||
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
if (isSecValueCopied) {
|
if (isSecValueCopied) {
|
||||||
@@ -202,206 +201,153 @@ export const SecretItem = memo(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||||
<CreateReminderForm
|
<div
|
||||||
repeatDays={secretReminderRepeatDays}
|
className={twMerge(
|
||||||
note={secretReminderNote}
|
"border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700",
|
||||||
isOpen={createReminderFormOpen}
|
isDirty && "border-primary-400/50"
|
||||||
onOpenChange={(_, data) => {
|
)}
|
||||||
setCreateReminderFormOpen.toggle();
|
>
|
||||||
|
<div className="group flex">
|
||||||
if (data) {
|
<div
|
||||||
setValue("reminderRepeatDays", data.days, { shouldDirty: true });
|
className={twMerge(
|
||||||
setValue("reminderNote", data.note, { shouldDirty: true });
|
"flex h-11 w-11 items-center justify-center px-4 py-3",
|
||||||
}
|
isDirty && "text-primary"
|
||||||
}}
|
)}
|
||||||
/>
|
>
|
||||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
<Checkbox
|
||||||
<div
|
id={`checkbox-${secret.id}`}
|
||||||
className={twMerge(
|
isChecked={isSelected}
|
||||||
"border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700",
|
onCheckedChange={() => onToggleSecretSelect(secret)}
|
||||||
isDirty && "border-primary-400/50"
|
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
|
||||||
)}
|
/>
|
||||||
>
|
<FontAwesomeSymbol
|
||||||
<div className="group flex">
|
|
||||||
<div
|
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex h-11 w-11 items-center justify-center px-4 py-3",
|
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
|
||||||
isDirty && "text-primary"
|
isSelected && "hidden"
|
||||||
)}
|
)}
|
||||||
>
|
symbolName={FontAwesomeSpriteName.SecretKey}
|
||||||
<Checkbox
|
/>
|
||||||
id={`checkbox-${secret.id}`}
|
</div>
|
||||||
isChecked={isSelected}
|
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
||||||
onCheckedChange={() => onToggleSecretSelect(secret)}
|
<Controller
|
||||||
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
|
name="key"
|
||||||
/>
|
control={control}
|
||||||
<FontAwesomeSymbol
|
render={({ field, fieldState: { error } }) => (
|
||||||
className={twMerge(
|
<Input
|
||||||
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
|
autoComplete="off"
|
||||||
isSelected && "hidden"
|
isReadOnly={isReadOnly}
|
||||||
)}
|
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||||
symbolName={FontAwesomeSpriteName.SecretKey}
|
variant="plain"
|
||||||
/>
|
isDisabled={isOverriden}
|
||||||
</div>
|
placeholder={error?.message}
|
||||||
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
|
isError={Boolean(error)}
|
||||||
|
onKeyUp={() => trigger("key")}
|
||||||
|
{...field}
|
||||||
|
className="w-full px-0 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
>
|
||||||
|
{isOverriden ? (
|
||||||
<Controller
|
<Controller
|
||||||
name="key"
|
name="valueOverride"
|
||||||
|
key="value-overriden"
|
||||||
control={control}
|
control={control}
|
||||||
render={({ field, fieldState: { error } }) => (
|
render={({ field }) => (
|
||||||
<Input
|
<SecretInput
|
||||||
autoComplete="off"
|
key="value-overriden"
|
||||||
|
isVisible={isVisible}
|
||||||
isReadOnly={isReadOnly}
|
isReadOnly={isReadOnly}
|
||||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
|
||||||
variant="plain"
|
|
||||||
isDisabled={isOverriden}
|
|
||||||
placeholder={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
onKeyUp={() => trigger("key")}
|
|
||||||
{...field}
|
{...field}
|
||||||
className="w-full px-0 placeholder:text-red-500 focus:text-bunker-100 focus:ring-transparent"
|
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
) : (
|
||||||
<div
|
<Controller
|
||||||
className="flex w-80 flex-grow items-center border-x border-mineshaft-600 py-1 pl-4 pr-2"
|
name="value"
|
||||||
tabIndex={0}
|
key="secret-value"
|
||||||
role="button"
|
control={control}
|
||||||
>
|
render={({ field }) => (
|
||||||
{isOverriden ? (
|
<InfisicalSecretInput
|
||||||
<Controller
|
isReadOnly={isReadOnly}
|
||||||
name="valueOverride"
|
key="secret-value"
|
||||||
key="value-overriden"
|
isVisible={isVisible}
|
||||||
control={control}
|
environment={environment}
|
||||||
render={({ field }) => (
|
secretPath={secretPath}
|
||||||
<SecretInput
|
{...field}
|
||||||
key="value-overriden"
|
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||||
isVisible={isVisible}
|
/>
|
||||||
isReadOnly={isReadOnly}
|
)}
|
||||||
{...field}
|
/>
|
||||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
)}
|
||||||
/>
|
<div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
|
||||||
)}
|
<Tooltip content="Copy secret">
|
||||||
/>
|
<IconButton
|
||||||
) : (
|
ariaLabel="copy-value"
|
||||||
<Controller
|
variant="plain"
|
||||||
name="value"
|
size="sm"
|
||||||
key="secret-value"
|
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
|
||||||
control={control}
|
onClick={copyTokenToClipboard}
|
||||||
render={({ field }) => (
|
>
|
||||||
<InfisicalSecretInput
|
<FontAwesomeSymbol
|
||||||
isReadOnly={isReadOnly}
|
className="h-3.5 w-3"
|
||||||
key="secret-value"
|
symbolName={
|
||||||
isVisible={isVisible}
|
isSecValueCopied
|
||||||
environment={environment}
|
? FontAwesomeSpriteName.Check
|
||||||
secretPath={secretPath}
|
: FontAwesomeSpriteName.ClipboardCopy
|
||||||
{...field}
|
}
|
||||||
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
|
/>
|
||||||
/>
|
</IconButton>
|
||||||
)}
|
</Tooltip>
|
||||||
/>
|
<ProjectPermissionCan
|
||||||
)}
|
I={ProjectPermissionActions.Edit}
|
||||||
<div key="actions" className="flex h-8 flex-shrink-0 self-start transition-all">
|
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName, secretTags: selectedTagSlugs})}
|
||||||
<Tooltip content="Copy secret">
|
>
|
||||||
<IconButton
|
{(isAllowed) => (
|
||||||
ariaLabel="copy-value"
|
<Modal>
|
||||||
variant="plain"
|
<ModalTrigger asChild>
|
||||||
size="sm"
|
<IconButton
|
||||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
|
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
|
||||||
onClick={copyTokenToClipboard}
|
variant="plain"
|
||||||
>
|
size="md"
|
||||||
<FontAwesomeSymbol
|
ariaLabel="reference-tree"
|
||||||
className="h-3.5 w-3"
|
isDisabled={!isAllowed || !hasSecretReference(secret?.value)}
|
||||||
symbolName={
|
|
||||||
isSecValueCopied
|
|
||||||
? FontAwesomeSpriteName.Check
|
|
||||||
: FontAwesomeSpriteName.ClipboardCopy
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<DropdownMenu>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="tags"
|
|
||||||
variant="plain"
|
|
||||||
size="sm"
|
|
||||||
className={twMerge(
|
|
||||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-5",
|
|
||||||
hasTagsApplied && "w-5 text-primary"
|
|
||||||
)}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<Tooltip content="Tags">
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
symbolName={FontAwesomeSpriteName.Tags}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
)}
|
|
||||||
</ProjectPermissionCan>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
|
||||||
{tags.map((tag) => {
|
|
||||||
const { id: tagId, slug, color } = tag;
|
|
||||||
|
|
||||||
const isTagSelected = selectedTagsGroupById?.[tagId];
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => handleTagSelect(tag)}
|
|
||||||
key={`${secret.id}-${tagId}`}
|
|
||||||
icon={
|
|
||||||
isTagSelected && (
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
symbolName={FontAwesomeSpriteName.CheckedCircle}
|
|
||||||
className="h-3 w-3"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
iconPos="right"
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<div
|
|
||||||
className="mr-2 h-2 w-2 rounded-full"
|
|
||||||
style={{ background: color || "#bec2c8" }}
|
|
||||||
/>
|
|
||||||
{slug}
|
|
||||||
</div>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
<DropdownMenuItem className="px-1.5" asChild>
|
|
||||||
<Button
|
|
||||||
size="xs"
|
|
||||||
className="w-full"
|
|
||||||
colorSchema="primary"
|
|
||||||
variant="outline_bg"
|
|
||||||
leftIcon={
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
symbolName={FontAwesomeSpriteName.Tags}
|
|
||||||
className="h-3 w-3"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
onClick={onCreateTag}
|
|
||||||
>
|
>
|
||||||
Create a tag
|
<Tooltip
|
||||||
</Button>
|
content={
|
||||||
</DropdownMenuItem>
|
hasSecretReference(secret?.value)
|
||||||
</DropdownMenuContent>
|
? "Secret Reference Tree"
|
||||||
</DropdownMenu>
|
: "Secret does not contain references"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FontAwesomeSymbol
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
symbolName={FontAwesomeSpriteName.SecretReferenceTree}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
title="Secret Reference Tree"
|
||||||
|
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||||
|
>
|
||||||
|
<SecretReferenceTree
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
|
secretKey={secret?.key}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<DropdownMenu>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Edit}
|
I={ProjectPermissionActions.Edit}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
@@ -410,238 +356,287 @@ export const SecretItem = memo(
|
|||||||
secretName,
|
secretName,
|
||||||
secretTags: selectedTagSlugs
|
secretTags: selectedTagSlugs
|
||||||
})}
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="tags"
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
className={twMerge(
|
||||||
|
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-5",
|
||||||
|
hasTagsApplied && "w-5 text-primary"
|
||||||
|
)}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<Tooltip content="Tags">
|
||||||
|
<FontAwesomeSymbol
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
symbolName={FontAwesomeSpriteName.Tags}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
|
||||||
|
{tags.map((tag) => {
|
||||||
|
const { id: tagId, slug, color } = tag;
|
||||||
|
|
||||||
|
const isTagSelected = selectedTagsGroupById?.[tagId];
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => handleTagSelect(tag)}
|
||||||
|
key={`${secret.id}-${tagId}`}
|
||||||
|
icon={
|
||||||
|
isTagSelected && (
|
||||||
|
<FontAwesomeSymbol
|
||||||
|
symbolName={FontAwesomeSpriteName.CheckedCircle}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
iconPos="right"
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div
|
||||||
|
className="mr-2 h-2 w-2 rounded-full"
|
||||||
|
style={{ background: color || "#bec2c8" }}
|
||||||
|
/>
|
||||||
|
{slug}
|
||||||
|
</div>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<DropdownMenuItem className="px-1.5" asChild>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
className="w-full"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="outline_bg"
|
||||||
|
leftIcon={
|
||||||
|
<FontAwesomeSymbol
|
||||||
|
symbolName={FontAwesomeSpriteName.Tags}
|
||||||
|
className="h-3 w-3"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={onCreateTag}
|
||||||
|
>
|
||||||
|
Create a tag
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName,
|
||||||
|
secretTags: selectedTagSlugs
|
||||||
|
})}
|
||||||
|
renderTooltip
|
||||||
|
allowedLabel="Override"
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="override-value"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
variant="plain"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleOverrideClick}
|
||||||
|
className={twMerge(
|
||||||
|
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5",
|
||||||
|
isOverriden && "w-5 text-primary"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeSymbol
|
||||||
|
symbolName={FontAwesomeSpriteName.Override}
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<Popover>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Edit}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName,
|
||||||
|
secretTags: selectedTagSlugs
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||||
|
<IconButton
|
||||||
|
className={twMerge(
|
||||||
|
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
||||||
|
hasComment && "w-5 text-primary"
|
||||||
|
)}
|
||||||
|
variant="plain"
|
||||||
|
size="md"
|
||||||
|
ariaLabel="add-comment"
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
>
|
||||||
|
<Tooltip content="Comment">
|
||||||
|
<FontAwesomeSymbol
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
symbolName={FontAwesomeSpriteName.Comment}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</PopoverTrigger>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
<IconButton
|
||||||
|
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
|
||||||
|
variant="plain"
|
||||||
|
size="md"
|
||||||
|
ariaLabel="share-secret"
|
||||||
|
onClick={handleSecretShare}
|
||||||
|
>
|
||||||
|
<Tooltip content="Share Secret">
|
||||||
|
<FontAwesomeSymbol
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
symbolName={FontAwesomeSpriteName.ShareSecret}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
|
||||||
|
sticky="always"
|
||||||
|
>
|
||||||
|
<FormControl label="Comment" className="mb-0">
|
||||||
|
<TextArea
|
||||||
|
className="border border-mineshaft-600 text-sm"
|
||||||
|
rows={8}
|
||||||
|
cols={30}
|
||||||
|
{...register("comment")}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AnimatePresence exitBeforeEnter>
|
||||||
|
{!isDirty ? (
|
||||||
|
<motion.div
|
||||||
|
key="options"
|
||||||
|
className="flex h-10 flex-shrink-0 items-center space-x-4 px-[0.64rem]"
|
||||||
|
initial={{ x: 0, opacity: 0 }}
|
||||||
|
animate={{ x: 0, opacity: 1 }}
|
||||||
|
exit={{ x: 10, opacity: 0 }}
|
||||||
|
>
|
||||||
|
<Tooltip content="More">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="more"
|
||||||
|
variant="plain"
|
||||||
|
size="md"
|
||||||
|
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
|
||||||
|
onClick={() => onDetailViewSecret(secret)}
|
||||||
|
>
|
||||||
|
<FontAwesomeSymbol
|
||||||
|
symbolName={FontAwesomeSpriteName.More}
|
||||||
|
className="h-5 w-4"
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Delete}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName,
|
||||||
|
secretTags: selectedTagSlugs
|
||||||
|
})}
|
||||||
renderTooltip
|
renderTooltip
|
||||||
allowedLabel="Override"
|
allowedLabel="Delete"
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="override-value"
|
ariaLabel="delete-value"
|
||||||
isDisabled={!isAllowed}
|
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
colorSchema="danger"
|
||||||
onClick={handleOverrideClick}
|
size="md"
|
||||||
className={twMerge(
|
className="p-0 opacity-0 group-hover:opacity-100"
|
||||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5",
|
onClick={() => onDeleteSecret(secret)}
|
||||||
isOverriden && "w-5 text-primary"
|
isDisabled={!isAllowed}
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<FontAwesomeSymbol
|
<FontAwesomeSymbol
|
||||||
symbolName={FontAwesomeSpriteName.Override}
|
symbolName={FontAwesomeSpriteName.Close}
|
||||||
className="h-3.5 w-3.5"
|
className="h-5 w-4"
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
</motion.div>
|
||||||
{!isOverriden && (
|
) : (
|
||||||
|
<motion.div
|
||||||
|
key="options-save"
|
||||||
|
className="flex h-10 flex-shrink-0 items-center space-x-4 px-3"
|
||||||
|
initial={{ x: -10, opacity: 0 }}
|
||||||
|
animate={{ x: 0, opacity: 1 }}
|
||||||
|
exit={{ x: -10, opacity: 0 }}
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
Object.keys(errors || {}).length
|
||||||
|
? Object.entries(errors)
|
||||||
|
.map(([key, { message }]) => `Field ${key}: ${message}`)
|
||||||
|
.join("\n")
|
||||||
|
: "Save"
|
||||||
|
}
|
||||||
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
ariaLabel="more"
|
||||||
|
variant="plain"
|
||||||
|
type="submit"
|
||||||
|
size="md"
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
"p-0 text-primary opacity-0 group-hover:opacity-100",
|
||||||
Boolean(secretReminderRepeatDays) && "w-5 text-primary"
|
isDirty && "opacity-100"
|
||||||
)}
|
)}
|
||||||
variant="plain"
|
isDisabled={isSubmitting || Boolean(errors.key)}
|
||||||
size="md"
|
|
||||||
ariaLabel="add-reminder"
|
|
||||||
onClick={() => setCreateReminderFormOpen.on()}
|
|
||||||
>
|
>
|
||||||
<Tooltip
|
{isSubmitting ? (
|
||||||
content={
|
<Spinner className="m-0 h-4 w-4 p-0" />
|
||||||
secretReminderRepeatDays && secretReminderRepeatDays > 0
|
) : (
|
||||||
? `Every ${secretReminderRepeatDays} day${
|
|
||||||
Number(secretReminderRepeatDays) > 1 ? "s" : ""
|
|
||||||
}
|
|
||||||
`
|
|
||||||
: "Reminder"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<FontAwesomeSymbol
|
<FontAwesomeSymbol
|
||||||
className="h-3.5 w-3.5"
|
symbolName={FontAwesomeSpriteName.Check}
|
||||||
symbolName={FontAwesomeSpriteName.Clock}
|
className={twMerge(
|
||||||
|
"h-4 w-4 text-primary",
|
||||||
|
Boolean(Object.keys(errors || {}).length) && "text-red"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Popover>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Edit}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
|
||||||
<IconButton
|
|
||||||
className={twMerge(
|
|
||||||
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
|
|
||||||
hasComment && "w-5 text-primary"
|
|
||||||
)}
|
|
||||||
variant="plain"
|
|
||||||
size="md"
|
|
||||||
ariaLabel="add-comment"
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<Tooltip content="Comment">
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
symbolName={FontAwesomeSpriteName.Comment}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
</PopoverTrigger>
|
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip content="Cancel">
|
||||||
<IconButton
|
<IconButton
|
||||||
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6"
|
ariaLabel="more"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="md"
|
size="md"
|
||||||
ariaLabel="share-secret"
|
className={twMerge(
|
||||||
onClick={handleSecretShare}
|
"p-0 opacity-0 group-hover:opacity-100",
|
||||||
>
|
isDirty && "opacity-100"
|
||||||
<Tooltip content="Share Secret">
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
className="h-3.5 w-3.5"
|
|
||||||
symbolName={FontAwesomeSpriteName.ShareSecret}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
</IconButton>
|
|
||||||
<PopoverContent
|
|
||||||
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
|
|
||||||
sticky="always"
|
|
||||||
>
|
|
||||||
<FormControl label="Comment" className="mb-0">
|
|
||||||
<TextArea
|
|
||||||
className="border border-mineshaft-600 text-sm"
|
|
||||||
rows={8}
|
|
||||||
cols={30}
|
|
||||||
{...register("comment")}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AnimatePresence exitBeforeEnter>
|
|
||||||
{!isDirty ? (
|
|
||||||
<motion.div
|
|
||||||
key="options"
|
|
||||||
className="flex h-10 flex-shrink-0 items-center space-x-4 px-[0.64rem]"
|
|
||||||
initial={{ x: 0, opacity: 0 }}
|
|
||||||
animate={{ x: 0, opacity: 1 }}
|
|
||||||
exit={{ x: 10, opacity: 0 }}
|
|
||||||
>
|
|
||||||
<Tooltip content="More">
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="more"
|
|
||||||
variant="plain"
|
|
||||||
size="md"
|
|
||||||
className="h-5 w-4 p-0 opacity-0 group-hover:opacity-100"
|
|
||||||
onClick={() => onDetailViewSecret(secret)}
|
|
||||||
>
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
symbolName={FontAwesomeSpriteName.More}
|
|
||||||
className="h-5 w-4"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<ProjectPermissionCan
|
|
||||||
I={ProjectPermissionActions.Delete}
|
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
secretName,
|
|
||||||
secretTags: selectedTagSlugs
|
|
||||||
})}
|
|
||||||
renderTooltip
|
|
||||||
allowedLabel="Delete"
|
|
||||||
>
|
|
||||||
{(isAllowed) => (
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="delete-value"
|
|
||||||
variant="plain"
|
|
||||||
colorSchema="danger"
|
|
||||||
size="md"
|
|
||||||
className="p-0 opacity-0 group-hover:opacity-100"
|
|
||||||
onClick={() => onDeleteSecret(secret)}
|
|
||||||
isDisabled={!isAllowed}
|
|
||||||
>
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
symbolName={FontAwesomeSpriteName.Close}
|
|
||||||
className="h-5 w-4"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
onClick={() => reset()}
|
||||||
</motion.div>
|
isDisabled={isSubmitting}
|
||||||
) : (
|
|
||||||
<motion.div
|
|
||||||
key="options-save"
|
|
||||||
className="flex h-10 flex-shrink-0 items-center space-x-4 px-3"
|
|
||||||
initial={{ x: -10, opacity: 0 }}
|
|
||||||
animate={{ x: 0, opacity: 1 }}
|
|
||||||
exit={{ x: -10, opacity: 0 }}
|
|
||||||
>
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
Object.keys(errors || {}).length
|
|
||||||
? Object.entries(errors)
|
|
||||||
.map(([key, { message }]) => `Field ${key}: ${message}`)
|
|
||||||
.join("\n")
|
|
||||||
: "Save"
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<IconButton
|
<FontAwesomeSymbol
|
||||||
ariaLabel="more"
|
symbolName={FontAwesomeSpriteName.Close}
|
||||||
variant="plain"
|
className="h-4 w-4 text-primary"
|
||||||
type="submit"
|
/>
|
||||||
size="md"
|
</IconButton>
|
||||||
className={twMerge(
|
</Tooltip>
|
||||||
"p-0 text-primary opacity-0 group-hover:opacity-100",
|
</motion.div>
|
||||||
isDirty && "opacity-100"
|
)}
|
||||||
)}
|
</AnimatePresence>
|
||||||
isDisabled={isSubmitting || Boolean(errors.key)}
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<Spinner className="m-0 h-4 w-4 p-0" />
|
|
||||||
) : (
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
symbolName={FontAwesomeSpriteName.Check}
|
|
||||||
className={twMerge(
|
|
||||||
"h-4 w-4 text-primary",
|
|
||||||
Boolean(Object.keys(errors || {}).length) && "text-red"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip content="Cancel">
|
|
||||||
<IconButton
|
|
||||||
ariaLabel="more"
|
|
||||||
variant="plain"
|
|
||||||
size="md"
|
|
||||||
className={twMerge(
|
|
||||||
"p-0 opacity-0 group-hover:opacity-100",
|
|
||||||
isDirty && "opacity-100"
|
|
||||||
)}
|
|
||||||
onClick={() => reset()}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
<FontAwesomeSymbol
|
|
||||||
symbolName={FontAwesomeSpriteName.Close}
|
|
||||||
className="h-4 w-4 text-primary"
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
faEllipsis,
|
faEllipsis,
|
||||||
faKey,
|
faKey,
|
||||||
faLock,
|
faLock,
|
||||||
|
faProjectDiagram,
|
||||||
faShare,
|
faShare,
|
||||||
faTags
|
faTags
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
@@ -73,7 +74,8 @@ export enum FontAwesomeSpriteName {
|
|||||||
CheckedCircle = "check-circle",
|
CheckedCircle = "check-circle",
|
||||||
ReplicatedSecretKey = "secret-replicated",
|
ReplicatedSecretKey = "secret-replicated",
|
||||||
ShareSecret = "share-secret",
|
ShareSecret = "share-secret",
|
||||||
KeyLock = "key-lock"
|
KeyLock = "key-lock",
|
||||||
|
SecretReferenceTree = "secret-reference-tree"
|
||||||
}
|
}
|
||||||
|
|
||||||
// this is an optimization technique
|
// this is an optimization technique
|
||||||
@@ -91,5 +93,6 @@ export const FontAwesomeSpriteSymbols = [
|
|||||||
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle },
|
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle },
|
||||||
{ 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 }
|
||||||
];
|
];
|
||||||
|
|||||||
-96
@@ -1,96 +0,0 @@
|
|||||||
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
|
||||||
import { twMerge } from "tailwind-merge";
|
|
||||||
|
|
||||||
import { FormControl, SecretInput, Spinner, Tooltip } from "@app/components/v2";
|
|
||||||
import { useWorkspace } from "@app/context";
|
|
||||||
import { useGetSecretReferenceTree } from "@app/hooks/api";
|
|
||||||
import { SecretV3RawSanitized, TSecretReferenceTraceNode } from "@app/hooks/api/types";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secret: SecretV3RawSanitized;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SecretReferenceNode = ({
|
|
||||||
node,
|
|
||||||
isRoot
|
|
||||||
}: {
|
|
||||||
node: TSecretReferenceTraceNode;
|
|
||||||
isRoot?: boolean;
|
|
||||||
}) => (
|
|
||||||
<div>
|
|
||||||
{isRoot ? (
|
|
||||||
<FormControl label="Reference value" className="sticky top-0 mb-0 bg-mineshaft-800">
|
|
||||||
<SecretInput
|
|
||||||
key="value-overriden"
|
|
||||||
isReadOnly
|
|
||||||
value={node.value}
|
|
||||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
) : (
|
|
||||||
<div className="relative bg-bunker-700 py-2 text-sm">
|
|
||||||
<div className="tree-line flex items-center">
|
|
||||||
{`${node.environment}${
|
|
||||||
node.secretPath === "/" ? "" : node.secretPath.split("/").join(".")
|
|
||||||
}.${node.key}`}
|
|
||||||
<Tooltip className="break-words" content={node.value}>
|
|
||||||
<span
|
|
||||||
className={twMerge("ml-1 px-1 text-xs text-gray-400", !node.value && "text-red-400")}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon icon={node.value ? faEye : faEyeSlash} />
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{node.children.length > 0 && (
|
|
||||||
<div className="bg-bunker-700 pl-6 transition-all hover:border-gray-400">
|
|
||||||
{node.children.map((el, index) => (
|
|
||||||
<SecretReferenceNode node={el} key={`node-${node.key}-${index + 1}`} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const SecretReferenceTree = ({ secretPath, environment, secret }: Props) => {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
|
||||||
const projectId = currentWorkspace?.id || "";
|
|
||||||
|
|
||||||
const { data, isLoading } = useGetSecretReferenceTree({
|
|
||||||
secretPath,
|
|
||||||
environmentSlug: environment,
|
|
||||||
projectId,
|
|
||||||
secretKey: secret?.key
|
|
||||||
});
|
|
||||||
|
|
||||||
const tree = data?.tree;
|
|
||||||
const secretValue = data?.value;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center py-4">
|
|
||||||
<Spinner size="xs" />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<FormControl label="Expanded value">
|
|
||||||
<SecretInput
|
|
||||||
key="value-overriden"
|
|
||||||
isReadOnly
|
|
||||||
value={secretValue}
|
|
||||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<div className="max-h-96 overflow-auto rounded">
|
|
||||||
{tree && <SecretReferenceNode node={tree} isRoot />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+79
@@ -0,0 +1,79 @@
|
|||||||
|
/* credits: https://iamkate.com/code/tree-views/ */
|
||||||
|
.tree {
|
||||||
|
--spacing: 1.5rem;
|
||||||
|
--radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree li {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding-left: calc(2 * var(--spacing) - var(--radius) - 2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul {
|
||||||
|
margin-left: calc(var(--radius) - var(--spacing));
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul li {
|
||||||
|
border-left: 2px solid #ddd;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul li:last-child {
|
||||||
|
border-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree ul li::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--spacing) / -1);
|
||||||
|
left: -2px;
|
||||||
|
width: calc(var(--spacing) + 2px);
|
||||||
|
height: calc(var(--spacing) + 13px);
|
||||||
|
border: solid #ddd;
|
||||||
|
border-width: 0 0 2px 2px;
|
||||||
|
transition: all 200ms linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree summary {
|
||||||
|
display: block;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree summary::marker,
|
||||||
|
.tree summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree summary:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree summary:focus-visible {
|
||||||
|
outline: 1px dotted #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree li::after,
|
||||||
|
.tree summary::before {
|
||||||
|
content: "";
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--spacing) / 2 - var(--radius));
|
||||||
|
left: calc(var(--spacing) - var(--radius) - 1px);
|
||||||
|
width: calc(2 * var(--radius));
|
||||||
|
height: calc(2 * var(--radius));
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree summary::before {
|
||||||
|
z-index: 1;
|
||||||
|
background: #ddd 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree details[open] > summary::before {
|
||||||
|
background-position: calc(-2 * var(--radius)) 0;
|
||||||
|
}
|
||||||
+114
@@ -0,0 +1,114 @@
|
|||||||
|
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
import { FormControl, FormLabel, SecretInput, Spinner, Tooltip } from "@app/components/v2";
|
||||||
|
import { useWorkspace } from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
|
import { useGetSecretReferenceTree } from "@app/hooks/api";
|
||||||
|
import { TSecretReferenceTraceNode } from "@app/hooks/api/types";
|
||||||
|
|
||||||
|
import style from "./SecretReferenceDetails.module.css";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
secretKey: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const INTERPOLATION_SYNTAX_REG = /\${([^}]+)}/;
|
||||||
|
export const hasSecretReference = (value: string | undefined) =>
|
||||||
|
value ? INTERPOLATION_SYNTAX_REG.test(value) : false;
|
||||||
|
|
||||||
|
export const SecretReferenceNode = ({
|
||||||
|
node,
|
||||||
|
isRoot,
|
||||||
|
secretKey
|
||||||
|
}: {
|
||||||
|
node: TSecretReferenceTraceNode;
|
||||||
|
isRoot?: boolean;
|
||||||
|
secretKey?: string;
|
||||||
|
}) => {
|
||||||
|
const [isOpen, setIsOpen] = useToggle();
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
<details
|
||||||
|
open={isOpen}
|
||||||
|
onToggle={(el) => {
|
||||||
|
el.preventDefault();
|
||||||
|
el.stopPropagation();
|
||||||
|
setIsOpen.toggle();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<summary className={twMerge(node.children.length > 0 && !isOpen && "text-primary")}>
|
||||||
|
{isRoot
|
||||||
|
? secretKey
|
||||||
|
: `${node.environment}${
|
||||||
|
node.secretPath === "/" ? "" : node.secretPath.split("/").join(".")
|
||||||
|
}.${node.key}`}
|
||||||
|
<Tooltip className="max-w-md break-words" content={node.value}>
|
||||||
|
<span
|
||||||
|
className={twMerge("ml-1 px-1 text-xs text-gray-400", !node.value && "text-red-400")}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={node.value ? faEye : faEyeSlash} />
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
</summary>
|
||||||
|
{node.children.length > 0 && (
|
||||||
|
<ul>
|
||||||
|
{node.children.map((el, index) => (
|
||||||
|
<SecretReferenceNode node={el} key={`${el.key}-${index + 1}`} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SecretReferenceTree = ({ secretPath, environment, secretKey }: Props) => {
|
||||||
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
const projectId = currentWorkspace?.id || "";
|
||||||
|
|
||||||
|
const { data, isLoading } = useGetSecretReferenceTree({
|
||||||
|
secretPath,
|
||||||
|
environmentSlug: environment,
|
||||||
|
projectId,
|
||||||
|
secretKey
|
||||||
|
});
|
||||||
|
|
||||||
|
const tree = data?.tree;
|
||||||
|
const secretValue = data?.value;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<Spinner size="xs" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<FormControl label="Expanded value">
|
||||||
|
<SecretInput
|
||||||
|
key="value-overriden"
|
||||||
|
isReadOnly
|
||||||
|
value={secretValue}
|
||||||
|
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormLabel className="mb-2" label="Reference Tree" />
|
||||||
|
<div className="relative max-h-96 overflow-auto rounded bg-bunker-700 py-2 pt-6 text-sm">
|
||||||
|
{tree && (
|
||||||
|
<ul className={style.tree}>
|
||||||
|
<SecretReferenceNode node={tree} isRoot secretKey={secretKey} />
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-sm text-gray-400">
|
||||||
|
Click on a secret key to view its corresponding sub references.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { hasSecretReference,SecretReferenceTree } from "./SecretReferenceDetails";
|
||||||
+61
-3
@@ -1,17 +1,34 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { subject } from "@casl/ability";
|
import { subject } from "@casl/ability";
|
||||||
import { faCheck, faCopy, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
faCheck,
|
||||||
|
faCopy,
|
||||||
|
faProjectDiagram,
|
||||||
|
faTrash,
|
||||||
|
faXmark
|
||||||
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
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 { DeleteActionModal, IconButton, Tooltip } from "@app/components/v2";
|
import {
|
||||||
|
DeleteActionModal,
|
||||||
|
IconButton,
|
||||||
|
Modal,
|
||||||
|
ModalContent,
|
||||||
|
ModalTrigger,
|
||||||
|
Tooltip
|
||||||
|
} from "@app/components/v2";
|
||||||
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
|
||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import { SecretType } from "@app/hooks/api/types";
|
import { SecretType } from "@app/hooks/api/types";
|
||||||
|
import {
|
||||||
|
hasSecretReference,
|
||||||
|
SecretReferenceTree
|
||||||
|
} from "@app/views/SecretMainPage/components/SecretReferenceDetails";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
defaultValue?: string | null;
|
defaultValue?: string | null;
|
||||||
@@ -143,7 +160,7 @@ export const SecretEditRow = ({
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"flex w-16 justify-center space-x-3 pl-2 transition-all",
|
"flex w-24 justify-center space-x-3 pl-2 transition-all",
|
||||||
isImportedSecret && "pointer-events-none opacity-0"
|
isImportedSecret && "pointer-events-none opacity-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -203,6 +220,47 @@ export const SecretEditRow = ({
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Read}
|
||||||
|
a={ProjectPermissionSub.Secrets}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Modal>
|
||||||
|
<ModalTrigger asChild>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
hasSecretReference(defaultValue || "")
|
||||||
|
? "Secret Reference Tree"
|
||||||
|
: "Secret does not contain references"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="reference-tree"
|
||||||
|
className="h-full"
|
||||||
|
isDisabled={!hasSecretReference(defaultValue || "") || !isAllowed}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faProjectDiagram} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</ModalTrigger>
|
||||||
|
<ModalContent
|
||||||
|
title="Secret Reference Tree"
|
||||||
|
subTitle="Visual breakdown of secrets referenced by this secret."
|
||||||
|
>
|
||||||
|
<SecretReferenceTree
|
||||||
|
secretPath={secretPath}
|
||||||
|
environment={environment}
|
||||||
|
secretKey={secretName}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
<ProjectPermissionCan
|
<ProjectPermissionCan
|
||||||
I={ProjectPermissionActions.Delete}
|
I={ProjectPermissionActions.Delete}
|
||||||
a={subject(ProjectPermissionSub.Secrets, {
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
|||||||
Reference in New Issue
Block a user