Merge remote-tracking branch 'origin/main' into feat/resource-metadata

This commit is contained in:
Sheen Capadngan
2025-01-07 19:15:33 +08:00
1432 changed files with 33901 additions and 42304 deletions
@@ -0,0 +1,148 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { faClock, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { twMerge } from "tailwind-merge";
import { z } from "zod";
import { Button, FormControl, Input, Modal, ModalContent, TextArea } from "@app/components/v2";
const ReminderFormSchema = z.object({
note: z.string().optional().nullable(),
days: z
.number()
.min(1, { message: "Must be at least 1 day" })
.max(365, { message: "Must be less than 365 days" })
.nullable()
});
export type TReminderFormSchema = z.infer<typeof ReminderFormSchema>;
interface ReminderFormProps {
isOpen: boolean;
repeatDays?: number | null;
note?: string | null;
onOpenChange: (isOpen: boolean, data?: TReminderFormSchema) => void;
}
export const CreateReminderForm = ({
isOpen,
onOpenChange,
repeatDays,
note
}: ReminderFormProps) => {
const {
register,
control,
reset,
setValue,
handleSubmit,
formState: { isSubmitting }
} = useForm<TReminderFormSchema>({
defaultValues: {
days: repeatDays || undefined,
note: note || ""
},
resolver: zodResolver(ReminderFormSchema)
});
const handleFormSubmit = async (data: TReminderFormSchema) => {
onOpenChange(false, data);
};
useEffect(() => {
if (isOpen) {
reset({
days: repeatDays || undefined,
note: note || ""
});
}
}, [isOpen]);
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent
title={`${repeatDays ? "Update" : "Create"} reminder`}
subTitle="Set up a reminder for when this secret should be rotated. Everyone with access to this project will be notified when the reminder is triggered."
>
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="space-y-2">
<div>
<Controller
control={control}
name="days"
render={({ field, fieldState }) => (
<>
<FormControl
className="mb-0"
label="Reminder Interval (in days)"
isError={Boolean(fieldState.error)}
errorText={fieldState.error?.message || ""}
>
<Input
onChange={(el) => setValue("days", parseInt(el.target.value, 10))}
type="number"
placeholder="31"
value={field.value || undefined}
/>
</FormControl>
<div
className={twMerge(
"ml-1 mt-2 text-xs",
field.value ? "opacity-60" : "opacity-0"
)}
>
A reminder will be sent every{" "}
{field.value && field.value > 1 ? `${field.value} days` : "day"}
</div>
</>
)}
/>
</div>
<FormControl label="Note" className="mb-0">
<TextArea
placeholder="Remember to rotate the AWS secret every month."
className="border border-mineshaft-600 text-sm"
rows={8}
reSize="none"
cols={30}
{...register("note")}
/>
</FormControl>
</div>
<div className="mt-7 flex items-center space-x-4">
<Button
isDisabled={isSubmitting}
isLoading={isSubmitting}
key="layout-create-project-submit"
className=""
leftIcon={<FontAwesomeIcon icon={faClock} />}
type="submit"
>
{repeatDays ? "Update" : "Create"} reminder
</Button>
{repeatDays && (
<Button
key="layout-cancel-create-project"
onClick={() => onOpenChange(false, { days: null, note: null })}
colorSchema="danger"
leftIcon={<FontAwesomeIcon icon={faTrash} />}
>
Delete reminder
</Button>
)}
<Button
key="layout-cancel-create-project"
onClick={() => onOpenChange(false)}
variant="plain"
colorSchema="secondary"
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -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,601 @@
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCircleQuestion } from "@fortawesome/free-regular-svg-icons";
import {
faCheckCircle,
faCircle,
faCircleDot,
faClock,
faPlus,
faShare,
faTag,
faTrash
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { format } from "date-fns";
import { createNotification } from "@app/components/notifications";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
Drawer,
DrawerContent,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FormControl,
FormLabel,
IconButton,
Input,
Switch,
Tag,
TextArea,
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { useToggle } from "@app/hooks";
import { useGetSecretVersion } from "@app/hooks/api";
import { SecretV3RawSanitized, WsTag } from "@app/hooks/api/types";
import { CreateReminderForm } from "./CreateReminderForm";
import { formSchema, SecretActionType, TFormSchema } from "./SecretListView.utils";
type Props = {
isOpen?: boolean;
environment: string;
secretPath: string;
onToggle: (isOpen: boolean) => void;
onClose: () => void;
secret: SecretV3RawSanitized;
onDeleteSecret: () => void;
onSaveSecret: (
orgSec: SecretV3RawSanitized,
modSec: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
cb?: () => void
) => Promise<void>;
tags: WsTag[];
onCreateTag: () => void;
handleSecretShare: (value: string) => void;
};
export const SecretDetailSidebar = ({
isOpen,
onToggle,
secret,
onDeleteSecret,
onSaveSecret,
tags,
onCreateTag,
environment,
secretPath,
handleSecretShare
}: Props) => {
const {
register,
control,
watch,
handleSubmit,
setValue,
reset,
formState: { isDirty, isSubmitting }
} = useForm<TFormSchema>({
resolver: zodResolver(formSchema),
values: secret
});
const { permission } = useProjectPermission();
const tagFields = useFieldArray({
control,
name: "tags"
});
const metadataFormFields = useFieldArray({
control,
name: "secretMetadata"
});
const secretKey = secret?.key || "";
const selectedTags = watch("tags", []) || [];
const selectedTagsGroupById = selectedTags.reduce<Record<string, boolean>>(
(prev, curr) => ({ ...prev, [curr.id]: true }),
{}
);
const selectTagSlugs = selectedTags.map((i) => i.slug);
const cannotEditSecret = permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
);
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})
) && cannotEditSecret;
const overrideAction = watch("overrideAction");
const isOverridden =
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
const { data: secretVersion } = useGetSecretVersion({
limit: 10,
offset: 0,
secretId: secret?.id
});
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) {
tagFields.remove(tagPos);
}
} else {
tagFields.append(tag);
}
};
const handleFormSubmit = async (data: TFormSchema) => {
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
};
const [createReminderFormOpen, setCreateReminderFormOpen] = useToggle(false);
const secretReminderRepeatDays = watch("reminderRepeatDays");
const secretReminderNote = watch("reminderNote");
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 });
}
}}
/>
<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 h-full flex-col">
<FormControl label="Key">
<Input isDisabled {...register("key")} />
</FormControl>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Controller
name="value"
key="secret-value"
control={control}
render={({ field }) => (
<FormControl label="Value">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value"
isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
autoFocus={false}
/>
</FormControl>
)}
/>
)}
</ProjectPermissionCan>
<div className="mb-2 border-b border-mineshaft-600 pb-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(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">
<InfisicalSecretInput
isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field}
/>
</FormControl>
)}
/>
)}
<FormControl label="Metadata">
<div className="flex flex-col space-y-2">
{metadataFormFields.fields.map(({ id: metadataFieldId }, i) => (
<div key={metadataFieldId} className="flex items-end space-x-2">
<div className="flex-grow">
{i === 0 && <span className="text-xs text-mineshaft-400">Key</span>}
<Controller
control={control}
name={`secretMetadata.${i}.key`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<div className="flex-grow">
{i === 0 && (
<FormLabel
label="Value"
className="text-xs text-mineshaft-400"
isOptional
/>
)}
<Controller
control={control}
name={`secretMetadata.${i}.value`}
render={({ field, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
className="mb-0"
>
<Input {...field} className="max-h-8" />
</FormControl>
)}
/>
</div>
<IconButton
ariaLabel="delete key"
className="bottom-0.5 max-h-8"
variant="outline_bg"
onClick={() => metadataFormFields.remove(i)}
>
<FontAwesomeIcon icon={faTrash} />
</IconButton>
</div>
))}
<div className="mt-2">
<Button
leftIcon={<FontAwesomeIcon icon={faPlus} />}
size="xs"
variant="outline_bg"
onClick={() => metadataFormFields.append({ key: "", value: "" })}
>
Add Key
</Button>
</div>
</div>
</FormControl>
<FormControl label="Tags" className="">
<div className="grid auto-cols-min grid-flow-col gap-2 overflow-hidden pt-2">
{tagFields.fields.map(({ tagColor, id: formId, slug, id }) => (
<Tag
className="flex w-min items-center space-x-2"
key={formId}
onClose={() => {
if (cannotEditSecret) {
createNotification({ type: "error", text: "Access denied" });
return;
}
const tag = tags?.find(({ id: tagId }) => id === tagId);
if (tag) handleTagSelect(tag);
}}
>
<div
className="h-3 w-3 rounded-full"
style={{ backgroundColor: tagColor || "#bec2c8" }}
/>
<div className="text-sm">{slug}</div>
</Tag>
))}
<DropdownMenu>
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<DropdownMenuTrigger asChild>
<IconButton
ariaLabel="add"
variant="outline_bg"
size="xs"
className="rounded-md"
isDisabled={!isAllowed}
>
<FontAwesomeIcon icon={faPlus} />
</IconButton>
</DropdownMenuTrigger>
)}
</ProjectPermissionCan>
<DropdownMenuContent align="end" className="z-[100]">
<DropdownMenuLabel>Add tags to this secret</DropdownMenuLabel>
{tags.map((tag) => {
const { id: tagId, slug, color } = tag;
const isSelected = selectedTagsGroupById?.[tagId];
return (
<DropdownMenuItem
onClick={() => handleTagSelect(tag)}
key={tagId}
icon={isSelected && <FontAwesomeIcon icon={faCheckCircle} />}
iconPos="right"
>
<div className="flex items-center">
<div
className="mr-2 h-2 w-2 rounded-full"
style={{ background: color || "#bec2c8" }}
/>
{slug}
</div>
</DropdownMenuItem>
);
})}
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={ProjectPermissionSub.Tags}
>
{(isAllowed) => (
<DropdownMenuItem asChild>
<Button
size="xs"
className="w-full"
colorSchema="primary"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faTag} />}
onClick={onCreateTag}
isDisabled={!isAllowed}
>
Create a tag
</Button>
</DropdownMenuItem>
)}
</ProjectPermissionCan>
</DropdownMenuContent>
</DropdownMenu>
</div>
</FormControl>
<FormControl label="Reminder">
{secretReminderRepeatDays && secretReminderRepeatDays > 0 ? (
<div className="ml-1 mt-2 flex items-center justify-between">
<div className="flex items-center space-x-2">
<FontAwesomeIcon className="text-primary-500" icon={faClock} />
<span className="text-sm text-bunker-300">
Reminder every {secretReminderRepeatDays}{" "}
{secretReminderRepeatDays > 1 ? "days" : "day"}
</span>
</div>
<div>
<Button
className="px-2 py-1"
variant="outline_bg"
onClick={() => setCreateReminderFormOpen.on()}
>
Update
</Button>
</div>
</div>
) : (
<div className="ml-1 mt-2 flex items-center space-x-2">
<Button
className="w-full px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faClock} />}
onClick={() => setCreateReminderFormOpen.on()}
isDisabled={cannotEditSecret}
>
Create Reminder
</Button>
</div>
)}
</FormControl>
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 text-sm"
{...register("comment")}
readOnly={isReadOnly}
rows={5}
/>
</FormControl>
<div className="my-2 mb-4 border-b border-mineshaft-600 pb-4">
<Controller
control={control}
name="skipMultilineEncoding"
render={({ field: { value, onChange, onBlur } }) => (
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Switch
id="skipmultiencoding-option"
onCheckedChange={(isChecked) => onChange(isChecked)}
isChecked={value}
onBlur={onBlur}
isDisabled={!isAllowed}
className="items-center"
>
Multi line encoding
<Tooltip
content="When enabled, multiline secrets will be handled by escaping newlines and enclosing the entire value in double quotes."
className="z-[100]"
>
<FontAwesomeIcon icon={faCircleQuestion} className="ml-1" size="sm" />
</Tooltip>
</Switch>
)}
</ProjectPermissionCan>
)}
/>
</div>
<div className="ml-1 flex items-center space-x-4">
<Button
className="w-full px-2 py-1"
variant="outline_bg"
leftIcon={<FontAwesomeIcon icon={faShare} />}
onClick={() => {
const value = secret?.valueOverride ?? secret?.value;
if (value) {
handleSecretShare(value);
}
}}
>
Share Secret
</Button>
</div>
<div className="dark mb-4 mt-4 flex-grow text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, secretValue, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
</div>
<div>{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">{secretValue}</div>
</div>
</div>
))}
</div>
</div>
<div className="flex flex-col space-y-4">
<div className="flex items-center space-x-4">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(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,
secretName: secretKey,
secretTags: selectTagSlugs
})}
>
{(isAllowed) => (
<Button colorSchema="danger" isDisabled={!isAllowed} onClick={onDeleteSecret}>
Delete
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
</div>
</form>
</DrawerContent>
</Drawer>
</>
);
};
@@ -0,0 +1,653 @@
/* eslint-disable simple-import-sort/imports */
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
FontAwesomeSymbol,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
ModalTrigger,
Popover,
PopoverContent,
PopoverTrigger,
SecretInput,
Spinner,
TextArea,
Tooltip
} from "@app/components/v2";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import {
ProjectPermissionActions,
ProjectPermissionSub,
useProjectPermission,
useWorkspace
} from "@app/context";
import { useToggle } from "@app/hooks";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types";
import { subject } from "@casl/ability";
import { zodResolver } from "@hookform/resolvers/zod";
import { AnimatePresence, motion } from "framer-motion";
import { memo, useEffect } from "react";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { twMerge } from "tailwind-merge";
import {
hasSecretReference,
SecretReferenceTree
} from "@app/components/secrets/SecretReferenceDetails";
import {
FontAwesomeSpriteName,
formSchema,
SecretActionType,
TFormSchema
} from "./SecretListView.utils";
type Props = {
secret: SecretV3RawSanitized;
onSaveSecret: (
orgSec: SecretV3RawSanitized,
modSec: Omit<SecretV3RawSanitized, "tags"> & { tags?: { id: string }[] },
cb?: () => void
) => Promise<void>;
onDeleteSecret: (sec: SecretV3RawSanitized) => void;
onDetailViewSecret: (sec: SecretV3RawSanitized) => void;
isVisible?: boolean;
isSelected?: boolean;
onToggleSecretSelect: (secret: SecretV3RawSanitized) => void;
tags: WsTag[];
onCreateTag: () => void;
environment: string;
secretPath: string;
handleSecretShare: () => void;
};
export const SecretItem = memo(
({
secret,
onSaveSecret,
onDeleteSecret,
onDetailViewSecret,
isVisible,
isSelected,
tags = [],
onCreateTag,
onToggleSecretSelect,
environment,
secretPath,
handleSecretShare
}: Props) => {
const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission();
const {
handleSubmit,
control,
register,
watch,
setValue,
reset,
getValues,
trigger,
formState: { isDirty, isSubmitting, errors }
} = useForm<TFormSchema>({
defaultValues: secret,
values: secret,
resolver: zodResolver(formSchema)
});
const secretName = watch("key");
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 selectedTagSlugs = selectedTags.map((i) => i.slug);
const { fields, append, remove } = useFieldArray({
control,
name: "tags"
});
const isReadOnly =
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})
) &&
permission.cannot(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: selectedTagSlugs
})
);
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 });
setValue("reminderRepeatDays", secret?.reminderRepeatDays, {
shouldDirty: !isUnsavedOverride
});
setValue("reminderNote", secret?.reminderNote, { shouldDirty: !isUnsavedOverride });
} else {
reset();
setValue("overrideAction", SecretActionType.Modified, { shouldDirty: true });
setValue("valueOverride", "", { shouldDirty: true });
}
};
const handleFormSubmit = async (data: TFormSchema) => {
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
};
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"]);
if (isOverriden) {
navigator.clipboard.writeText(value as string);
} else {
navigator.clipboard.writeText(overrideValue as string);
}
setIsSecValueCopied.on();
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div
className={twMerge(
"border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700",
isDirty && "border-primary-400/50"
)}
>
<div className="group flex">
<div
className={twMerge(
"flex h-11 w-11 items-center justify-center px-4 py-3",
isDirty && "text-primary"
)}
>
<Checkbox
id={`checkbox-${secret.id}`}
isChecked={isSelected}
onCheckedChange={() => onToggleSecretSelect(secret)}
className={twMerge("ml-3 hidden group-hover:flex", isSelected && "flex")}
/>
<FontAwesomeSymbol
className={twMerge(
"ml-3 block h-3.5 w-3.5 group-hover:hidden",
isSelected && "hidden"
)}
symbolName={FontAwesomeSpriteName.SecretKey}
/>
</div>
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
<Controller
name="key"
control={control}
render={({ field, fieldState: { error } }) => (
<Input
autoComplete="off"
isReadOnly={isReadOnly}
autoCapitalization={currentWorkspace?.autoCapitalization}
variant="plain"
isDisabled={isOverriden}
placeholder={error?.message}
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
name="valueOverride"
key="value-overriden"
control={control}
render={({ field }) => (
<SecretInput
key="value-overriden"
isVisible={isVisible}
isReadOnly={isReadOnly}
{...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/>
)}
/>
) : (
<Controller
name="value"
key="secret-value"
control={control}
render={({ field }) => (
<InfisicalSecretInput
isReadOnly={isReadOnly}
key="secret-value"
isVisible={isVisible}
environment={environment}
secretPath={secretPath}
{...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"
variant="plain"
size="sm"
className="w-0 overflow-hidden p-0 group-hover:mr-2 group-hover:w-5"
onClick={copyTokenToClipboard}
>
<FontAwesomeSymbol
className="h-3.5 w-3"
symbolName={
isSecValueCopied
? FontAwesomeSpriteName.Check
: FontAwesomeSpriteName.ClipboardCopy
}
/>
</IconButton>
</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 Details"
subTitle="Visual breakdown of secrets referenced by this secret."
onOpenAutoFocus={(e) => e.preventDefault()} // prevents secret input from displaying value on open
>
<SecretReferenceTree
secretPath={secretPath}
environment={environment}
secretKey={secret?.key}
/>
</ModalContent>
</Modal>
)}
</ProjectPermissionCan>
<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
</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 mode="wait">
{!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>
</motion.div>
) : (
<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
ariaLabel="more"
variant="plain"
type="submit"
size="md"
className={twMerge(
"p-0 text-primary opacity-0 group-hover:opacity-100",
isDirty && "opacity-100"
)}
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>
</form>
);
}
);
SecretItem.displayName = "SecretItem";
@@ -0,0 +1,353 @@
import { useCallback } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQueryClient } from "@tanstack/react-query";
import { createNotification } from "@app/components/notifications";
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 { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { secretSnapshotKeys } from "@app/hooks/api/secretSnapshots/queries";
import { WsTag } from "@app/hooks/api/types";
import { AddShareSecretModal } from "@app/pages/organization/SecretSharingPage/components/AddShareSecretModal";
import { useSelectedSecretActions, useSelectedSecrets } from "../../SecretMainPage.store";
import { SecretDetailSidebar } from "./SecretDetailSidebar";
import { SecretItem } from "./SecretItem";
import { FontAwesomeSpriteSymbols } from "./SecretListView.utils";
type Props = {
secrets?: SecretV3RawSanitized[];
environment: string;
workspaceId: string;
secretPath?: string;
tags?: WsTag[];
isVisible?: boolean;
isProtectedBranch?: boolean;
};
export const SecretListView = ({
secrets = [],
environment,
workspaceId,
secretPath = "/",
tags: wsTags = [],
isVisible,
isProtectedBranch = false
}: Props) => {
const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteSecret",
"secretDetail",
"createTag",
"createSharedSecret"
] 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 selectedSecrets = useSelectedSecrets();
const { toggle: toggleSelectedSecret } = useSelectedSecretActions();
const handleSecretOperation = async (
operation: "create" | "update" | "delete",
type: SecretType,
key: string,
{
value,
comment,
reminderRepeatDays,
reminderNote,
tags,
skipMultilineEncoding,
newKey,
secretId,
secretMetadata
}: Partial<{
value: string;
comment: string;
reminderRepeatDays: number | null;
reminderNote: string | null;
tags: string[];
skipMultilineEncoding: boolean;
newKey: string;
secretId: string;
secretMetadata?: { key: string; value: string }[];
}> = {}
) => {
if (operation === "delete") {
await deleteSecretV3({
environment,
workspaceId,
secretPath,
secretKey: key,
type,
secretId
});
return;
}
if (operation === "update") {
await updateSecretV3({
environment,
workspaceId,
secretPath,
secretKey: key,
secretValue: value || "",
type,
tagIds: tags,
secretComment: comment,
secretReminderRepeatDays: reminderRepeatDays,
secretReminderNote: reminderNote,
skipMultilineEncoding,
newSecretName: newKey,
secretMetadata
});
return;
}
await createSecretV3(
{
environment,
workspaceId,
secretPath,
secretKey: key,
secretValue: value || "",
secretComment: "",
skipMultilineEncoding,
type
},
{}
);
};
const handleSaveSecret = useCallback(
async (
orgSecret: SecretV3RawSanitized,
modSecret: Omit<SecretV3RawSanitized, "tags"> & {
tags?: { id: string }[];
secretMetadata?: { key: string; value: string }[];
},
cb?: () => void
) => {
const { key: oldKey } = orgSecret;
const {
key,
value,
overrideAction,
idOverride,
valueOverride,
tags,
comment,
reminderRepeatDays,
reminderNote,
secretMetadata
} = modSecret;
const hasKeyChanged = oldKey !== key && 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",
"reminderRepeatDays",
"reminderNote",
"secretMetadata"
] as const
).every((el) => orgSecret[el] === modSecret[el]) && isSameTags;
try {
// personal secret change
if (overrideAction === "deleted") {
await handleSecretOperation("delete", SecretType.Personal, oldKey, {
secretId: orgSecret.idOverride
});
} else if (overrideAction && idOverride) {
await handleSecretOperation("update", SecretType.Personal, oldKey, {
value: valueOverride,
newKey: hasKeyChanged ? key : undefined,
secretId: orgSecret.idOverride,
skipMultilineEncoding: modSecret.skipMultilineEncoding
});
} else if (overrideAction) {
await handleSecretOperation("create", SecretType.Personal, oldKey, {
value: valueOverride
});
}
// shared secret change
if (!isSharedSecUnchanged) {
await handleSecretOperation("update", SecretType.Shared, oldKey, {
value,
tags: tagIds,
comment,
reminderRepeatDays,
reminderNote,
secretId: orgSecret.id,
newKey: hasKeyChanged ? key : undefined,
skipMultilineEncoding: modSecret.skipMultilineEncoding,
secretMetadata
});
if (cb) cb();
}
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({
projectId: workspaceId,
secretPath
})
});
queryClient.invalidateQueries({
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.list({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretApprovalRequestKeys.count({ workspaceId })
});
handlePopUpClose("secretDetail");
createNotification({
type: isProtectedBranch ? "info" : "success",
text: isProtectedBranch
? "Requested changes have been sent for review"
: "Successfully saved secrets"
});
} catch (error) {
console.log(error);
createNotification({
type: "error",
text: "Failed to save secret"
});
}
},
[environment, secretPath, isProtectedBranch]
);
const handleSecretDelete = useCallback(async () => {
const { key, id: secretId } = popUp.deleteSecret?.data as SecretV3RawSanitized;
try {
await handleSecretOperation("delete", SecretType.Shared, key, { secretId });
// wrap this in another function and then reuse
queryClient.invalidateQueries({
queryKey: dashboardKeys.getDashboardSecrets({ projectId: workspaceId, secretPath })
});
queryClient.invalidateQueries({
queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.list({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory: secretPath })
});
queryClient.invalidateQueries({
queryKey: secretApprovalRequestKeys.count({ workspaceId })
});
handlePopUpClose("deleteSecret");
handlePopUpClose("secretDetail");
createNotification({
type: isProtectedBranch ? "info" : "success",
text: isProtectedBranch
? "Requested changes have been sent for review"
: "Successfully deleted secret"
});
} catch (error) {
console.log(error);
createNotification({
type: "error",
text: "Failed to delete secret"
});
}
}, [(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key, environment, secretPath]);
// for optimization on minimise re-rendering of secret items
const onCreateTag = useCallback(() => handlePopUpOpen("createTag"), []);
const onDeleteSecret = useCallback(
(sec: SecretV3RawSanitized) => handlePopUpOpen("deleteSecret", sec),
[]
);
const onDetailViewSecret = useCallback(
(sec: SecretV3RawSanitized) => handlePopUpOpen("secretDetail", sec),
[]
);
return (
<>
{FontAwesomeSpriteSymbols.map(({ icon, symbol }) => (
<FontAwesomeIcon icon={icon} symbol={symbol} key={`font-awesome-svg-spritie-${symbol}`} />
))}
{secrets.map((secret) => (
<SecretItem
environment={environment}
secretPath={secretPath}
tags={wsTags}
isSelected={Boolean(selectedSecrets?.[secret.id])}
onToggleSecretSelect={toggleSelectedSecret}
isVisible={isVisible}
secret={secret}
key={secret.id}
onSaveSecret={handleSaveSecret}
onDeleteSecret={onDeleteSecret}
onDetailViewSecret={onDetailViewSecret}
onCreateTag={onCreateTag}
handleSecretShare={() =>
handlePopUpOpen("createSharedSecret", {
value: secret.valueOverride ?? secret.value
})
}
/>
))}
<DeleteActionModal
isOpen={popUp.deleteSecret.isOpen}
deleteKey={(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key}
title="Do you want to delete this secret?"
onChange={(isOpen) => handlePopUpToggle("deleteSecret", isOpen)}
onDeleteApproved={handleSecretDelete}
buttonText="Delete Secret"
/>
<SecretDetailSidebar
environment={environment}
secretPath={secretPath}
isOpen={popUp.secretDetail.isOpen}
onToggle={(isOpen) => handlePopUpToggle("secretDetail", isOpen)}
secret={popUp.secretDetail.data as SecretV3RawSanitized}
onDeleteSecret={() => handlePopUpOpen("deleteSecret", popUp.secretDetail.data)}
onClose={() => handlePopUpClose("secretDetail")}
onSaveSecret={handleSaveSecret}
tags={wsTags}
onCreateTag={() => handlePopUpOpen("createTag")}
handleSecretShare={(value: string) => handlePopUpOpen("createSharedSecret", { value })}
/>
<CreateTagModal
isOpen={popUp.createTag.isOpen}
onToggle={(isOpen) => handlePopUpToggle("createTag", isOpen)}
/>
<AddShareSecretModal popUp={popUp} handlePopUpToggle={handlePopUpToggle} />
</>
);
};
@@ -0,0 +1,104 @@
/* eslint-disable no-nested-ternary */
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons";
import {
faCheck,
faClock,
faClone,
faClose,
faCodeBranch,
faComment,
faCopy,
faEllipsis,
faKey,
faLock,
faProjectDiagram,
faShare,
faTags
} from "@fortawesome/free-solid-svg-icons";
import { z } from "zod";
export enum SecretActionType {
Created = "created",
Modified = "modified",
Deleted = "deleted"
}
export const formSchema = z.object({
key: z.string().trim().min(1, { message: "Secret key is required" }),
value: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.optional(),
idOverride: z.string().trim().optional(),
valueOverride: z
.string()
.optional()
.transform((val) =>
typeof val === "string" ? (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()) : val
),
overrideAction: z.string().trim().optional(),
comment: z.string().trim().optional(),
skipMultilineEncoding: z.boolean().optional(),
reminderRepeatDays: z
.number()
.min(1, { message: "Days must be between 1 and 365" })
.max(365, { message: "Days must be between 1 and 365" })
.nullable()
.optional(),
reminderNote: z.string().trim().nullable().optional(),
secretMetadata: z
.object({
key: z.string().trim().min(1),
value: z.string().trim().default("")
})
.array()
.optional(),
tags: z
.object({
id: z.string(),
slug: z.string(),
tagColor: z.string().optional()
})
.array()
.default([])
.optional()
});
export type TFormSchema = z.infer<typeof formSchema>;
export enum FontAwesomeSpriteName {
SecretKey = "secret-key",
Check = "check",
ClipboardCopy = "clipboard-copy",
Tags = "secret-tags",
Clock = "reminder-clock",
Comment = "comment",
More = "more",
Override = "secret-override",
Close = "close",
CheckedCircle = "check-circle",
ReplicatedSecretKey = "secret-replicated",
ShareSecret = "share-secret",
KeyLock = "key-lock",
SecretReferenceTree = "secret-reference-tree"
}
// this is an optimization technique
// https://docs.fontawesome.com/web/add-icons/svg-symbols
export const FontAwesomeSpriteSymbols = [
{ icon: faKey, symbol: FontAwesomeSpriteName.SecretKey },
{ icon: faCheck, symbol: FontAwesomeSpriteName.Check },
{ icon: faCopy, symbol: FontAwesomeSpriteName.ClipboardCopy },
{ icon: faTags, symbol: FontAwesomeSpriteName.Tags },
{ icon: faClock, symbol: FontAwesomeSpriteName.Clock },
{ icon: faComment, symbol: FontAwesomeSpriteName.Comment },
{ icon: faEllipsis, symbol: FontAwesomeSpriteName.More },
{ icon: faCodeBranch, symbol: FontAwesomeSpriteName.Override },
{ icon: faClose, symbol: FontAwesomeSpriteName.Close },
{ icon: faCheckCircle, symbol: FontAwesomeSpriteName.CheckedCircle },
{ icon: faClone, symbol: FontAwesomeSpriteName.ReplicatedSecretKey },
{ icon: faShare, symbol: FontAwesomeSpriteName.ShareSecret },
{ icon: faLock, symbol: FontAwesomeSpriteName.KeyLock },
{ icon: faProjectDiagram, symbol: FontAwesomeSpriteName.SecretReferenceTree }
];
@@ -0,0 +1,49 @@
import { FontAwesomeSymbol, Input, Tooltip } from "@app/components/v2";
import { FontAwesomeSpriteName } from "./SecretListView.utils";
type Props = {
count: number;
};
export const SecretNoAccessListView = ({ count }: Props) => {
return (
<>
{Array.from(Array(count)).map((_, i) => (
<Tooltip
className="max-w-sm"
asChild
content="You do not have permission to view this secret"
key={`no-access-secret-${i + 1}`}
>
<div className="flex border-b border-mineshaft-600 bg-mineshaft-800 shadow-none hover:bg-mineshaft-700">
<div className="flex h-11 w-11 items-center justify-center px-4 py-3">
<FontAwesomeSymbol
className="ml-3 block h-3.5 w-3.5"
symbolName={FontAwesomeSpriteName.KeyLock}
/>
</div>
<div className="flex h-11 w-80 flex-shrink-0 items-center px-4 py-2">
<Input
autoComplete="off"
isReadOnly
variant="plain"
value="NO ACCESS"
isDisabled
className="w-full px-0 blur-sm 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"
>
<span className="blur">********</span>
</div>
</div>
</Tooltip>
))}
</>
);
};
@@ -0,0 +1,2 @@
export { SecretListView } from "./SecretListView";
export { SecretNoAccessListView } from "./SecretNoAccessListView";