diff --git a/frontend/src/components/v2/SecretInput/SecretInput.tsx b/frontend/src/components/v2/SecretInput/SecretInput.tsx index e815e9319..6ffb25f6f 100644 --- a/frontend/src/components/v2/SecretInput/SecretInput.tsx +++ b/frontend/src/components/v2/SecretInput/SecretInput.tsx @@ -1,12 +1,16 @@ /* 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { twMerge } from "tailwind-merge"; +import { useWorkspace } from "@app/context"; 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) => { let finalStr = ""; 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 (!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; - const formatedContent = content.split(REGEX).flatMap((el, i) => { + const formatedContent = content.split(REGEX_REFERENCE).flatMap((el, i) => { const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}"); if (isInterpolationSyntax) { skipNext = true; @@ -55,6 +55,14 @@ type Props = TextareaHTMLAttributes & { isReadOnly?: boolean; isDisabled?: boolean; 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"; @@ -65,6 +73,113 @@ export const SecretInput = forwardRef( ref ) => { const [isSecretFocused, setIsSecretFocused] = useToggle(); + const [showReferencePopup, setShowReferencePopup] = useState(false); + const { currentWorkspace } = useWorkspace(); + const [listVariables, setListVariables] = useState([]); + + 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) { + // 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) { + 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) { + handleVariablePopup(event.currentTarget); + } return (
@@ -87,11 +202,14 @@ export const SecretInput = forwardRef( ref={ref} className={`absolute inset-0 block h-full resize-none overflow-hidden bg-transparent text-transparent no-scrollbar focus:border-0 ${commonClassName}`} onFocus={() => setIsSecretFocused.on()} + onKeyDown={handleKeyDown} + onKeyUp={handleKeyUp} + onClick={handleMouseClick} disabled={isDisabled} spellCheck={false} onBlur={(evt) => { onBlur?.(evt); - setIsSecretFocused.off(); + if (!showReferencePopup) setIsSecretFocused.off(); }} value={value || ""} {...props} @@ -99,61 +217,62 @@ export const SecretInput = forwardRef( />
- {isSecretFocused && ( -
-
- {[ - { name: "SECRET NAME", type: "secret" }, - { name: "Folder", type: "folder" }, - { name: "Development", type: "environment" } - ].map((e, i) => { - return ( + {showReferencePopup && isSecretFocused && ( +
+
+ {listVariables.map((e, i) => { + return ( +
+ {e.type === "folder" && ( + <> +
+
+ +
+
{e.name}
+
+
+ +
+ + )} + + {e.type === "secret" && ( +
+
+ +
+
{e.name}
+
+ )} +
+ ); + })} + +
All Secrets
+ + {currentWorkspace?.environments.map((env, i) => (
- {e.type === "folder" && ( - <> -
-
- -
-
{e.name}
-
-
- -
- - )} - - {e.type === "environment" && ( - <> -
-
- -
-
{e.name}
-
-
- -
- - )} - - {e.type === "secret" && ( -
-
- -
-
{e.name}
+
+
+
- )} +
{env.name}
+
+
+ +
- ); - })} + ))} +
-
- )} + )}
); } diff --git a/frontend/src/hooks/api/secretFolders/queries.tsx b/frontend/src/hooks/api/secretFolders/queries.tsx index bcda2b0a4..968fe06bd 100644 --- a/frontend/src/hooks/api/secretFolders/queries.tsx +++ b/frontend/src/hooks/api/secretFolders/queries.tsx @@ -24,7 +24,7 @@ export const folderQueryKeys = { ["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", { params: { workspaceId, diff --git a/frontend/src/hooks/api/secrets/queries.tsx b/frontend/src/hooks/api/secrets/queries.tsx index 1ba9a5251..28999389e 100644 --- a/frontend/src/hooks/api/secrets/queries.tsx +++ b/frontend/src/hooks/api/secrets/queries.tsx @@ -98,7 +98,7 @@ export const decryptSecrets = ( return secrets; }; -const fetchProjectEncryptedSecrets = async ({ +export const fetchProjectEncryptedSecrets = async ({ workspaceId, environment, secretPath diff --git a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx index a8e1fb336..5743a9f74 100644 --- a/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx +++ b/frontend/src/views/SecretMainPage/components/CreateSecretForm/CreateSecretForm.tsx @@ -105,6 +105,8 @@ export const CreateSecretForm = ({ > diff --git a/frontend/src/views/SecretMainPage/components/SecretImportListView/SecretImportItem.tsx b/frontend/src/views/SecretMainPage/components/SecretImportListView/SecretImportItem.tsx index dd0f5fbec..0070ef248 100644 --- a/frontend/src/views/SecretMainPage/components/SecretImportListView/SecretImportItem.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretImportListView/SecretImportItem.tsx @@ -172,7 +172,12 @@ export const SecretImportItem = ({ {key} - + diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx index 092495597..772bb2da0 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretDetaiSidebar.tsx @@ -206,6 +206,8 @@ export const SecretDetailSidebar = ({ diff --git a/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx b/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx index 7996d932c..b459d5508 100644 --- a/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx +++ b/frontend/src/views/SecretMainPage/components/SecretListView/SecretItem.tsx @@ -267,6 +267,8 @@ export const SecretItem = memo( key="value-overriden" isVisible={isVisible} isReadOnly={isReadOnly} + environment={environment} + secretPath={secretPath} {...field} containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2" /> @@ -282,6 +284,8 @@ export const SecretItem = memo( isReadOnly={isReadOnly} key="secret-value" isVisible={isVisible} + environment={environment} + secretPath={secretPath} {...field} containerClassName="py-1.5 rounded-md transition-all group-hover:mr-2" /> diff --git a/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx b/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx index 249727655..a571ca5d3 100644 --- a/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx +++ b/frontend/src/views/SecretMainPage/components/SnapshotView/SecretItem.tsx @@ -120,7 +120,9 @@ export const SecretItem = ({ mode, preSecret, postSecret }: Props) => { Value {isModified && ( - + )} diff --git a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx index 478d99086..10c6e229a 100644 --- a/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx +++ b/frontend/src/views/SecretOverviewPage/components/SecretOverviewTableRow/SecretEditRow.tsx @@ -93,7 +93,7 @@ export const SecretEditRow = ({ control={control} name="value" render={({ field }) => ( - + )} />
diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx index ed7b571e3..c83c400ff 100644 --- a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/CreateRotationForm.tsx @@ -102,7 +102,8 @@ export const CreateRotationForm = ({ ))} - {wizardStep === 0 && ( + {/* TODO: Check this before merge */} + {wizardStep === 0 && wizardData.current.output && ( state + 1); }} inputSchema={provider.template?.inputs || {}} + secretPath={wizardData.current.output.secretPath} + environment={wizardData.current.output.environment} /> )} diff --git a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx index 125515bf1..783abfec8 100644 --- a/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx +++ b/frontend/src/views/SecretRotationPage/components/CreateRotationForm/steps/RotationInputForm.tsx @@ -13,11 +13,13 @@ type Props = { properties: Record; required: string[]; }; + secretPath: string; + environment: string; }; 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 { control, handleSubmit, @@ -60,6 +62,7 @@ export const RotationInputForm = ({ onSubmit, onCancel, inputSchema }: Props) => {...field} 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)} + secretPath={secretPath} environment={environment} /> )}