mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-17 16:27:39 +00:00
feat: implemented new overview page with improvement in dashboard
This commit is contained in:
@@ -1,135 +0,0 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { useSyntaxHighlight } from "@app/hooks";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
|
||||
type Props = {
|
||||
secrets: any[] | undefined;
|
||||
// permission and external state's that decided to hide or show
|
||||
isReadOnly?: boolean;
|
||||
isSecretValueHidden: boolean;
|
||||
userAvailableEnvs?: any[];
|
||||
};
|
||||
|
||||
const SEC_VAL_LINE_HEIGHT = 21;
|
||||
const MAX_MULTI_LINE = 6;
|
||||
|
||||
const DashboardInput = ({
|
||||
isOverridden,
|
||||
isSecretValueHidden,
|
||||
secret,
|
||||
isReadOnly = true
|
||||
}: {
|
||||
isOverridden: boolean;
|
||||
isSecretValueHidden: boolean;
|
||||
isReadOnly?: boolean;
|
||||
secret?: any;
|
||||
}): JSX.Element => {
|
||||
const ref = useRef<HTMLElement | null>(null);
|
||||
const [isFocused, setIsFocused] = useToggle();
|
||||
const syntaxHighlight = useSyntaxHighlight();
|
||||
|
||||
const value = isOverridden ? secret.valueOverride : secret?.value;
|
||||
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
||||
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
||||
|
||||
return (
|
||||
<td
|
||||
key={`row-${secret?.key || ""}--`}
|
||||
className={`flex w-full cursor-default flex-row ${
|
||||
!(secret?.value || secret?.value === "") ? "bg-red-800/10" : "bg-mineshaft-900/30"
|
||||
}`}
|
||||
>
|
||||
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
||||
<textarea
|
||||
readOnly={isReadOnly}
|
||||
value={value}
|
||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
||||
style={{ height: `${maxMultilineHeight}px` }}
|
||||
spellCheck="false"
|
||||
onBlur={() => setIsFocused.off()}
|
||||
onFocus={() => setIsFocused.on()}
|
||||
onInput={(el) => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||
}
|
||||
}}
|
||||
onScroll={(el) => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
<code
|
||||
ref={ref}
|
||||
className={`absolute top-1.5 left-3.5 z-10 overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
||||
isOverridden && "text-primary-300"
|
||||
} ${
|
||||
(value || "") === "" && "text-mineshaft-400"
|
||||
}`}
|
||||
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
||||
>
|
||||
{value === undefined ? (
|
||||
<span className="cursor-default font-sans text-xs italic text-red-500/80">
|
||||
<FontAwesomeIcon icon={faMinus} className="mt-1" />
|
||||
</span>
|
||||
) : (
|
||||
syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)
|
||||
)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
};
|
||||
|
||||
export const EnvComparisonRow = ({
|
||||
secrets,
|
||||
isSecretValueHidden,
|
||||
isReadOnly,
|
||||
userAvailableEnvs
|
||||
}: Props): JSX.Element => {
|
||||
const [areValuesHiddenThisRow, setAreValuesHiddenThisRow] = useState(true);
|
||||
|
||||
const getSecretByEnv = useCallback(
|
||||
(secEnv: string, secs?: any[]) => secs?.find(({ env }) => env === secEnv),
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<tr className="group flex min-w-full flex-row hover:bg-mineshaft-800">
|
||||
<td className="flex w-10 justify-center border-none px-4">
|
||||
<div className="flex h-8 w-10 items-center justify-center text-center text-xs text-bunker-400">
|
||||
<FontAwesomeIcon icon={faKey} />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex min-w-[200px] flex-row justify-between lg:min-w-[220px] xl:min-w-[250px]">
|
||||
<div className="flex h-8 cursor-default flex-row items-center justify-center truncate">
|
||||
{secrets![0].key || ""}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="invisible mr-1 ml-2 text-bunker-400 hover:text-bunker-300 group-hover:visible"
|
||||
onClick={() => setAreValuesHiddenThisRow(!areValuesHiddenThisRow)}
|
||||
>
|
||||
<FontAwesomeIcon icon={areValuesHiddenThisRow ? faEye : faEyeSlash} />
|
||||
</button>
|
||||
</td>
|
||||
{userAvailableEnvs?.map(({ slug }) => (
|
||||
<DashboardInput
|
||||
isReadOnly={isReadOnly}
|
||||
key={`row-${secrets![0].key || ""}-${slug}`}
|
||||
isOverridden={false}
|
||||
secret={getSecretByEnv(slug, secrets)}
|
||||
isSecretValueHidden={areValuesHiddenThisRow && isSecretValueHidden}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { faCheck, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
type Props = {
|
||||
folderInEnv: Record<string, boolean>;
|
||||
userAvailableEnvs?: Array<{ slug: string; name: string }>;
|
||||
folderName: string;
|
||||
onClick: (folderName: string) => void;
|
||||
};
|
||||
|
||||
export const FolderComparisonRow = ({
|
||||
folderInEnv = {},
|
||||
userAvailableEnvs = [],
|
||||
folderName,
|
||||
onClick
|
||||
}: Props) => (
|
||||
<tr
|
||||
className="group flex min-w-full cursor-pointer flex-row items-center hover:bg-mineshaft-800"
|
||||
onClick={() => onClick(folderName)}
|
||||
>
|
||||
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<div className="w-10 text-center text-xs text-bunker-400">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
||||
</div>
|
||||
</td>
|
||||
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[200px] xl:min-w-[250px]">
|
||||
<div className="flex h-8 flex-row items-center truncate">{folderName}</div>
|
||||
</td>
|
||||
{userAvailableEnvs?.map(({ slug }) => (
|
||||
<td
|
||||
className={`flex h-10 w-full cursor-default flex-row items-center justify-center ${
|
||||
folderInEnv[slug]
|
||||
? "bg-mineshaft-900/30 text-green-500/80"
|
||||
: "bg-red-800/10 text-red-500/80"
|
||||
}`}
|
||||
key={`${folderName}-${slug}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={folderInEnv[slug] ? faCheck : faXmark} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
@@ -1 +0,0 @@
|
||||
export { EnvComparisonRow } from "./EnvComparisonRow";
|
||||
@@ -1,3 +1,4 @@
|
||||
import { memo } from "react";
|
||||
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
@@ -11,70 +12,74 @@ type Props = {
|
||||
onFolderOpen: (folderId: string) => void;
|
||||
};
|
||||
|
||||
export const FolderSection = ({
|
||||
onFolderUpdate: handleFolderUpdate,
|
||||
onFolderDelete: handleFolderDelete,
|
||||
onFolderOpen: handleFolderOpen,
|
||||
search = "",
|
||||
folders = []
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
{folders
|
||||
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
|
||||
.map(({ id, name }) => (
|
||||
<tr
|
||||
key={id}
|
||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||
>
|
||||
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
||||
</td>
|
||||
<td
|
||||
colSpan={2}
|
||||
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
||||
style={{ paddingTop: "0", paddingBottom: "0" }}
|
||||
export const FolderSection = memo(
|
||||
({
|
||||
onFolderUpdate: handleFolderUpdate,
|
||||
onFolderDelete: handleFolderDelete,
|
||||
onFolderOpen: handleFolderOpen,
|
||||
search = "",
|
||||
folders = []
|
||||
}: Props) => {
|
||||
return (
|
||||
<>
|
||||
{folders
|
||||
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
|
||||
.map(({ id, name }) => (
|
||||
<tr
|
||||
key={id}
|
||||
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
|
||||
>
|
||||
<div
|
||||
className="flex-grow cursor-default p-2"
|
||||
onKeyDown={() => null}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={() => handleFolderOpen(id)}
|
||||
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
|
||||
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
||||
</td>
|
||||
<td
|
||||
colSpan={2}
|
||||
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
|
||||
style={{ paddingTop: "0", paddingBottom: "0" }}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Settings" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
onClick={() => handleFolderUpdate(id, name)}
|
||||
ariaLabel="expand"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div
|
||||
className="flex-grow cursor-default p-2"
|
||||
onKeyDown={() => null}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={() => handleFolderOpen(id)}
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Settings" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
onClick={() => handleFolderUpdate(id, name)}
|
||||
ariaLabel="expand"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEdit} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Tooltip content="Delete" className="capitalize">
|
||||
<IconButton
|
||||
size="md"
|
||||
variant="plain"
|
||||
colorSchema="danger"
|
||||
ariaLabel="delete"
|
||||
onClick={() => handleFolderDelete(id, name)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} size="lg" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
FolderSection.displayName = "FolderSection";
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { EmptyState, IconButton, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { EmptyState, IconButton, SecretInput, TableContainer, Tooltip } from "@app/components/v2";
|
||||
import { useWorkspace } from "@app/context";
|
||||
import { useToggle } from "@app/hooks/useToggle";
|
||||
|
||||
@@ -138,7 +138,7 @@ export const SecretImportItem = ({
|
||||
{key}
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
{value}
|
||||
<SecretInput value={value} isDisabled isVisible />
|
||||
</td>
|
||||
<td className="h-10" style={{ padding: "0.25rem 1rem" }}>
|
||||
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
|
||||
|
||||
+28
-28
@@ -1,3 +1,4 @@
|
||||
import { memo } from "react";
|
||||
import { SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
|
||||
import { useWorkspace } from "@app/context";
|
||||
@@ -62,32 +63,31 @@ type Props = {
|
||||
items: { id: string; environment: string; secretPath: string }[];
|
||||
};
|
||||
|
||||
export const SecretImportSection = ({
|
||||
secrets = [],
|
||||
importedSecrets = [],
|
||||
onSecretImportDelete,
|
||||
items = []
|
||||
}: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
export const SecretImportSection = memo(
|
||||
({ secrets = [], importedSecrets = [], onSecretImportDelete, items = [] }: Props) => {
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const environments = currentWorkspace?.environments || [];
|
||||
|
||||
return (
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
|
||||
<SecretImportItem
|
||||
key={id}
|
||||
importedEnv={importSecEnv}
|
||||
importedSecrets={computeImportedSecretRows(
|
||||
importSecEnv,
|
||||
impSecPath,
|
||||
importedSecrets,
|
||||
secrets,
|
||||
environments
|
||||
)}
|
||||
onDelete={onSecretImportDelete}
|
||||
importedSecPath={impSecPath}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<SortableContext items={items} strategy={verticalListSortingStrategy}>
|
||||
{items.map(({ secretPath: impSecPath, environment: importSecEnv, id }) => (
|
||||
<SecretImportItem
|
||||
key={id}
|
||||
importedEnv={importSecEnv}
|
||||
importedSecrets={computeImportedSecretRows(
|
||||
importSecEnv,
|
||||
impSecPath,
|
||||
importedSecrets,
|
||||
secrets,
|
||||
environments
|
||||
)}
|
||||
onDelete={onSecretImportDelete}
|
||||
importedSecPath={impSecPath}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
SecretImportSection.displayName = "SecretImportSection";
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import { useRef } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
|
||||
import { useSyntaxHighlight, useToggle } from "@app/hooks";
|
||||
|
||||
import { FormData } from "../../DashboardPage.utils";
|
||||
|
||||
type Props = {
|
||||
isReadOnly?: boolean;
|
||||
isSecretValueHidden?: boolean;
|
||||
isOverridden?: boolean;
|
||||
index: number;
|
||||
};
|
||||
|
||||
const SEC_VAL_LINE_HEIGHT = 21;
|
||||
const MAX_MULTI_LINE = 6;
|
||||
|
||||
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
|
||||
const { control } = useFormContext<FormData>();
|
||||
const ref = useRef<HTMLElement | null>(null);
|
||||
const [isFocused, setIsFocused] = useToggle();
|
||||
const syntaxHighlight = useSyntaxHighlight();
|
||||
|
||||
const secretValue = useWatch({ control, name: `secrets.${index}.value` });
|
||||
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
|
||||
const value = isOverridden ? secretValueOverride : secretValue;
|
||||
|
||||
const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
|
||||
const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
|
||||
|
||||
return (
|
||||
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
|
||||
{isOverridden ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secrets.${index}.valueOverride`}
|
||||
render={({ field }) => (
|
||||
<textarea
|
||||
key={`secrets.${index}.valueOverride`}
|
||||
{...field}
|
||||
readOnly={isReadOnly}
|
||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
||||
style={{ height: `${maxMultilineHeight}px` }}
|
||||
spellCheck="false"
|
||||
onBlur={() => setIsFocused.off()}
|
||||
onFocus={() => setIsFocused.on()}
|
||||
onInput={(el) => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||
}
|
||||
}}
|
||||
onScroll={(el) => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secrets.${index}.value`}
|
||||
key={`secrets.${index}.value`}
|
||||
render={({ field }) => (
|
||||
<textarea
|
||||
{...field}
|
||||
readOnly={isReadOnly}
|
||||
className="ph-no-capture min-w-16 duration-50 peer z-20 w-full resize-none overflow-auto text-ellipsis bg-transparent px-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar"
|
||||
style={{ height: `${maxMultilineHeight}px` }}
|
||||
spellCheck="false"
|
||||
onBlur={() => setIsFocused.off()}
|
||||
onFocus={() => setIsFocused.on()}
|
||||
onInput={(el) => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||
}
|
||||
}}
|
||||
onScroll={(el) => {
|
||||
if (ref.current) {
|
||||
ref.current.scrollTop = el.currentTarget.scrollTop;
|
||||
ref.current.scrollLeft = el.currentTarget.scrollLeft;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<pre className="whitespace-pre-wrap break-words">
|
||||
<code
|
||||
ref={ref}
|
||||
className={`absolute top-1.5 left-3.5 z-10 w-full overflow-auto font-mono text-sm transition-all no-scrollbar ${
|
||||
isOverridden && "text-primary-300"
|
||||
} ${
|
||||
(value || "") === "" && "text-mineshaft-400"
|
||||
}`}
|
||||
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
|
||||
>
|
||||
{syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)}
|
||||
</code>
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,13 @@
|
||||
/* eslint-disable react/jsx-no-useless-fragment */
|
||||
import { memo, useEffect, useRef } from "react";
|
||||
import { Controller, useFieldArray, useFormContext, useWatch } from "react-hook-form";
|
||||
import { memo, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Control,
|
||||
Controller,
|
||||
useFieldArray,
|
||||
UseFormRegister,
|
||||
UseFormSetValue,
|
||||
useWatch
|
||||
} from "react-hook-form";
|
||||
import {
|
||||
faCheck,
|
||||
faCodeBranch,
|
||||
@@ -28,6 +35,7 @@ import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
SecretInput,
|
||||
Tag,
|
||||
TextArea,
|
||||
Tooltip
|
||||
@@ -36,24 +44,6 @@ import { useToggle } from "@app/hooks";
|
||||
import { WsTag } from "@app/hooks/api/types";
|
||||
|
||||
import { FormData, SecretActionType } from "../../DashboardPage.utils";
|
||||
import { MaskedInput } from "./MaskedInput";
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
// permission and external state's that decided to hide or show
|
||||
isReadOnly?: boolean;
|
||||
isAddOnly?: boolean;
|
||||
isRollbackMode?: boolean;
|
||||
isSecretValueHidden: boolean;
|
||||
searchTerm: string;
|
||||
// to record the ids of deleted ones
|
||||
onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
|
||||
// sidebar control props
|
||||
onRowExpand: () => void;
|
||||
// tag props
|
||||
wsTags?: WsTag[];
|
||||
onCreateTagOpen: () => void;
|
||||
};
|
||||
|
||||
const tagColors = [
|
||||
{ bg: "bg-[#f1c40f]/40", text: "text-[#fcf0c3]/70" },
|
||||
@@ -66,6 +56,31 @@ const tagColors = [
|
||||
{ bg: "bg-[#332FD0]/40", text: "text-[#DFF6FF]/70" }
|
||||
];
|
||||
|
||||
type Props = {
|
||||
index: number;
|
||||
// backend generated unique id
|
||||
secUniqId?: string;
|
||||
// permission and external state's that decided to hide or show
|
||||
isReadOnly?: boolean;
|
||||
isAddOnly?: boolean;
|
||||
isRollbackMode?: boolean;
|
||||
isSecretValueHidden: boolean;
|
||||
searchTerm: string;
|
||||
// to record the ids of deleted ones
|
||||
onSecretDelete: (index: number, id?: string, overrideId?: string) => void;
|
||||
// sidebar control props
|
||||
onRowExpand: (secId: string | undefined, index: number) => void;
|
||||
// tag props
|
||||
wsTags?: WsTag[];
|
||||
onCreateTagOpen: () => void;
|
||||
// rhf specific functions, dont put this using useFormContext. This is passed as props to avoid re-rendering
|
||||
control: Control<FormData>;
|
||||
register: UseFormRegister<FormData>;
|
||||
setValue: UseFormSetValue<FormData>;
|
||||
isKeyError?: boolean;
|
||||
keyError?: string;
|
||||
};
|
||||
|
||||
export const SecretInputRow = memo(
|
||||
({
|
||||
index,
|
||||
@@ -77,10 +92,15 @@ export const SecretInputRow = memo(
|
||||
wsTags,
|
||||
onCreateTagOpen,
|
||||
onSecretDelete,
|
||||
searchTerm
|
||||
searchTerm,
|
||||
control,
|
||||
register,
|
||||
setValue,
|
||||
isKeyError,
|
||||
keyError,
|
||||
secUniqId
|
||||
}: Props): JSX.Element => {
|
||||
const isKeySubDisabled = useRef<boolean>(false);
|
||||
const { register, setValue, control } = useFormContext<FormData>();
|
||||
// comment management in a row
|
||||
const {
|
||||
fields: secretTags,
|
||||
@@ -89,28 +109,40 @@ export const SecretInputRow = memo(
|
||||
} = useFieldArray({ control, name: `secrets.${index}.tags` });
|
||||
|
||||
// to get details on a secret
|
||||
const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
|
||||
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride` });
|
||||
const secComment = useWatch({ control, name: `secrets.${index}.comment` });
|
||||
const overrideAction = useWatch({
|
||||
control,
|
||||
name: `secrets.${index}.overrideAction`,
|
||||
exact: true
|
||||
});
|
||||
const idOverride = useWatch({ control, name: `secrets.${index}.idOverride`, exact: true });
|
||||
const secComment = useWatch({ control, name: `secrets.${index}.comment`, exact: true });
|
||||
const hasComment = Boolean(secComment);
|
||||
const secKey = useWatch({
|
||||
control,
|
||||
name: `secrets.${index}.key`,
|
||||
disabled: isKeySubDisabled.current
|
||||
disabled: isKeySubDisabled.current,
|
||||
exact: true
|
||||
});
|
||||
const secValue = useWatch({
|
||||
control,
|
||||
name: `secrets.${index}.value`,
|
||||
disabled: isKeySubDisabled.current
|
||||
disabled: isKeySubDisabled.current,
|
||||
exact: true
|
||||
});
|
||||
const secValueOverride = useWatch({
|
||||
control,
|
||||
name: `secrets.${index}.valueOverride`,
|
||||
disabled: isKeySubDisabled.current
|
||||
})
|
||||
const secId = useWatch({ control, name: `secrets.${index}._id` });
|
||||
disabled: isKeySubDisabled.current,
|
||||
exact: true
|
||||
});
|
||||
// when secret is override by personal values
|
||||
const isOverridden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
const [editorRef, setEditorRef] = useState(isOverridden ? secValueOverride : secValue);
|
||||
|
||||
const tags = useWatch({ control, name: `secrets.${index}.tags`, defaultValue: [] }) || [];
|
||||
const secId = useWatch({ control, name: `secrets.${index}._id`, exact: true });
|
||||
const tags =
|
||||
useWatch({ control, name: `secrets.${index}.tags`, exact: true, defaultValue: [] }) || [];
|
||||
const selectedTagIds = tags.reduce<Record<string, boolean>>(
|
||||
(prev, curr) => ({ ...prev, [curr.slug]: true }),
|
||||
{}
|
||||
@@ -126,15 +158,15 @@ export const SecretInputRow = memo(
|
||||
return () => clearTimeout(timer);
|
||||
}, [isInviteLinkCopied]);
|
||||
|
||||
useEffect(() => {
|
||||
setEditorRef(isOverridden ? secValueOverride : secValue);
|
||||
}, [isOverridden]);
|
||||
|
||||
const copyTokenToClipboard = () => {
|
||||
navigator.clipboard.writeText((secValueOverride || secValue) as string);
|
||||
setInviteLinkCopied.on();
|
||||
};
|
||||
|
||||
// when secret is override by personal values
|
||||
const isOverridden =
|
||||
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
|
||||
|
||||
const onSecretOverride = () => {
|
||||
if (isOverridden) {
|
||||
// when user created a new override but then removes
|
||||
@@ -193,10 +225,10 @@ export const SecretInputRow = memo(
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name={`secrets.${index}.key`}
|
||||
render={({ fieldState: { error }, field }) => (
|
||||
<HoverCard openDelay={0} open={error?.message ? undefined : false}>
|
||||
render={({ field }) => (
|
||||
<HoverCard openDelay={0} open={isKeyError ? undefined : false}>
|
||||
<HoverCardTrigger asChild>
|
||||
<td className={cx(error?.message ? "rounded ring ring-red/50" : null)}>
|
||||
<td className={cx(isKeyError ? "rounded ring ring-red/50" : null)}>
|
||||
<div className="relative flex w-full min-w-[220px] items-center justify-end lg:min-w-[240px] xl:min-w-[280px]">
|
||||
<Input
|
||||
autoComplete="off"
|
||||
@@ -220,21 +252,70 @@ export const SecretInputRow = memo(
|
||||
<div>
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-red" />
|
||||
</div>
|
||||
<div className="text-sm">{error?.message}</div>
|
||||
<div className="text-sm">{keyError}</div>
|
||||
</div>
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
)}
|
||||
/>
|
||||
<td className="flex w-full flex-grow flex-row border-r border-none border-red">
|
||||
<MaskedInput
|
||||
isReadOnly={
|
||||
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||
}
|
||||
isOverridden={isOverridden}
|
||||
isSecretValueHidden={isSecretValueHidden}
|
||||
index={index}
|
||||
/>
|
||||
<td
|
||||
className="flex w-full flex-grow flex-row border-r border-none border-red"
|
||||
style={{ padding: "0.5rem 0 0.5rem 1rem" }}
|
||||
>
|
||||
<div className="w-full">
|
||||
{isOverridden ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secrets.${index}.valueOverride`}
|
||||
render={({ field: { onChange, onBlur } }) => (
|
||||
<SecretInput
|
||||
key={`secrets.${index}.valueOverride`}
|
||||
isDisabled={
|
||||
isReadOnly ||
|
||||
isRollbackMode ||
|
||||
(isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||
}
|
||||
value={editorRef}
|
||||
isVisible={!isSecretValueHidden}
|
||||
onChange={(val, html) => {
|
||||
console.log(val);
|
||||
onChange(val);
|
||||
setEditorRef(html);
|
||||
}}
|
||||
onBlur={(html) => {
|
||||
setEditorRef(html);
|
||||
onBlur();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
control={control}
|
||||
name={`secrets.${index}.value`}
|
||||
render={({ field: { onBlur, onChange } }) => (
|
||||
<SecretInput
|
||||
key={`secrets.${index}.value`}
|
||||
isVisible={!isSecretValueHidden}
|
||||
isDisabled={
|
||||
isReadOnly ||
|
||||
isRollbackMode ||
|
||||
(isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||
}
|
||||
onChange={(val, html) => {
|
||||
onChange(val);
|
||||
setEditorRef(html);
|
||||
}}
|
||||
value={editorRef}
|
||||
onBlur={(html) => {
|
||||
setEditorRef(html);
|
||||
onBlur();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="min-w-sm flex">
|
||||
<div className="flex h-8 items-center pl-2">
|
||||
@@ -251,7 +332,7 @@ export const SecretInputRow = memo(
|
||||
{slug}
|
||||
</Tag>
|
||||
))}
|
||||
<div className="w-0 group-hover:w-6 overflow-hidden">
|
||||
<div className="w-0 overflow-hidden group-hover:w-6">
|
||||
<Tooltip content="Copy value">
|
||||
<IconButton
|
||||
variant="plain"
|
||||
@@ -396,7 +477,7 @@ export const SecretInputRow = memo(
|
||||
size="lg"
|
||||
colorSchema="primary"
|
||||
variant="plain"
|
||||
onClick={onRowExpand}
|
||||
onClick={() => onRowExpand(secUniqId, index)}
|
||||
ariaLabel="expand"
|
||||
>
|
||||
<FontAwesomeIcon icon={faEllipsis} />
|
||||
|
||||
Reference in New Issue
Block a user