Merge pull request #692 from akhilmhdh/feat/multi-line-secrets

multi line support for secrets
This commit is contained in:
Maidul Islam
2023-06-29 17:35:25 -04:00
committed by GitHub
8 changed files with 383 additions and 351 deletions

View File

@ -54,23 +54,20 @@ export const getSecretVersions = async (req: Request, res: Response) => {
}
}
*/
const { secretId, workspaceId, environment, folderId } = req.params;
const { secretId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
const secretVersions = await SecretVersion.find({
secret: secretId,
workspace: workspaceId,
environment,
folder: folderId,
secret: secretId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
return res.status(200).send({
secretVersions,
secretVersions
});
};
@ -135,7 +132,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
// validate secret version
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId,
version,
version
}).select("+secretBlindIndex");
if (!oldSecretVersion) throw new Error("Failed to find secret version");
@ -154,7 +151,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueTag,
algorithm,
folder,
keyEncoding,
keyEncoding
} = oldSecretVersion;
// update secret
@ -162,7 +159,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretId,
{
$inc: {
version: 1,
version: 1
},
workspace,
type,
@ -177,10 +174,10 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueTag,
folderId: folder,
algorithm,
keyEncoding,
keyEncoding
},
{
new: true,
new: true
}
);
@ -204,17 +201,17 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueTag,
folder,
algorithm,
keyEncoding,
keyEncoding
}).save();
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace,
environment,
folderId: folder,
folderId: folder
});
return res.status(200).send({
secret,
secret
});
};

View File

@ -81,76 +81,79 @@ export const useGetProjectSecrets = ({
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const sharedSecrets: DecryptedSecret[] = [];
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
select: useCallback(
(data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const sharedSecrets: DecryptedSecret[] = [];
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
sharedSecrets.push(decryptedSecret);
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
sharedSecrets.push(decryptedSecret);
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
});
sharedSecrets.forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
}
});
return { secrets: sharedSecrets };
}, [decryptFileKey])
});
sharedSecrets.forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
}
});
return { secrets: sharedSecrets };
},
[decryptFileKey]
)
});
export const useGetProjectSecretsByKey = ({
@ -167,82 +170,85 @@ export const useGetProjectSecretsByKey = ({
// right now secretpath is passed as folderid as only this is used in overview
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
select: useCallback((data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
const uniqSecKeys: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
select: useCallback(
(data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const sharedSecrets: Record<string, DecryptedSecret[]> = {};
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
const uniqSecKeys: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
if (!uniqSecKeys?.[secretKey]) uniqSecKeys[secretKey] = true;
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
sharedSecrets[secretKey].push(decryptedSecret);
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
});
Object.keys(sharedSecrets).forEach((secName) => {
sharedSecrets[secName].forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
sharedSecrets[secretKey].push(decryptedSecret);
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
});
});
Object.keys(sharedSecrets).forEach((secName) => {
sharedSecrets[secName].forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
}
});
});
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
}, [decryptFileKey])
return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
},
[decryptFileKey]
)
});
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
@ -263,29 +269,32 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
enabled: Boolean(dto.secretId && dto.decryptFileKey),
queryKey: secretKeys.getSecretVersion(dto.secretId),
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
select: useCallback((data: EncryptedSecretVersion[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = dto.decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
select: useCallback(
(data: EncryptedSecretVersion[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = dto.decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
return data
.map((el) => ({
createdAt: el.createdAt,
id: el._id,
value: decryptSymmetric({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key
})
}))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}, [])
return data
.map((el) => ({
createdAt: el.createdAt,
id: el._id,
value: decryptSymmetric({
ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV,
tag: el.secretValueTag,
key
})
}))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
},
[dto.decryptFileKey]
)
});
export const useBatchSecretsOp = () => {

View File

@ -1,4 +1,5 @@
export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useToggle } from "./useToggle";

View File

@ -0,0 +1,45 @@
import { useCallback } from "react";
import { faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
const REGEX = /([$]{.*?})/g;
export const useSyntaxHighlight = () => {
const syntaxHighlight = useCallback((text: string, isHidden?: boolean) => {
if (isHidden) {
return text
.split("")
.slice(0, 200)
.map((el, i) =>
el === "\n" ? (
el
) : (
<FontAwesomeIcon
key={`${text}_${el}_${i + 1}`}
className="mr-0.5 text-xxs"
icon={faCircle}
/>
)
);
}
// append a space on last new line this to show new line in ui for code component
const val = text.at(-1) === "\n" ? text.concat(" ") : text;
if (val?.length === 0) return <span className="font-mono text-bunker-400/80">EMPTY</span>;
return val?.split(REGEX).map((word, i) =>
word.match(REGEX) !== null ? (
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
$&#123;
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
&#125;
</span>
) : (
<span key={`${word}_${i + 1}`} className="ph-no-capture">
{word}
</span>
)
);
}, []);
return syntaxHighlight;
};

View File

@ -1,8 +1,11 @@
/* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useState } from "react";
import { faCircle, faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
import { useCallback, useRef, useState } from "react";
import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useSyntaxHighlight } from "@app/hooks";
import { useToggle } from "~/hooks/useToggle";
type Props = {
secrets: any[] | undefined;
@ -12,7 +15,8 @@ type Props = {
userAvailableEnvs?: any[];
};
const REGEX = /([$]{.*?})/g;
const SEC_VAL_LINE_HEIGHT = 21;
const MAX_MULTI_LINE = 6;
const DashboardInput = ({
isOverridden,
@ -25,84 +29,60 @@ const DashboardInput = ({
isReadOnly?: boolean;
secret?: any;
}): JSX.Element => {
const syntaxHighlight = useCallback((val: string) => {
if (val === undefined)
return (
<span className="cursor-default font-sans text-xs italic text-red-500/80">
<FontAwesomeIcon icon={faMinus} className="mt-1" />
</span>
);
if (val?.length === 0)
return <span className="w-full font-sans text-bunker-400/80">EMPTY</span>;
return val?.split(REGEX).map((word, index) =>
word.match(REGEX) !== null ? (
<span className="ph-no-capture text-yellow" key={`${val}-${index + 1}`}>
{word.slice(0, 2)}
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
{word.slice(word.length - 1, word.length) === "}" ? (
<span className="ph-no-capture text-yellow">
{word.slice(word.length - 1, word.length)}
</span>
) : (
<span className="ph-no-capture text-yellow-400">
{word.slice(word.length - 1, word.length)}
</span>
)}
</span>
) : (
<span key={word} className="ph-no-capture">
{word}
</span>
)
);
}, []);
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 h-10 w-full cursor-default flex-row items-center justify-center ${
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 cursor-default flex-col justify-center whitespace-pre">
<input
value={isOverridden ? secret.valueOverride : secret?.value || ""}
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
<textarea
readOnly={isReadOnly}
className={twMerge(
"ph-no-capture no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full cursor-default bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-transparent outline-none no-scrollbar",
isSecretValueHidden && "text-transparent focus:text-transparent active:text-transparent"
)}
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;
}
}}
/>
<div
className={twMerge(
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full cursor-default flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible",
isSecretValueHidden && secret?.value ? "invisible" : "visible",
isSecretValueHidden &&
secret?.value &&
"duration-50 text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400",
!secret?.value && "justify-center text-bunker-400"
)}
>
{syntaxHighlight(secret?.value)}
</div>
{isSecretValueHidden && secret?.value && (
<div className="duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden">
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
{(isOverridden ? secret.valueOverride : secret?.value || "")
?.split("")
.map((_a: string, index: number) => (
<FontAwesomeIcon
key={`${secret?.value}_${index + 1}`}
className="mr-0.5 text-xxs"
icon={faCircle}
/>
))}
{(isOverridden ? secret.valueOverride : secret?.value || "")?.split("").length ===
0 && <span className="text-sm text-bunker-400/80">EMPTY</span>}
</div>
</div>
)}
<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"
}`}
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>
);
@ -122,14 +102,14 @@ export const EnvComparisonRow = ({
);
return (
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800">
<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">
<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 h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]">
<div className="flex h-8 cursor-default flex-row items-center truncate">
<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

View File

@ -193,8 +193,8 @@ export const SecretDetailDrawer = ({
</div>
</div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
<div className="rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="font-mono">{value}</div>
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="break-all font-mono">{value}</div>
</div>
</div>
))}

View File

@ -1,8 +1,7 @@
import { useCallback } from "react";
import { useFormContext, useWatch } from "react-hook-form";
import { faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useRef } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useSyntaxHighlight, useToggle } from "@app/hooks";
import { FormData } from "../../DashboardPage.utils";
@ -13,95 +12,94 @@ type Props = {
index: number;
};
const REGEX = /([$]{.*?})/g;
const SEC_VAL_LINE_HEIGHT = 21;
const MAX_MULTI_LINE = 6;
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => {
const { register, control } = useFormContext<FormData>();
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 syntaxHighlight = useCallback((val: string) => {
if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>;
return val?.split(REGEX).map((word, i) =>
word.match(REGEX) !== null ? (
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
{word.slice(0, 2)}
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
{word.slice(word.length - 1, word.length) === "}" ? (
<span className="ph-no-capture text-yellow">
{word.slice(word.length - 1, word.length)}
</span>
) : (
<span className="ph-no-capture text-yellow-400">
{word.slice(word.length - 1, word.length)}
</span>
)}
</span>
) : (
<span key={`${word}_${i + 1}`} className="ph-no-capture">
{word}
</span>
)
);
}, []);
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 justify-center whitespace-pre px-1.5">
<div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
{isOverridden ? (
<input
{...register(`secrets.${index}.valueOverride`)}
readOnly={isReadOnly}
className={twMerge(
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar",
!isSecretValueHidden &&
"text-transparent focus:text-transparent active:text-transparent"
<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;
}
}}
/>
)}
spellCheck="false"
/>
) : (
<input
{...register(`secrets.${index}.value`)}
readOnly={isReadOnly}
className={twMerge(
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 peer z-10 w-full bg-transparent px-2 py-2 font-mono text-sm text-transparent caret-white outline-none no-scrollbar",
!isSecretValueHidden &&
"text-transparent focus:text-transparent active:text-transparent"
<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;
}
}}
/>
)}
spellCheck="false"
/>
)}
<div
className={twMerge(
"ph-no-capture min-w-16 no-scrollbar::-webkit-scrollbar duration-50 absolute z-0 mt-0.5 flex h-10 w-full flex-row overflow-x-scroll whitespace-pre bg-transparent px-2 py-2 font-mono text-sm outline-none no-scrollbar peer-focus:visible",
isSecretValueHidden ? "invisible" : "visible",
isOverridden
? "text-primary-300"
: "duration-50 text-gray-400 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400"
)}
>
{syntaxHighlight(value || "")}
</div>
<div
className={twMerge(
"duration-50 peer absolute z-0 flex h-10 w-full flex-row items-center justify-between text-clip pr-2 text-bunker-400 group-hover:bg-white/[0.00] peer-focus:hidden peer-active:hidden",
!isSecretValueHidden ? "invisible" : "visible"
)}
>
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar">
{value?.split("").map((val, i) => (
<FontAwesomeIcon
key={`${value}_${val}_${i + 1}`}
className="mr-0.5 text-xxs"
icon={faCircle}
/>
))}
{value?.split("").length === 0 && (
<span className="text-sm text-bunker-400/80">EMPTY</span>
)}
</div>
</div>
<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"
}`}
style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
>
{syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)}
</code>
</pre>
</div>
);
};

View File

@ -157,7 +157,7 @@ export const SecretInputRow = memo(
}
return (
<tr className="group flex flex-row items-center" key={index}>
<tr className="group flex flex-row" key={index}>
<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">{index + 1}</div>
</td>
@ -198,7 +198,7 @@ export const SecretInputRow = memo(
</HoverCard>
)}
/>
<td className="flex h-10 w-full flex-grow flex-row items-center justify-center border-r border-none border-red">
<td className="flex w-full flex-grow flex-row border-r border-none border-red">
<MaskedInput
isReadOnly={
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
@ -208,8 +208,8 @@ export const SecretInputRow = memo(
index={index}
/>
</td>
<td className="min-w-sm flex h-10 items-center">
<div className="flex items-center pl-2">
<td className="min-w-sm flex">
<div className="flex h-8 items-center pl-2">
{secretTags.map(({ id, slug }, i) => (
<Tag
className={cx(
@ -289,7 +289,7 @@ export const SecretInputRow = memo(
</div>
)}
</div>
<div className="flex h-full flex-row items-center pr-2">
<div className="flex h-8 flex-row items-center pr-2">
{!isAddOnly && (
<div>
<Tooltip content="Override with a personal value">
@ -346,35 +346,37 @@ export const SecretInputRow = memo(
</div>
</Tooltip>
</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">
{!isAddOnly && (
<div className="duration-0 flex w-16 justify-center overflow-hidden border-l border-mineshaft-600 pl-2 transition-all">
<div className="flex h-8 items-center space-x-2.5">
{!isAddOnly && (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings">
<IconButton
size="lg"
colorSchema="primary"
variant="plain"
onClick={onRowExpand}
ariaLabel="expand"
>
<FontAwesomeIcon icon={faEllipsis} />
</IconButton>
</Tooltip>
</div>
)}
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings">
<Tooltip content="Delete">
<IconButton
size="lg"
colorSchema="primary"
size="md"
variant="plain"
onClick={onRowExpand}
ariaLabel="expand"
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode}
onClick={() => onSecretDelete(index, secId, idOverride)}
>
<FontAwesomeIcon icon={faEllipsis} />
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
</div>
)}
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete">
<IconButton
size="md"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode}
onClick={() => onSecretDelete(index, secId, idOverride)}
>
<FontAwesomeIcon icon={faXmark} />
</IconButton>
</Tooltip>
</div>
</div>
</td>