feat: added reference tree to overview and secret main page, changed to better graph structure

This commit is contained in:
=
2024-10-25 13:18:41 +05:30
parent 35826c288e
commit 203422c131
9 changed files with 662 additions and 549 deletions
@@ -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);
@@ -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,20 +201,6 @@ export const SecretItem = memo(
}; };
return ( return (
<>
<CreateReminderForm
repeatDays={secretReminderRepeatDays}
note={secretReminderNote}
isOpen={createReminderFormOpen}
onOpenChange={(_, data) => {
setCreateReminderFormOpen.toggle();
if (data) {
setValue("reminderRepeatDays", data.days, { shouldDirty: true });
setValue("reminderNote", data.note, { shouldDirty: true });
}
}}
/>
<form onSubmit={handleSubmit(handleFormSubmit)}> <form onSubmit={handleSubmit(handleFormSubmit)}>
<div <div
className={twMerge( className={twMerge(
@@ -321,6 +306,47 @@ export const SecretItem = memo(
/> />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath, secretName, secretTags: selectedTagSlugs})}
>
{(isAllowed) => (
<Modal>
<ModalTrigger asChild>
<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="reference-tree"
isDisabled={!isAllowed || !hasSecretReference(secret?.value)}
>
<Tooltip
content={
hasSecretReference(secret?.value)
? "Secret Reference Tree"
: "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> <DropdownMenu>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
@@ -432,36 +458,6 @@ export const SecretItem = memo(
</IconButton> </IconButton>
)} )}
</ProjectPermissionCan> </ProjectPermissionCan>
{!isOverriden && (
<IconButton
className={twMerge(
"w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5 data-[state=open]:w-6",
Boolean(secretReminderRepeatDays) && "w-5 text-primary"
)}
variant="plain"
size="md"
ariaLabel="add-reminder"
onClick={() => setCreateReminderFormOpen.on()}
>
<Tooltip
content={
secretReminderRepeatDays && secretReminderRepeatDays > 0
? `Every ${secretReminderRepeatDays} day${
Number(secretReminderRepeatDays) > 1 ? "s" : ""
}
`
: "Reminder"
}
>
<FontAwesomeSymbol
className="h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.Clock}
/>
</Tooltip>
</IconButton>
)}
<Popover> <Popover>
<ProjectPermissionCan <ProjectPermissionCan
I={ProjectPermissionActions.Edit} I={ProjectPermissionActions.Edit}
@@ -641,7 +637,6 @@ export const SecretItem = memo(
</div> </div>
</div> </div>
</form> </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 }
]; ];
@@ -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>
);
};
@@ -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;
}
@@ -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";
@@ -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, {