From 3b284b4fc693c98240dafe9336af0244e3142a5d Mon Sep 17 00:00:00 2001 From: Nasyarobby Putra Date: Thu, 20 Aug 2026 22:31:53 +0700 Subject: [PATCH] feat(web): peek variable values in workflow config field refs Co-authored-by: Cursor --- .../src/components/workflow/ConfigFields.jsx | 138 ++++++++++++++++-- 1 file changed, 127 insertions(+), 11 deletions(-) diff --git a/packages/web/src/components/workflow/ConfigFields.jsx b/packages/web/src/components/workflow/ConfigFields.jsx index 365a4f4..373fcd1 100644 --- a/packages/web/src/components/workflow/ConfigFields.jsx +++ b/packages/web/src/components/workflow/ConfigFields.jsx @@ -1,10 +1,13 @@ import { useEffect, useState } from "react"; -import { LuPlus, LuTrash2 } from "react-icons/lu"; +import { Link } from "react-router-dom"; +import { LuEye, LuEyeOff, LuExternalLink, LuPlus, LuTrash2 } from "react-icons/lu"; +import { useVariables } from "../../api/hooks.js"; import { triggerDestinations } from "../../lib/workflow-doc.js"; import { prettyJson } from "../../lib/script.js"; import { FieldLabel } from "./FieldHelp.jsx"; const MULTILINE_KEYS = new Set(["expression", "jsonata"]); +const VAR_PEEK_MAX = 48; const CONFIG_REF_PREFIXES = [ { prefix: "$SECRET_", label: "secret" }, @@ -17,19 +20,131 @@ function describeConfigRef(value) { const trimmed = value.trim(); for (const { prefix, label } of CONFIG_REF_PREFIXES) { if (trimmed.startsWith(prefix) && trimmed.length > prefix.length) { - return { label, name: trimmed.slice(prefix.length) }; + return { label, name: trimmed.slice(prefix.length), kind: prefix }; } } return null; } -function ConfigRefHint({ value }) { +function formatVarDisplay(value) { + if (typeof value === "string") return value === "" ? '""' : value; + return String(value); +} + +function truncatePeek(text, maxLen = VAR_PEEK_MAX) { + if (!text) return ""; + if (text.length <= maxLen) return text; + return `${text.slice(0, Math.max(0, maxLen - 1))}…`; +} + +/** Edit-time lookup for `$VAR_` against workflow owner (not a runtime guarantee). */ +function lookupVariable(variables, owner, name) { + const list = Array.isArray(variables) ? variables : []; + const match = list.find((v) => v.owner === owner && v.name === name); + if (match) { + return { status: "found", value: match.value, type: match.type }; + } + const otherOwners = [ + ...new Set(list.filter((v) => v.name === name && v.owner !== owner).map((v) => v.owner)), + ]; + if (otherOwners.length > 0) { + return { status: "other_owner", otherOwners }; + } + return { status: "missing" }; +} + +function variablesDeepLink({ owner, name, missing }) { + if (missing) { + const params = new URLSearchParams(); + if (owner) params.set("owner", owner); + if (name) params.set("name", name); + const q = params.toString(); + return q ? `/variables/new?${q}` : "/variables/new"; + } + return `/variables/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/edit`; +} + +function ConfigRefHint({ value, owner }) { const ref = describeConfigRef(value); + const isVar = ref?.kind === "$VAR_"; + const { data: variables = [], isPending } = useVariables(undefined, { enabled: isVar }); + const [revealed, setRevealed] = useState(false); + + useEffect(() => { + setRevealed(false); + }, [value, owner]); + if (!ref) return null; + + if (!isVar) { + return ( +

+ from {ref.label} {ref.name} +

+ ); + } + + const lookup = !owner + ? { status: "missing" } + : isPending + ? { status: "loading" } + : lookupVariable(variables, owner, ref.name); + const linkTo = variablesDeepLink({ + owner: owner || "", + name: ref.name, + missing: lookup.status !== "found", + }); + + let statusNote = null; + if (lookup.status === "missing") { + statusNote = (missing); + } else if (lookup.status === "other_owner") { + statusNote = ( + + (missing · other owner: {lookup.otherOwners.join(", ")}) + + ); + } + + const fullText = lookup.status === "found" ? formatVarDisplay(lookup.value) : ""; + const peek = truncatePeek(fullText); + const masked = lookup.status === "found" ? "******" : null; + return ( -

- from {ref.label} {ref.name} -

+
+ + from variable {ref.name} + {lookup.status === "found" ? ":" : null} + + {lookup.status === "found" ? ( + <> + + {revealed ? peek : masked} + + + + ) : null} + {statusNote} + {lookup.status === "loading" ? null : ( + + {lookup.status === "found" ? "Open Variables" : "Create on Variables"} + + + )} +
); } @@ -185,7 +300,7 @@ function ValueEditor({ value, onChange, spec, script, fieldKey, workflows, owner if (type === "object" || (value != null && typeof value === "object" && !Array.isArray(value) && type !== "any")) { const obj = value && typeof value === "object" && !Array.isArray(value) ? value : {}; - return ; + return ; } if (type === "any") { @@ -242,7 +357,7 @@ function JsonOrTextArea({ text, onCommit, disabled }) { ); } -function ObjectFields({ value, onChange }) { +function ObjectFields({ value, onChange, owner }) { const entries = Object.entries(value ?? {}); function setKey(oldKey, nextKey) { @@ -286,6 +401,7 @@ function ObjectFields({ value, onChange }) { setVal(k, nv)} + owner={owner} /> ) : (
@@ -294,7 +410,7 @@ function ObjectFields({ value, onChange }) { onCommit={(nv) => setVal(k, nv)} className="font-mono text-xs" /> - +
)} @@ -425,7 +541,7 @@ export function ConfigFields({ excludeFile={excludeFile} disabled={disabled} /> - + ); })} @@ -461,7 +577,7 @@ export function ConfigFields({ excludeFile={excludeFile} disabled={disabled} /> - + ))}