feat: fetch folder and secrets

This commit is contained in:
Salman
2024-03-14 09:39:57 +05:30
parent 126e385046
commit 1fc9e60254
11 changed files with 205 additions and 63 deletions
@@ -1,12 +1,16 @@
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import { forwardRef, TextareaHTMLAttributes } from "react"; import React, { forwardRef, TextareaHTMLAttributes, useState } from "react";
import { faChevronRight, faFolder, faKey, faRecycle } from "@fortawesome/free-solid-svg-icons"; import { faChevronRight, faFolder, faKey, faRecycle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { useWorkspace } from "@app/context";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { useGetUserWsKey } from "@app/hooks/api";
import { fetchProjectFolders } from "@app/hooks/api/secretFolders/queries";
import { decryptSecrets, fetchProjectEncryptedSecrets } from "@app/hooks/api/secrets/queries";
const REGEX = /(\${([^}]+)})/g; const REGEX_REFERENCE = /(\${([^}]*)})/g;
const replaceContentWithDot = (str: string) => { const replaceContentWithDot = (str: string) => {
let finalStr = ""; let finalStr = "";
for (let i = 0; i < str.length; i += 1) { for (let i = 0; i < str.length; i += 1) {
@@ -21,12 +25,8 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
if (!content) return "EMPTY"; if (!content) return "EMPTY";
if (!isVisible) return replaceContentWithDot(content); if (!isVisible) return replaceContentWithDot(content);
// List all the all the variable and the enviroments
// On Environment select list all the secret name and folder
//
let skipNext = false; let skipNext = false;
const formatedContent = content.split(REGEX).flatMap((el, i) => { const formatedContent = content.split(REGEX_REFERENCE).flatMap((el, i) => {
const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}"); const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
if (isInterpolationSyntax) { if (isInterpolationSyntax) {
skipNext = true; skipNext = true;
@@ -55,6 +55,14 @@ type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
isReadOnly?: boolean; isReadOnly?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
containerClassName?: string; containerClassName?: string;
environment?: string;
secretPath?: string;
};
type VariableType = {
name: string;
type: "folder" | "secret";
slug?: string;
}; };
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all"; const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
@@ -65,6 +73,113 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
ref ref
) => { ) => {
const [isSecretFocused, setIsSecretFocused] = useToggle(); const [isSecretFocused, setIsSecretFocused] = useToggle();
const [showReferencePopup, setShowReferencePopup] = useState<boolean>(false);
const { currentWorkspace } = useWorkspace();
const [listVariables, setListVariables] = useState<VariableType[]>([]);
const workspaceId = currentWorkspace?.id || "";
const { data: decryptFileKey } = useGetUserWsKey(workspaceId);
const { environment, secretPath } = props;
async function extractReference(refValue: string, refIndex: number) {
console.log({ refIndex });
const isNested = refValue.includes(".");
const currentListVariable: VariableType[] = [];
let currentEnvironment = environment;
let currentSecretPath = secretPath || "/";
if (isNested) {
const [envSlug, ...folderPaths] = refValue.split(".");
currentEnvironment = envSlug;
currentSecretPath = `/${folderPaths?.join("/")}` || "/";
}
if (!currentEnvironment || !decryptFileKey || !currentSecretPath || !currentWorkspace) {
setListVariables(currentListVariable);
return;
}
console.log({ currentEnvironment, currentSecretPath });
const [encryptSecrets, folders] = await Promise.all([
fetchProjectEncryptedSecrets({
workspaceId,
environment: currentEnvironment,
secretPath: currentSecretPath
}),
// secret reference based on folder only support for nested reference that start with envs
isNested ? fetchProjectFolders(workspaceId, currentEnvironment, currentSecretPath) : []
]);
folders?.forEach((folder) => {
currentListVariable.unshift({ name: folder.name, type: "folder" });
});
const secrets = decryptSecrets(encryptSecrets, decryptFileKey);
secrets?.forEach((secret) => {
currentListVariable.unshift({ name: secret.key, type: "secret" });
});
// get list of secrets, folder name and envs
// On env select get list of secrets
// on env select show list of secrets and folder
// on env or folder select replace the text and update the caret?
// fetch secrets based on current base environment and the path
setListVariables(currentListVariable);
}
function handleVariablePopup(element: HTMLTextAreaElement) {
const { selectionStart, selectionEnd, value: elValue } = element;
if (selectionStart !== selectionEnd || selectionStart === 0) {
setShowReferencePopup(false);
return;
}
let match = null;
for (
let matches = REGEX_REFERENCE.exec(elValue);
matches !== null;
matches = REGEX_REFERENCE.exec(elValue)
) {
if (matches.index <= selectionStart && REGEX_REFERENCE.lastIndex >= selectionStart) {
match = matches?.[2];
extractReference(match, matches.index);
}
}
setShowReferencePopup(Boolean(match));
}
function handleKeyDown(event: React.KeyboardEvent<HTMLTextAreaElement>) {
// On Key up or down if the popup is open ignore it
if ((showReferencePopup && event.key === "ArrowUp") || event.key === "ArrowDown") {
event.preventDefault();
// todo: point up or down in the variable popup
// return;
}
}
function handleKeyUp(event: React.KeyboardEvent<HTMLTextAreaElement>) {
if (event.key === "Escape") {
setShowReferencePopup(false);
return;
}
// On Key up or down if the popup is open ignore it
if ((showReferencePopup && event.key === "ArrowUp") || event.key === "ArrowDown") {
event.preventDefault();
// todo: point up or down in the variable popup
// return;
}
handleVariablePopup(event.currentTarget);
}
function handleMouseClick(event: React.MouseEvent<HTMLTextAreaElement, MouseEvent>) {
handleVariablePopup(event.currentTarget);
}
return ( return (
<div> <div>
@@ -87,11 +202,14 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
ref={ref} ref={ref}
className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`} className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`}
onFocus={() => setIsSecretFocused.on()} onFocus={() => setIsSecretFocused.on()}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onClick={handleMouseClick}
disabled={isDisabled} disabled={isDisabled}
spellCheck={false} spellCheck={false}
onBlur={(evt) => { onBlur={(evt) => {
onBlur?.(evt); onBlur?.(evt);
setIsSecretFocused.off(); if (!showReferencePopup) setIsSecretFocused.off();
}} }}
value={value || ""} value={value || ""}
{...props} {...props}
@@ -99,61 +217,62 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
/> />
</div> </div>
</div> </div>
{isSecretFocused && ( {showReferencePopup && isSecretFocused && (
<div className="absolute z-10 mt-2 w-60 rounded-md border border-mineshaft-600 bg-mineshaft-700 text-sm text-bunker-200"> <div className="absolute z-10 mt-2 w-60 rounded-md border border-mineshaft-600 bg-mineshaft-700 text-sm text-bunker-200">
<div className="h-full w-full flex-col items-center justify-center rounded-md py-4 text-white"> <div className="max-w-60 h-full w-full flex-col items-center justify-center rounded-md py-4 text-white">
{[ {listVariables.map((e, i) => {
{ name: "SECRET NAME", type: "secret" }, return (
{ name: "Folder", type: "folder" }, <div
{ name: "Development", type: "environment" } className="flex items-center justify-between border-b border-mineshaft-600 px-2 py-1 last:border-b-0"
].map((e, i) => { key={`key-${i + 1}`}
return ( >
{e.type === "folder" && (
<>
<div className="flex gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon icon={faFolder} />
</div>
<div className="w-48 truncate">{e.name}</div>
</div>
<div className="flex items-center text-bunker-200">
<FontAwesomeIcon icon={faChevronRight} />
</div>
</>
)}
{e.type === "secret" && (
<div className="flex gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon icon={faKey} />
</div>
<div className="w-48 truncate">{e.name}</div>
</div>
)}
</div>
);
})}
<div className="flex w-full justify-center gap-2">All Secrets</div>
{currentWorkspace?.environments.map((env, i) => (
<div <div
className="flex items-center justify-between border-b border-mineshaft-600 px-2 py-1 last:border-b-0" className="flex items-center justify-between border-b border-mineshaft-600 px-2 py-1 last:border-b-0"
key={`key-${i + 1}`} key={`key-${i + 1}`}
> >
{e.type === "folder" && ( <div className="flex gap-2">
<> <div className="flex items-center text-yellow-700">
<div className="flex gap-2"> <FontAwesomeIcon icon={faRecycle} />
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon icon={faFolder} />
</div>
<div>{e.name}</div>
</div>
<div className="flex items-center text-bunker-200">
<FontAwesomeIcon icon={faChevronRight} />
</div>
</>
)}
{e.type === "environment" && (
<>
<div className="flex gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon icon={faRecycle} />
</div>
<div>{e.name}</div>
</div>
<div className="flex items-center text-bunker-200">
<FontAwesomeIcon icon={faChevronRight} />
</div>
</>
)}
{e.type === "secret" && (
<div className="flex gap-2">
<div className="flex items-center text-yellow-700">
<FontAwesomeIcon icon={faKey} />
</div>
<div>{e.name}</div>
</div> </div>
)} <div className="w-48 truncate">{env.name}</div>
</div>
<div className="flex items-center text-bunker-200">
<FontAwesomeIcon icon={faChevronRight} />
</div>
</div> </div>
); ))}
})} </div>
</div> </div>
</div> )}
)}
</div> </div>
); );
} }
@@ -24,7 +24,7 @@ export const folderQueryKeys = {
["secret-folders", { projectId, environment, path }] as const ["secret-folders", { projectId, environment, path }] as const
}; };
const fetchProjectFolders = async (workspaceId: string, environment: string, path = "/") => { export const fetchProjectFolders = async (workspaceId: string, environment: string, path = "/") => {
const { data } = await apiRequest.get<{ folders: TSecretFolder[] }>("/api/v1/folders", { const { data } = await apiRequest.get<{ folders: TSecretFolder[] }>("/api/v1/folders", {
params: { params: {
workspaceId, workspaceId,
+1 -1
View File
@@ -98,7 +98,7 @@ export const decryptSecrets = (
return secrets; return secrets;
}; };
const fetchProjectEncryptedSecrets = async ({ export const fetchProjectEncryptedSecrets = async ({
workspaceId, workspaceId,
environment, environment,
secretPath secretPath
@@ -105,6 +105,8 @@ export const CreateSecretForm = ({
> >
<SecretInput <SecretInput
{...field} {...field}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5" containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-mineshaft-900 px-2 py-1.5"
/> />
</FormControl> </FormControl>
@@ -172,7 +172,12 @@ export const SecretImportItem = ({
{key} {key}
</td> </td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}> <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<SecretInput value={value} isReadOnly /> <SecretInput
value={value}
isReadOnly
environment={overriden?.env}
secretPath={overriden?.secretPath}
/>
</td> </td>
<td className="h-10" style={{ padding: "0.25rem 1rem" }}> <td className="h-10" style={{ padding: "0.25rem 1rem" }}>
<EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} /> <EnvFolderIcon env={overriden?.env} secretPath={overriden?.secretPath} />
@@ -206,6 +206,8 @@ export const SecretDetailSidebar = ({
<FormControl label="Value"> <FormControl label="Value">
<SecretInput <SecretInput
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
key="secret-value" key="secret-value"
isDisabled={isOverridden || !isAllowed} isDisabled={isOverridden || !isAllowed}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5" containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
@@ -242,6 +244,8 @@ export const SecretDetailSidebar = ({
<FormControl label="Value Override"> <FormControl label="Value Override">
<SecretInput <SecretInput
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5" containerClassName="text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
{...field} {...field}
/> />
@@ -267,6 +267,8 @@ export const SecretItem = memo(
key="value-overriden" key="value-overriden"
isVisible={isVisible} isVisible={isVisible}
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
environment={environment}
secretPath={secretPath}
{...field} {...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2" containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/> />
@@ -282,6 +284,8 @@ export const SecretItem = memo(
isReadOnly={isReadOnly} isReadOnly={isReadOnly}
key="secret-value" key="secret-value"
isVisible={isVisible} isVisible={isVisible}
environment={environment}
secretPath={secretPath}
{...field} {...field}
containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2" containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2"
/> />
@@ -120,7 +120,9 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => {
<Td className="border-r border-mineshaft-600">Value</Td> <Td className="border-r border-mineshaft-600">Value</Td>
{isModified && ( {isModified && (
<Td className="border-r border-mineshaft-600"> <Td className="border-r border-mineshaft-600">
<SecretInput value={preSecret?.value} /> <SecretInput
value={preSecret?.value}
/>
</Td> </Td>
)} )}
<Td> <Td>
@@ -93,7 +93,7 @@ export const SecretEditRow = ({
control={control} control={control}
name="value" name="value"
render={({ field }) => ( render={({ field }) => (
<SecretInput {...field} value={field.value as string} isVisible={isVisible} /> <SecretInput {...field} value={field.value as string} isVisible={isVisible} secretPath={secretPath} environment={environment} />
)} )}
/> />
</div> </div>
@@ -102,7 +102,8 @@ export const CreateRotationForm = ({
))} ))}
</Stepper> </Stepper>
<AnimatePresence exitBeforeEnter> <AnimatePresence exitBeforeEnter>
{wizardStep === 0 && ( {/* TODO: Check this before merge */}
{wizardStep === 0 && wizardData.current.output && (
<motion.div <motion.div
key="input-step" key="input-step"
transition={{ duration: 0.1 }} transition={{ duration: 0.1 }}
@@ -117,6 +118,8 @@ export const CreateRotationForm = ({
setWizardStep((state) => state + 1); setWizardStep((state) => state + 1);
}} }}
inputSchema={provider.template?.inputs || {}} inputSchema={provider.template?.inputs || {}}
secretPath={wizardData.current.output.secretPath}
environment={wizardData.current.output.environment}
/> />
</motion.div> </motion.div>
)} )}
@@ -13,11 +13,13 @@ type Props = {
properties: Record<string, { type: string; desc?: string; default?: string }>; properties: Record<string, { type: string; desc?: string; default?: string }>;
required: string[]; required: string[];
}; };
secretPath: string;
environment: string;
}; };
const formSchema = z.record(z.string().trim().optional()); const formSchema = z.record(z.string().trim().optional());
export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) => { export const RotationInputForm = ({ onSubmit, onCancel, inputSchema, secretPath, environment }: Props) => {
const { const {
control, control,
handleSubmit, handleSubmit,
@@ -60,6 +62,7 @@ export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) =>
{...field} {...field}
containerClassName="normal-case text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5" containerClassName="normal-case text-bunker-300 hover:border-primary-400/50 border border-mineshaft-600 bg-bunker-800 px-2 py-1.5"
required={inputSchema.required.includes(inputName)} required={inputSchema.required.includes(inputName)}
secretPath={secretPath} environment={environment}
/> />
</FormControl> </FormControl>
)} )}