mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 06:34:57 +00:00
feat(dashboard-v3): implemented new the dashboard with v3 support
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { faMinus, faPlus, faShuffle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { Button, Input } from "@app/components/v2";
|
||||
|
||||
type Props = {
|
||||
onGenerate: (val: string) => void;
|
||||
};
|
||||
|
||||
export const GenRandomNumber = ({ onGenerate }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [value, setValue] = useState(32);
|
||||
|
||||
const onGenerateRandomHex = () => {
|
||||
const rand = crypto.randomBytes(value).toString("hex");
|
||||
onGenerate(rand);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<Button
|
||||
className="rounded-r-none"
|
||||
colorSchema="secondary"
|
||||
leftIcon={<FontAwesomeIcon icon={faShuffle} />}
|
||||
onClick={onGenerateRandomHex}
|
||||
>
|
||||
{t("dashboard.sidebar.generate-random-hex")}
|
||||
</Button>
|
||||
<div className="rounded-r-md bg-mineshaft p-2">
|
||||
<div className="flex rounded-md border-mineshaft-400 bg-bunker-800 p-1 px-2">
|
||||
<button type="button" onClick={() => setValue((val) => val - 1)}>
|
||||
<FontAwesomeIcon icon={faMinus} size="xs" />
|
||||
</button>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-8 p-0 text-center"
|
||||
value={value}
|
||||
variant="plain"
|
||||
onChange={(e) => setValue(parseInt(e.target.value, 10))}
|
||||
/>
|
||||
<button type="button" onClick={() => setValue((val) => val + 1)}>
|
||||
<FontAwesomeIcon icon={faPlus} size="xs" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,389 @@
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import {
|
||||
faCheckCircle,
|
||||
faCircle,
|
||||
faCircleDot,
|
||||
faPlus,
|
||||
faTag
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { format } from "date-fns";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
SecretInput,
|
||||
Switch,
|
||||
Tag,
|
||||
TextArea
|
||||
} from "@app/components/v2";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
|
||||
import { useGetSecretVersion } from "@app/hooks/api";
|
||||
import { DecryptedSecret, UserWsKeyPair, WsTag } from "@app/hooks/api/types";
|
||||
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
isOpen?: boolean;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
onToggle: (isOpen: boolean) => void;
|
||||
onClose: () => void;
|
||||
secret: DecryptedSecret;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
onDeleteSecret: () => void;
|
||||
onSaveSecret: (
|
||||
orgSec: DecryptedSecret,
|
||||
modSec: Omit<DecryptedSecret, "tags"> & { tags: { _id: string }[] }
|
||||
) => Promise<void>;
|
||||
tags: WsTag[];
|
||||
onCreateTag: () => void;
|
||||
};
|
||||
|
||||
export const SecretDetailSidebar = ({
|
||||
isOpen,
|
||||
onToggle,
|
||||
decryptFileKey,
|
||||
secret,
|
||||
onDeleteSecret,
|
||||
onSaveSecret,
|
||||
tags,
|
||||
onCreateTag,
|
||||
environment,
|
||||
secretPath
|
||||
}: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const {
|
||||
register,
|
||||
control,
|
||||
watch,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
reset,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
resolver: zodResolver(formSchema),
|
||||
values: secret
|
||||
});
|
||||
const permission = useProjectPermission();
|
||||
const cannotEditSecret = permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
const isReadOnly =
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
) && cannotEditSecret;
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "tags"
|
||||
});
|
||||
const selectedTags = watch("tags", []);
|
||||
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr._id]: true }),
|
||||
{}
|
||||
);
|
||||
|
||||
const overrideAction = watch("overrideAction");
|
||||
const isOverridden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
|
||||
const { data: secretVersion } = useGetSecretVersion({
|
||||
limit: 10,
|
||||
offset: 0,
|
||||
secretId: secret?._id,
|
||||
decryptFileKey
|
||||
});
|
||||
|
||||
const handleOverrideClick = () => {
|
||||
if (isOverridden) {
|
||||
// override need not be flagged delete if it was never saved in server
|
||||
// meaning a new unsaved personal secret but user toggled back later
|
||||
const isUnsavedOverride = !secret.idOverride;
|
||||
setValue(
|
||||
"overrideAction",
|
||||
isUnsavedOverride ? secret?.overrideAction : SecretActionType.Deleted,
|
||||
{
|
||||
shouldDirty: !isUnsavedOverride
|
||||
}
|
||||
);
|
||||
setValue("valueOverride", secret?.valueOverride, { shouldDirty: !isUnsavedOverride });
|
||||
} else {
|
||||
setValue("overrideAction", SecretActionType.Modified, { shouldDirty: true });
|
||||
setValue("valueOverride", "", { shouldDirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagSelect = (tag: WsTag) => {
|
||||
if (selectedTagsGroupById?.[tag._id]) {
|
||||
const tagPos = selectedTags.findIndex(({ _id }) => _id === tag._id);
|
||||
if (tagPos !== -1) {
|
||||
remove(tagPos);
|
||||
}
|
||||
} else {
|
||||
append(tag);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
await onSaveSecret(secret, { ...secret, ...data });
|
||||
};
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
onOpenChange={(state) => {
|
||||
if (isOpen && isDirty) {
|
||||
if (
|
||||
// eslint-disable-next-line no-alert
|
||||
window.confirm("You have edited the secret. Are you sure you want to reset the change?")
|
||||
) {
|
||||
onToggle(false);
|
||||
reset();
|
||||
} else return;
|
||||
}
|
||||
onToggle(state);
|
||||
}}
|
||||
isOpen={isOpen}
|
||||
>
|
||||
<DrawerContent title="Secret">
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)} className="h-full">
|
||||
<div className="flex flex-col h-full">
|
||||
<FormControl label="Key">
|
||||
<Input isDisabled {...register("key")} />
|
||||
</FormControl>
|
||||
<Controller
|
||||
name="value"
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value">
|
||||
<SecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
key="secret-value"
|
||||
isDisabled={isOverridden}
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
<div className="mb-2">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
isDisabled={!isAllowed}
|
||||
id="personal-override"
|
||||
onCheckedChange={handleOverrideClick}
|
||||
isChecked={isOverridden}
|
||||
>
|
||||
Override with a personal value
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
{isOverridden && (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormControl label="Value Override">
|
||||
<SecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div className="my-2 mb-6 border-t border-mineshaft-600 pt-4">
|
||||
<Controller
|
||||
control={control}
|
||||
name="skipMultilineEncoding"
|
||||
render={({ field: { value, onChange, onBlur } }) => (
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Switch
|
||||
id="skipmultiencoding-option"
|
||||
onCheckedChange={onChange}
|
||||
isChecked={value}
|
||||
onBlur={onBlur}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Disable multi line encoding
|
||||
</Switch>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormControl label="Tags" className="">
|
||||
<div className="overflow-hidden grid gap-2 grid-flow-col auto-cols-min pt-2">
|
||||
{fields.map(({ tagColor, id: formId, name, _id }) => (
|
||||
<Tag
|
||||
className="flex items-center space-x-2 w-min"
|
||||
key={formId}
|
||||
onClose={() => {
|
||||
if (cannotEditSecret) {
|
||||
createNotification({ type: "error", text: "Access denied" });
|
||||
return;
|
||||
}
|
||||
const tag = tags?.find(({ _id: id }) => id === _id);
|
||||
if (tag) handleTagSelect(tag);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
<div className="text-sm">{name}</div>
|
||||
</Tag>
|
||||
))}
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(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>Apply tags to this secrets</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { _id: tagId, name, tagColor } = 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="w-2 h-2 rounded-full mr-2"
|
||||
style={{ background: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Create}
|
||||
a={ProjectPermissionSub.Tags}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuItem>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||
onClick={onCreateTag}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormControl label="Comments & Notes">
|
||||
<TextArea
|
||||
className="border border-mineshaft-600 text-sm"
|
||||
{...register("comment")}
|
||||
readOnly={isReadOnly}
|
||||
rows={5}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="dark mb-4 text-sm text-bunker-300 flex-grow">
|
||||
<div className="mb-2">Version History</div>
|
||||
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
|
||||
{secretVersion?.map(({ createdAt, value, id }, i) => (
|
||||
<div key={id} className="flex flex-col space-y-1">
|
||||
<div className="flex items-center space-x-2">
|
||||
<div>
|
||||
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
|
||||
</div>
|
||||
<div>{format(new Date(createdAt), "Pp")}</div>
|
||||
</div>
|
||||
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
|
||||
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
|
||||
<div className="break-all font-mono">{value}</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex space-x-4 items-center">
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
isFullWidth
|
||||
type="submit"
|
||||
isDisabled={isSubmitting || !isDirty || !isAllowed}
|
||||
isLoading={isSubmitting}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,481 @@
|
||||
import { memo, useEffect } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { subject } from "@casl/ability";
|
||||
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
|
||||
import {
|
||||
faCheck,
|
||||
faClose,
|
||||
faCodeBranch,
|
||||
faComment,
|
||||
faCopy,
|
||||
faEllipsis,
|
||||
faKey,
|
||||
faSave,
|
||||
faTag,
|
||||
faTags
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { ProjectPermissionCan } from "@app/components/permissions";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuTrigger,
|
||||
FormControl,
|
||||
IconButton,
|
||||
Input,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
SecretInput,
|
||||
Spinner,
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
useProjectPermission,
|
||||
useWorkspace
|
||||
} from "@app/context";
|
||||
import { useToggle } from "@app/hooks";
|
||||
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
|
||||
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
|
||||
|
||||
type Props = {
|
||||
secret: DecryptedSecret;
|
||||
onSaveSecret: (
|
||||
orgSec: DecryptedSecret,
|
||||
modSec: Omit<DecryptedSecret, "tags"> & { tags: { _id: string }[] }
|
||||
) => Promise<void>;
|
||||
onDeleteSecret: (sec: DecryptedSecret) => void;
|
||||
onDetailViewSecret: (sec: DecryptedSecret) => void;
|
||||
isVisible?: boolean;
|
||||
isSelected?: boolean;
|
||||
onToggleSecretSelect: (id: string) => void;
|
||||
tags: WsTag[];
|
||||
onCreateTag: () => void;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
|
||||
export const SecretItem = memo(
|
||||
({
|
||||
secret,
|
||||
onSaveSecret,
|
||||
onDeleteSecret,
|
||||
onDetailViewSecret,
|
||||
isVisible,
|
||||
isSelected,
|
||||
tags = [],
|
||||
onCreateTag,
|
||||
onToggleSecretSelect,
|
||||
environment,
|
||||
secretPath
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const permission = useProjectPermission();
|
||||
const isReadOnly =
|
||||
permission.can(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
) &&
|
||||
permission.cannot(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
control,
|
||||
register,
|
||||
watch,
|
||||
setValue,
|
||||
reset,
|
||||
getValues,
|
||||
formState: { isDirty, isSubmitting }
|
||||
} = useForm<TFormSchema>({
|
||||
defaultValues: secret,
|
||||
values: secret,
|
||||
resolver: zodResolver(formSchema)
|
||||
});
|
||||
|
||||
const overrideAction = watch("overrideAction");
|
||||
const hasComment = Boolean(watch("comment"));
|
||||
|
||||
const selectedTags = watch("tags", []);
|
||||
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr._id]: true }),
|
||||
{}
|
||||
);
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control,
|
||||
name: "tags"
|
||||
});
|
||||
|
||||
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
|
||||
useEffect(() => {
|
||||
let timer: NodeJS.Timeout;
|
||||
if (isSecValueCopied) {
|
||||
timer = setTimeout(() => setIsSecValueCopied.off(), 2000);
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSecValueCopied]);
|
||||
|
||||
const isOverriden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
const hasTagsApplied = Boolean(fields.length);
|
||||
|
||||
const handleOverrideClick = () => {
|
||||
if (isOverriden) {
|
||||
// override need not be flagged delete if it was never saved in server
|
||||
// meaning a new unsaved personal secret but user toggled back later
|
||||
const isUnsavedOverride = !secret.idOverride;
|
||||
setValue(
|
||||
"overrideAction",
|
||||
isUnsavedOverride ? secret?.overrideAction : SecretActionType.Deleted,
|
||||
{
|
||||
shouldDirty: !isUnsavedOverride
|
||||
}
|
||||
);
|
||||
setValue("valueOverride", secret?.valueOverride, { shouldDirty: !isUnsavedOverride });
|
||||
} else {
|
||||
setValue("overrideAction", SecretActionType.Modified, { shouldDirty: true });
|
||||
setValue("valueOverride", "", { shouldDirty: true });
|
||||
}
|
||||
};
|
||||
|
||||
const handleFormSubmit = async (data: TFormSchema) => {
|
||||
await onSaveSecret(secret, { ...secret, ...data });
|
||||
};
|
||||
|
||||
const handleTagSelect = (tag: WsTag) => {
|
||||
if (selectedTagsGroupById?.[tag._id]) {
|
||||
const tagPos = selectedTags.findIndex(({ _id }) => _id === tag._id);
|
||||
if (tagPos !== -1) {
|
||||
remove(tagPos);
|
||||
}
|
||||
} else {
|
||||
append(tag);
|
||||
}
|
||||
};
|
||||
|
||||
const copyTokenToClipboard = () => {
|
||||
const [overrideValue, value] = getValues(["value", "valueOverride"]);
|
||||
navigator.clipboard.writeText((overrideValue || value) as string);
|
||||
setIsSecValueCopied.on();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleFormSubmit)}>
|
||||
<div
|
||||
className={twMerge(
|
||||
"shadow-none border-b border-mineshaft-600 bg-mineshaft-800 hover:bg-mineshaft-700",
|
||||
isDirty && "border-primary-400/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex group">
|
||||
<div
|
||||
className={twMerge(
|
||||
"flex items-center justify-center w-11 px-4 py-3 h-11",
|
||||
isDirty && "text-primary"
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
id={`checkbox-${secret._id}`}
|
||||
isChecked={isSelected}
|
||||
onCheckedChange={() => onToggleSecretSelect(secret._id)}
|
||||
className={twMerge("group-hover:flex hidden", isSelected && "flex")}
|
||||
/>
|
||||
<FontAwesomeIcon
|
||||
icon={faKey}
|
||||
className={twMerge("group-hover:hidden block", isSelected && "hidden")}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-80 h-11 flex items-center px-4 py-2 flex-shrink-0">
|
||||
<Controller
|
||||
name="key"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
autoComplete="off"
|
||||
isReadOnly={isReadOnly}
|
||||
autoCapitalization={currentWorkspace?.autoCapitalization}
|
||||
variant="plain"
|
||||
{...field}
|
||||
className="w-full focus:text-bunker-100 hover:ring-primary-400/50 hover:ring-1"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="flex-grow flex items-center border-x border-mineshaft-600 pl-4 pr-2 py-1"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
>
|
||||
{isOverriden ? (
|
||||
<Controller
|
||||
name="valueOverride"
|
||||
key="value-overriden"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SecretInput
|
||||
key="value-overriden"
|
||||
isVisible={isVisible}
|
||||
isReadOnly={isReadOnly}
|
||||
{...field}
|
||||
containerClassName="hover:border-primary-400/50 border border-transparent px-2 py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
name="value"
|
||||
key="secret-value"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<SecretInput
|
||||
isReadOnly={isReadOnly}
|
||||
key="secret-value"
|
||||
isVisible={isVisible}
|
||||
{...field}
|
||||
containerClassName="hover:border-primary-400/50 border border-transparent px-2 py-1.5 rounded-md transition-all group-hover:mr-2"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<div key="actions" className="h-8 flex self-start flex-shrink-0 transition-all">
|
||||
<Tooltip content="Copy secret">
|
||||
<IconButton
|
||||
ariaLabel="copy-value"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className="w-0 group-hover:w-5 group-hover:mr-2 overflow-hidden p-0"
|
||||
onClick={copyTokenToClipboard}
|
||||
>
|
||||
<FontAwesomeIcon icon={isSecValueCopied ? faCheck : faCopy} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<DropdownMenuTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
ariaLabel="tags"
|
||||
variant="plain"
|
||||
size="sm"
|
||||
className={twMerge(
|
||||
"w-0 group-hover:w-5 group-hover:mr-2 overflow-hidden p-0 data-[state=open]:w-5",
|
||||
hasTagsApplied && "w-5 text-primary"
|
||||
)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Tags">
|
||||
<FontAwesomeIcon icon={faTags} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</DropdownMenuTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Apply tags to this secrets</DropdownMenuLabel>
|
||||
{tags.map((tag) => {
|
||||
const { _id: tagId, name, tagColor } = tag;
|
||||
|
||||
const isTagSelected = selectedTagsGroupById?.[tagId];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleTagSelect(tag)}
|
||||
key={tagId}
|
||||
icon={isTagSelected && <FontAwesomeIcon icon={faCheckCircle} />}
|
||||
iconPos="right"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="w-2 h-2 rounded-full mr-2"
|
||||
style={{ background: tagColor || "#bec2c8" }}
|
||||
/>
|
||||
{name}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
<DropdownMenuItem>
|
||||
<Button
|
||||
size="xs"
|
||||
className="w-full"
|
||||
leftIcon={<FontAwesomeIcon icon={faTag} />}
|
||||
onClick={onCreateTag}
|
||||
>
|
||||
Create a tag
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Override"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="override-value"
|
||||
isDisabled={!isAllowed}
|
||||
variant="plain"
|
||||
size="sm"
|
||||
onClick={handleOverrideClick}
|
||||
className={twMerge(
|
||||
"w-0 group-hover:w-5 group-hover:mr-2 overflow-hidden p-0",
|
||||
isOverriden && "w-5 text-primary"
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCodeBranch} />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<Popover>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Edit}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<PopoverTrigger asChild disabled={!isAllowed}>
|
||||
<IconButton
|
||||
className={twMerge(
|
||||
"overflow-hidden w-0 p-0 group-hover:w-5 group-hover:mr-2 data-[state=open]:w-6",
|
||||
hasComment && "w-5 text-primary"
|
||||
)}
|
||||
variant="plain"
|
||||
size="md"
|
||||
ariaLabel="add-comment"
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<Tooltip content="Comment">
|
||||
<FontAwesomeIcon icon={faComment} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
<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="h-10 flex items-center space-x-4 flex-shrink-0 px-3"
|
||||
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="group-hover:opacity-100 opacity-0 p-0"
|
||||
onClick={() => onDetailViewSecret(secret)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsis} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<ProjectPermissionCan
|
||||
I={ProjectPermissionActions.Delete}
|
||||
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
|
||||
renderTooltip
|
||||
allowedLabel="Delete"
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<IconButton
|
||||
ariaLabel="delete-value"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className="group-hover:opacity-100 opacity-0 p-0"
|
||||
onClick={() => onDeleteSecret(secret)}
|
||||
isDisabled={!isAllowed}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="options-save"
|
||||
className="h-10 flex items-center space-x-4 flex-shrink-0 px-3"
|
||||
initial={{ x: -10, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: -10, opacity: 0 }}
|
||||
>
|
||||
<Tooltip content="Save">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
type="submit"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"group-hover:opacity-100 opacity-0 p-0 text-primary",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<Spinner className="w-4 h-4 p-0 m-0" />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faSave} size="lg" className="text-primary" />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip content="Cancel">
|
||||
<IconButton
|
||||
ariaLabel="more"
|
||||
variant="plain"
|
||||
size="md"
|
||||
className={twMerge(
|
||||
"group-hover:opacity-100 opacity-0 p-0",
|
||||
isDirty && "opacity-100"
|
||||
)}
|
||||
onClick={() => reset()}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
<FontAwesomeIcon icon={faClose} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SecretItem.displayName = "SecretItem";
|
||||
@@ -0,0 +1,334 @@
|
||||
import { useCallback } from "react";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||
import { CreateTagModal } from "@app/components/tags/CreateTagModal";
|
||||
import { DeleteActionModal } from "@app/components/v2";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
|
||||
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||
import { DecryptedSecret } from "@app/hooks/api/secrets/types";
|
||||
import { UserWsKeyPair, WsTag } from "@app/hooks/api/types";
|
||||
|
||||
import { Filter, GroupBy, SortDir } from "../../SecretMainPage.types";
|
||||
import { SecretDetailSidebar } from "./SecretDetaiSidebar";
|
||||
import { SecretItem } from "./SecretItem";
|
||||
|
||||
type Props = {
|
||||
secrets?: DecryptedSecret[];
|
||||
environment: string;
|
||||
workspaceId: string;
|
||||
decryptFileKey: UserWsKeyPair;
|
||||
secretPath?: string;
|
||||
filter: Filter;
|
||||
sortDir?: SortDir;
|
||||
tags?: WsTag[];
|
||||
isVisible?: boolean;
|
||||
selectedSecrets: Record<string, boolean>;
|
||||
onToggleSecretSelect: (id: string) => void;
|
||||
};
|
||||
|
||||
const reorderSecretGroupByUnderscore = (secrets: DecryptedSecret[], sortDir: SortDir) => {
|
||||
const groupedSecrets: Record<string, DecryptedSecret[]> = {};
|
||||
secrets.forEach((secret) => {
|
||||
const lastSeperatorIndex = secret.key.lastIndexOf("_");
|
||||
const namespace =
|
||||
lastSeperatorIndex !== -1 ? secret.key.substring(0, lastSeperatorIndex) : "misc";
|
||||
if (!groupedSecrets?.[namespace]) groupedSecrets[namespace] = [];
|
||||
groupedSecrets[namespace].push(secret);
|
||||
});
|
||||
|
||||
return Object.keys(groupedSecrets)
|
||||
.sort((a, b) =>
|
||||
sortDir === SortDir.ASC
|
||||
? a.toLowerCase().localeCompare(b.toLowerCase())
|
||||
: b.toLowerCase().localeCompare(a.toLowerCase())
|
||||
)
|
||||
.map((namespace) => ({ namespace, secrets: groupedSecrets[namespace] }));
|
||||
};
|
||||
|
||||
const reorderSecret = (secrets: DecryptedSecret[], sortDir: SortDir, filter?: GroupBy | null) => {
|
||||
console.log(secrets);
|
||||
if (filter === GroupBy.PREFIX) {
|
||||
return reorderSecretGroupByUnderscore(secrets, sortDir);
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
namespace: "",
|
||||
secrets: secrets?.sort((a, b) =>
|
||||
sortDir === SortDir.ASC
|
||||
? a.key.toLowerCase().localeCompare(b.key.toLowerCase())
|
||||
: b.key.toLowerCase().localeCompare(a.key.toLowerCase())
|
||||
)
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export const filterSecrets = (secrets: DecryptedSecret[], filter: Filter) =>
|
||||
secrets.filter(({ key, value, tags }) => {
|
||||
const isTagFilterActive = Boolean(Object.keys(filter.tags).length);
|
||||
const searchTerm = filter.searchFilter.toLowerCase();
|
||||
return (
|
||||
(!isTagFilterActive || tags.some(({ _id }) => filter.tags?.[_id])) &&
|
||||
(key.toLowerCase().includes(searchTerm) || value.toLowerCase().includes(searchTerm))
|
||||
);
|
||||
});
|
||||
|
||||
export const SecretListView = ({
|
||||
secrets = [],
|
||||
environment,
|
||||
workspaceId,
|
||||
decryptFileKey,
|
||||
secretPath = "/",
|
||||
filter,
|
||||
sortDir = SortDir.ASC,
|
||||
tags: wsTags = [],
|
||||
isVisible,
|
||||
selectedSecrets,
|
||||
onToggleSecretSelect
|
||||
}: Props) => {
|
||||
const { createNotification } = useNotificationContext();
|
||||
const queryClient = useQueryClient();
|
||||
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
|
||||
"deleteSecret",
|
||||
"secretDetail",
|
||||
"createTag"
|
||||
] as const);
|
||||
|
||||
// strip of side effect queries
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3({
|
||||
options: {
|
||||
onSuccess: undefined
|
||||
}
|
||||
});
|
||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3({
|
||||
options: {
|
||||
onSuccess: undefined
|
||||
}
|
||||
});
|
||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3({
|
||||
options: {
|
||||
onSuccess: undefined
|
||||
}
|
||||
});
|
||||
|
||||
const handleSecretOperation = async (
|
||||
operation: "create" | "update" | "delete",
|
||||
type: "shared" | "personal",
|
||||
key: string,
|
||||
{
|
||||
value,
|
||||
comment,
|
||||
tags,
|
||||
skipMultilineEncoding,
|
||||
newKey
|
||||
}: Partial<{
|
||||
value: string;
|
||||
comment: string;
|
||||
tags: string[];
|
||||
skipMultilineEncoding: boolean;
|
||||
newKey: string;
|
||||
}> = {}
|
||||
) => {
|
||||
if (operation === "delete") {
|
||||
await deleteSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretName: key,
|
||||
type
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (operation === "update") {
|
||||
await updateSecretV3({
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretName: key,
|
||||
secretValue: value || "",
|
||||
type,
|
||||
latestFileKey: decryptFileKey,
|
||||
tags,
|
||||
secretComment: comment,
|
||||
skipMultilineEncoding,
|
||||
newSecretName: newKey
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await createSecretV3(
|
||||
{
|
||||
environment,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretName: key,
|
||||
secretValue: value || "",
|
||||
secretComment: "",
|
||||
skipMultilineEncoding,
|
||||
type,
|
||||
latestFileKey: decryptFileKey
|
||||
},
|
||||
{}
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveSecret = useCallback(
|
||||
async (
|
||||
orgSecret: DecryptedSecret,
|
||||
modSecret: Omit<DecryptedSecret, "tags"> & { tags: { _id: string }[] }
|
||||
) => {
|
||||
const { key: oldKey } = orgSecret;
|
||||
const { key, value, overrideAction, idOverride, valueOverride, tags, comment } = modSecret;
|
||||
const hasKeyChanged = oldKey !== key;
|
||||
|
||||
const tagIds = tags.map(({ _id }) => _id);
|
||||
const oldTagIds = orgSecret.tags.map(({ _id }) => _id);
|
||||
const isSameTags = JSON.stringify(tagIds) === JSON.stringify(oldTagIds);
|
||||
const isSharedSecUnchanged =
|
||||
(["key", "value", "comment", "skipMultilineEncoding"] as const).every(
|
||||
(el) => orgSecret[el] === modSecret[el]
|
||||
) && isSameTags;
|
||||
|
||||
try {
|
||||
// personal secret change
|
||||
if (overrideAction === "deleted") await handleSecretOperation("delete", "personal", key);
|
||||
else if (overrideAction && idOverride)
|
||||
await handleSecretOperation("update", "personal", oldKey, {
|
||||
value: valueOverride,
|
||||
newKey: hasKeyChanged ? key : undefined,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
||||
});
|
||||
else if (overrideAction)
|
||||
await handleSecretOperation("create", "personal", key, { value: valueOverride });
|
||||
|
||||
// shared secret change
|
||||
if (!isSharedSecUnchanged)
|
||||
await handleSecretOperation("update", "shared", oldKey, {
|
||||
value,
|
||||
tags: tagIds,
|
||||
comment,
|
||||
newKey: hasKeyChanged ? key : undefined,
|
||||
skipMultilineEncoding: modSecret.skipMultilineEncoding
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
);
|
||||
handlePopUpClose("secretDetail");
|
||||
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully saved secrets"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to save secret"
|
||||
});
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSecretDelete = useCallback(async () => {
|
||||
const { key } = popUp.deleteSecret?.data as DecryptedSecret;
|
||||
try {
|
||||
await handleSecretOperation("delete", "shared", key);
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
);
|
||||
handlePopUpClose("deleteSecret");
|
||||
handlePopUpClose("secretDetail");
|
||||
createNotification({
|
||||
type: "success",
|
||||
text: "Successfully deleted secret"
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to delete secret"
|
||||
});
|
||||
}
|
||||
}, [(popUp.deleteSecret?.data as DecryptedSecret)?.key]);
|
||||
|
||||
// for optimization on minimise re-rendering of secret items
|
||||
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);
|
||||
const onDeleteSecret = useCallback(
|
||||
(sec: DecryptedSecret) => handlePopUpOpen("deleteSecret", sec),
|
||||
[]
|
||||
);
|
||||
const onDetailViewSecret = useCallback(
|
||||
(sec: DecryptedSecret) => handlePopUpOpen("secretDetail", sec),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{reorderSecret(secrets, sortDir, filter.groupBy).map(
|
||||
({ namespace, secrets: groupedSecrets }) => {
|
||||
const filteredSecrets = filterSecrets(groupedSecrets, filter);
|
||||
return (
|
||||
<div className="flex flex-col" key={`${namespace}-${groupedSecrets.length}`}>
|
||||
<div
|
||||
className={twMerge(
|
||||
"bg-bunker-600 capitalize text-md h-0 transition-all",
|
||||
Boolean(namespace) && Boolean(filteredSecrets.length) && "h-11 py-3 pl-4 "
|
||||
)}
|
||||
key={namespace}
|
||||
>
|
||||
{namespace}
|
||||
</div>
|
||||
|
||||
{filteredSecrets.map((secret) => (
|
||||
<SecretItem
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
tags={wsTags}
|
||||
isSelected={selectedSecrets?.[secret._id]}
|
||||
onToggleSecretSelect={onToggleSecretSelect}
|
||||
isVisible={isVisible}
|
||||
secret={secret}
|
||||
key={secret._id}
|
||||
onSaveSecret={handleSaveSecret}
|
||||
onDeleteSecret={onDeleteSecret}
|
||||
onDetailViewSecret={onDetailViewSecret}
|
||||
onCreateTag={onCreateTag}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
)}
|
||||
<DeleteActionModal
|
||||
isOpen={popUp.deleteSecret.isOpen}
|
||||
deleteKey={(popUp.deleteSecret?.data as DecryptedSecret)?.key}
|
||||
title="Do you want to delete this secret?"
|
||||
onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)}
|
||||
onDeleteApproved={handleSecretDelete}
|
||||
/>
|
||||
<SecretDetailSidebar
|
||||
environment={environment}
|
||||
secretPath={secretPath}
|
||||
isOpen={popUp.secretDetail.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("secretDetail", isOpen)}
|
||||
decryptFileKey={decryptFileKey}
|
||||
secret={popUp.secretDetail.data as DecryptedSecret}
|
||||
onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)}
|
||||
onClose={() => handlePopUpClose("secretDetail")}
|
||||
onSaveSecret={handleSaveSecret}
|
||||
tags={wsTags}
|
||||
onCreateTag={() => handlePopUpOpen("createTag")}
|
||||
/>
|
||||
<CreateTagModal
|
||||
isOpen={popUp.createTag.isOpen}
|
||||
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export enum SecretActionType {
|
||||
Created = "created",
|
||||
Modified = "modified",
|
||||
Deleted = "deleted"
|
||||
}
|
||||
|
||||
export const formSchema = z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
idOverride: z.string().optional(),
|
||||
valueOverride: z.string().optional(),
|
||||
overrideAction: z.string().optional(),
|
||||
comment: z.string().trim().optional(),
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
tags: z
|
||||
.object({
|
||||
_id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string(),
|
||||
tagColor: z.string().optional()
|
||||
})
|
||||
.array()
|
||||
.default([])
|
||||
});
|
||||
|
||||
export type TFormSchema = z.infer<typeof formSchema>;
|
||||
@@ -0,0 +1 @@
|
||||
export { SecretListView } from "./SecretListView";
|
||||
Reference in New Issue
Block a user