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
@@ -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 offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string); const limit: number = parseInt(req.query.limit as string);
const secretVersions = await SecretVersion.find({ const secretVersions = await SecretVersion.find({
secret: secretId, secret: secretId
workspace: workspaceId,
environment,
folder: folderId,
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);
return res.status(200).send({ return res.status(200).send({
secretVersions, secretVersions
}); });
}; };
@@ -135,7 +132,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
// validate secret version // validate secret version
const oldSecretVersion = await SecretVersion.findOne({ const oldSecretVersion = await SecretVersion.findOne({
secret: secretId, secret: secretId,
version, version
}).select("+secretBlindIndex"); }).select("+secretBlindIndex");
if (!oldSecretVersion) throw new Error("Failed to find secret version"); if (!oldSecretVersion) throw new Error("Failed to find secret version");
@@ -154,7 +151,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueTag, secretValueTag,
algorithm, algorithm,
folder, folder,
keyEncoding, keyEncoding
} = oldSecretVersion; } = oldSecretVersion;
// update secret // update secret
@@ -162,7 +159,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretId, secretId,
{ {
$inc: { $inc: {
version: 1, version: 1
}, },
workspace, workspace,
type, type,
@@ -177,10 +174,10 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueTag, secretValueTag,
folderId: folder, folderId: folder,
algorithm, algorithm,
keyEncoding, keyEncoding
}, },
{ {
new: true, new: true
} }
); );
@@ -204,17 +201,17 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueTag, secretValueTag,
folder, folder,
algorithm, algorithm,
keyEncoding, keyEncoding
}).save(); }).save();
// take secret snapshot // take secret snapshot
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace, workspaceId: secret.workspace,
environment, environment,
folderId: folder, folderId: folder
}); });
return res.status(200).send({ return res.status(200).send({
secret, secret
}); });
}; };
+162 -153
View File
@@ -81,76 +81,79 @@ export const useGetProjectSecrets = ({
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId), queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
select: useCallback((data: EncryptedSecret[]) => { select: useCallback(
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; (data: EncryptedSecret[]) => {
const latestKey = decryptFileKey; const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const key = decryptAssymmetric({ const latestKey = decryptFileKey;
ciphertext: latestKey.encryptedKey, const key = decryptAssymmetric({
nonce: latestKey.nonce, ciphertext: latestKey.encryptedKey,
publicKey: latestKey.sender.publicKey, nonce: latestKey.nonce,
privateKey: PRIVATE_KEY 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
}); });
const secretValue = decryptSymmetric({ const sharedSecrets: DecryptedSecret[] = [];
ciphertext: encSecret.secretValueCiphertext, const personalSecrets: Record<string, { id: string; value: string }> = {};
iv: encSecret.secretValueIV, // this used for add-only mode in dashboard
tag: encSecret.secretValueTag, // type won't be there thus only one key is shown
key 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({ const secretValue = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext, ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretCommentIV, iv: encSecret.secretValueIV,
tag: encSecret.secretCommentTag, tag: encSecret.secretValueTag,
key key
}); });
const decryptedSecret = { const secretComment = decryptSymmetric({
_id: encSecret._id, ciphertext: encSecret.secretCommentCiphertext,
env: encSecret.environment, iv: encSecret.secretCommentIV,
key: secretKey, tag: encSecret.secretCommentTag,
value: secretValue, key
tags: encSecret.tags, });
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
if (encSecret.type === "personal") { const decryptedSecret = {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { _id: encSecret._id,
id: encSecret._id, env: encSecret.environment,
value: secretValue key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
}; };
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) { if (encSecret.type === "personal") {
sharedSecrets.push(decryptedSecret); 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}`;
sharedSecrets.forEach((val) => { if (personalSecrets?.[dupKey]) {
const dupKey = `${val.key}-${val.env}`; val.idOverride = personalSecrets[dupKey].id;
if (personalSecrets?.[dupKey]) { val.valueOverride = personalSecrets[dupKey].value;
val.idOverride = personalSecrets[dupKey].id; val.overrideAction = "modified";
val.valueOverride = personalSecrets[dupKey].value; }
val.overrideAction = "modified"; });
} return { secrets: sharedSecrets };
}); },
return { secrets: sharedSecrets }; [decryptFileKey]
}, [decryptFileKey]) )
}); });
export const useGetProjectSecretsByKey = ({ export const useGetProjectSecretsByKey = ({
@@ -167,82 +170,85 @@ export const useGetProjectSecretsByKey = ({
// right now secretpath is passed as folderid as only this is used in overview // right now secretpath is passed as folderid as only this is used in overview
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath), queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath),
select: useCallback((data: EncryptedSecret[]) => { select: useCallback(
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; (data: EncryptedSecret[]) => {
const latestKey = decryptFileKey; const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const key = decryptAssymmetric({ const latestKey = decryptFileKey;
ciphertext: latestKey.encryptedKey, const key = decryptAssymmetric({
nonce: latestKey.nonce, ciphertext: latestKey.encryptedKey,
publicKey: latestKey.sender.publicKey, nonce: latestKey.nonce,
privateKey: PRIVATE_KEY 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
}); });
const secretComment = decryptSymmetric({ const sharedSecrets: Record<string, DecryptedSecret[]> = {};
ciphertext: encSecret.secretCommentCiphertext, const personalSecrets: Record<string, { id: string; value: string }> = {};
iv: encSecret.secretCommentIV, // this used for add-only mode in dashboard
tag: encSecret.secretCommentTag, // type won't be there thus only one key is shown
key 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 = { const secretValue = decryptSymmetric({
_id: encSecret._id, ciphertext: encSecret.secretValueCiphertext,
env: encSecret.environment, iv: encSecret.secretValueIV,
key: secretKey, tag: encSecret.secretValueTag,
value: secretValue, key
tags: encSecret.tags, });
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
if (encSecret.type === "personal") { const secretComment = decryptSymmetric({
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = { ciphertext: encSecret.secretCommentCiphertext,
id: encSecret._id, iv: encSecret.secretCommentIV,
value: secretValue 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 (encSecret.type === "personal") {
if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = []; personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
sharedSecrets[secretKey].push(decryptedSecret); id: encSecret._id,
} value: secretValue
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true; };
} } else {
}); if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
Object.keys(sharedSecrets).forEach((secName) => { if (!sharedSecrets?.[secretKey]) sharedSecrets[secretKey] = [];
sharedSecrets[secName].forEach((val) => { sharedSecrets[secretKey].push(decryptedSecret);
const dupKey = `${val.key}-${val.env}`; }
if (personalSecrets?.[dupKey]) { duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
} }
}); });
}); 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 }; return { secrets: sharedSecrets, uniqueSecCount: Object.keys(uniqSecKeys).length };
}, [decryptFileKey]) },
[decryptFileKey]
)
}); });
const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => { const fetchEncryptedSecretVersion = async (secretId: string, offset: number, limit: number) => {
@@ -263,29 +269,32 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
enabled: Boolean(dto.secretId && dto.decryptFileKey), enabled: Boolean(dto.secretId && dto.decryptFileKey),
queryKey: secretKeys.getSecretVersion(dto.secretId), queryKey: secretKeys.getSecretVersion(dto.secretId),
queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit), queryFn: () => fetchEncryptedSecretVersion(dto.secretId, dto.offset, dto.limit),
select: useCallback((data: EncryptedSecretVersion[]) => { select: useCallback(
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; (data: EncryptedSecretVersion[]) => {
const latestKey = dto.decryptFileKey; const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const key = decryptAssymmetric({ const latestKey = dto.decryptFileKey;
ciphertext: latestKey.encryptedKey, const key = decryptAssymmetric({
nonce: latestKey.nonce, ciphertext: latestKey.encryptedKey,
publicKey: latestKey.sender.publicKey, nonce: latestKey.nonce,
privateKey: PRIVATE_KEY publicKey: latestKey.sender.publicKey,
}); privateKey: PRIVATE_KEY
});
return data return data
.map((el) => ({ .map((el) => ({
createdAt: el.createdAt, createdAt: el.createdAt,
id: el._id, id: el._id,
value: decryptSymmetric({ value: decryptSymmetric({
ciphertext: el.secretValueCiphertext, ciphertext: el.secretValueCiphertext,
iv: el.secretValueIV, iv: el.secretValueIV,
tag: el.secretValueTag, tag: el.secretValueTag,
key key
}) })
})) }))
.sort((a, b) => b.createdAt.localeCompare(a.createdAt)); .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
}, []) },
[dto.decryptFileKey]
)
}); });
export const useBatchSecretsOp = () => { export const useBatchSecretsOp = () => {
+1
View File
@@ -1,4 +1,5 @@
export { useLeaveConfirm } from "./useLeaveConfirm"; export { useLeaveConfirm } from "./useLeaveConfirm";
export { usePersistentState } from "./usePersistentState"; export { usePersistentState } from "./usePersistentState";
export { usePopUp } from "./usePopUp"; export { usePopUp } from "./usePopUp";
export { useSyntaxHighlight } from "./useSyntaxHighlight";
export { useToggle } from "./useToggle"; export { useToggle } from "./useToggle";
+45
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;
};
@@ -1,8 +1,11 @@
/* eslint-disable react/jsx-no-useless-fragment */ /* eslint-disable react/jsx-no-useless-fragment */
import { useCallback, useState } from "react"; import { useCallback, useRef, useState } from "react";
import { faCircle, faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons"; import { faEye, faEyeSlash, faKey, faMinus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge";
import { useSyntaxHighlight } from "@app/hooks";
import { useToggle } from "~/hooks/useToggle";
type Props = { type Props = {
secrets: any[] | undefined; secrets: any[] | undefined;
@@ -12,7 +15,8 @@ type Props = {
userAvailableEnvs?: any[]; userAvailableEnvs?: any[];
}; };
const REGEX = /([$]{.*?})/g; const SEC_VAL_LINE_HEIGHT = 21;
const MAX_MULTI_LINE = 6;
const DashboardInput = ({ const DashboardInput = ({
isOverridden, isOverridden,
@@ -25,84 +29,60 @@ const DashboardInput = ({
isReadOnly?: boolean; isReadOnly?: boolean;
secret?: any; secret?: any;
}): JSX.Element => { }): JSX.Element => {
const syntaxHighlight = useCallback((val: string) => { const ref = useRef<HTMLElement | null>(null);
if (val === undefined) const [isFocused, setIsFocused] = useToggle();
return ( const syntaxHighlight = useSyntaxHighlight();
<span className="cursor-default font-sans text-xs italic text-red-500/80">
<FontAwesomeIcon icon={faMinus} className="mt-1" /> const value = isOverridden ? secret.valueOverride : secret?.value;
</span> const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
); const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
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>
)
);
}, []);
return ( return (
<td <td
key={`row-${secret?.key || ""}--`} 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" !(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"> <div className="group relative flex w-full flex-col whitespace-pre px-1.5 pt-1.5">
<input <textarea
value={isOverridden ? secret.valueOverride : secret?.value || ""}
readOnly={isReadOnly} readOnly={isReadOnly}
className={twMerge( value={value}
"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", 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"
isSecretValueHidden && "text-transparent focus:text-transparent active:text-transparent" style={{ height: `${maxMultilineHeight}px` }}
)}
spellCheck="false" 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 <pre className="whitespace-pre-wrap break-words">
className={twMerge( <code
"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", ref={ref}
isSecretValueHidden && secret?.value ? "invisible" : "visible", className={`absolute top-1.5 left-3.5 z-10 overflow-auto font-mono text-sm transition-all no-scrollbar ${
isSecretValueHidden && isOverridden && "text-primary-300"
secret?.value && }`}
"duration-50 text-bunker-800 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400", style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
!secret?.value && "justify-center text-bunker-400" >
)} {value === undefined ? (
> <span className="cursor-default font-sans text-xs italic text-red-500/80">
{syntaxHighlight(secret?.value)} <FontAwesomeIcon icon={faMinus} className="mt-1" />
</div> </span>
{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"> syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)
<div className="no-scrollbar::-webkit-scrollbar flex flex-row items-center overflow-x-scroll px-2 no-scrollbar"> )}
{(isOverridden ? secret.valueOverride : secret?.value || "") </code>
?.split("") </pre>
.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>
)}
</div> </div>
</td> </td>
); );
@@ -122,14 +102,14 @@ export const EnvComparisonRow = ({
); );
return ( return (
<tr className="group flex min-w-full flex-row items-center hover:bg-mineshaft-800"> <tr className="group flex min-w-full flex-row hover:bg-mineshaft-800">
<td className="flex h-10 w-10 items-center justify-center border-none px-4"> <td className="flex w-10 justify-center border-none px-4">
<div className="w-10 text-center text-xs text-bunker-400"> <div className="flex h-8 w-10 items-center justify-center text-center text-xs text-bunker-400">
<FontAwesomeIcon icon={faKey} /> <FontAwesomeIcon icon={faKey} />
</div> </div>
</td> </td>
<td className="flex h-full min-w-[200px] flex-row items-center justify-between lg:min-w-[220px] xl:min-w-[250px]"> <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 truncate"> <div className="flex h-8 cursor-default flex-row items-center justify-center truncate">
{secrets![0].key || ""} {secrets![0].key || ""}
</div> </div>
<button <button
@@ -193,8 +193,8 @@ export const SecretDetailDrawer = ({
</div> </div>
</div> </div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4"> <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="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="font-mono">{value}</div> <div className="break-all font-mono">{value}</div>
</div> </div>
</div> </div>
))} ))}
@@ -1,8 +1,7 @@
import { useCallback } from "react"; import { useRef } from "react";
import { useFormContext, useWatch } from "react-hook-form"; import { Controller, useFormContext, useWatch } from "react-hook-form";
import { faCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSyntaxHighlight, useToggle } from "@app/hooks";
import { twMerge } from "tailwind-merge";
import { FormData } from "../../DashboardPage.utils"; import { FormData } from "../../DashboardPage.utils";
@@ -13,95 +12,94 @@ type Props = {
index: number; index: number;
}; };
const REGEX = /([$]{.*?})/g; const SEC_VAL_LINE_HEIGHT = 21;
const MAX_MULTI_LINE = 6;
export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridden }: Props) => { 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 secretValue = useWatch({ control, name: `secrets.${index}.value` });
const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` }); const secretValueOverride = useWatch({ control, name: `secrets.${index}.valueOverride` });
const value = isOverridden ? secretValueOverride : secretValue; const value = isOverridden ? secretValueOverride : secretValue;
const syntaxHighlight = useCallback((val: string) => { const multilineExpandUnit = ((value?.match(/\n/g)?.length || 0) + 1) * SEC_VAL_LINE_HEIGHT;
if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>; const maxMultilineHeight = Math.min(multilineExpandUnit, 21 * MAX_MULTI_LINE);
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>
)
);
}, []);
return ( 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 ? ( {isOverridden ? (
<input <Controller
{...register(`secrets.${index}.valueOverride`)} control={control}
readOnly={isReadOnly} name={`secrets.${index}.valueOverride`}
className={twMerge( render={({ field }) => (
"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", <textarea
!isSecretValueHidden && key={`secrets.${index}.valueOverride`}
"text-transparent focus:text-transparent active:text-transparent" {...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 <Controller
{...register(`secrets.${index}.value`)} control={control}
readOnly={isReadOnly} name={`secrets.${index}.value`}
className={twMerge( key={`secrets.${index}.value`}
"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", render={({ field }) => (
!isSecretValueHidden && <textarea
"text-transparent focus:text-transparent active:text-transparent" {...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 <pre className="whitespace-pre-wrap break-words">
className={twMerge( <code
"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", ref={ref}
isSecretValueHidden ? "invisible" : "visible", className={`absolute top-1.5 left-3.5 z-10 w-full overflow-auto font-mono text-sm transition-all no-scrollbar ${
isOverridden isOverridden && "text-primary-300"
? "text-primary-300" }`}
: "duration-50 text-gray-400 group-hover:text-gray-400 peer-focus:text-gray-100 peer-active:text-gray-400" style={{ height: `${maxMultilineHeight}px`, width: "calc(100% - 12px)" }}
)} >
> {syntaxHighlight(value || "", isSecretValueHidden ? !isFocused : isSecretValueHidden)}
{syntaxHighlight(value || "")} </code>
</div> </pre>
<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>
</div> </div>
); );
}; };
@@ -157,7 +157,7 @@ export const SecretInputRow = memo(
} }
return ( 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"> <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> <div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
</td> </td>
@@ -198,7 +198,7 @@ export const SecretInputRow = memo(
</HoverCard> </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 <MaskedInput
isReadOnly={ isReadOnly={
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly) isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
@@ -208,8 +208,8 @@ export const SecretInputRow = memo(
index={index} index={index}
/> />
</td> </td>
<td className="min-w-sm flex h-10 items-center"> <td className="min-w-sm flex">
<div className="flex items-center pl-2"> <div className="flex h-8 items-center pl-2">
{secretTags.map(({ id, slug }, i) => ( {secretTags.map(({ id, slug }, i) => (
<Tag <Tag
className={cx( className={cx(
@@ -289,7 +289,7 @@ export const SecretInputRow = memo(
</div> </div>
)} )}
</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 && ( {!isAddOnly && (
<div> <div>
<Tooltip content="Override with a personal value"> <Tooltip content="Override with a personal value">
@@ -346,35 +346,37 @@ export const SecretInputRow = memo(
</div> </div>
</Tooltip> </Tooltip>
</div> </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="duration-0 flex w-16 justify-center overflow-hidden border-l border-mineshaft-600 pl-2 transition-all">
{!isAddOnly && ( <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"> <div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings"> <Tooltip content="Delete">
<IconButton <IconButton
size="lg" size="md"
colorSchema="primary"
variant="plain" variant="plain"
onClick={onRowExpand} colorSchema="danger"
ariaLabel="expand" ariaLabel="delete"
isDisabled={isReadOnly || isRollbackMode}
onClick={() => onSecretDelete(index, secId, idOverride)}
> >
<FontAwesomeIcon icon={faEllipsis} /> <FontAwesomeIcon icon={faXmark} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </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>
</div> </div>
</td> </td>