refactor: split oversized modules and start server src/stores layout
Consolidate format helpers under lib/format, split React Query hooks by domain, extract AuthPicker/ConfigRefHint and HTTP trigger routing, demote file-private exports, and move KV/secrets/variables/profiles/http-auths stores under src/stores with root re-exports. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { errorMessage } from "../api/client.js";
|
||||
import { HeartbeatsToolbar } from "./HeartbeatsToolbar.jsx";
|
||||
import { useNotifications } from "../notifications.jsx";
|
||||
import { formatBytes, formatCpu, formatDuration } from "../lib/format.js";
|
||||
import { formatBytes, formatCpu, formatDuration } from "../lib/format";
|
||||
|
||||
const MAX_WORKERS = 32;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { LuX } from "react-icons/lu";
|
||||
import { levelName } from "../lib/format.jsx";
|
||||
import { levelName } from "../lib/format";
|
||||
|
||||
export function LogViewer({ logs = [], filters = [], onRemoveFilter, className = "" }) {
|
||||
const [wordWrap, setWordWrap] = useState(true);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatBytes, formatCpu } from "../lib/format.js";
|
||||
import { formatBytes, formatCpu } from "../lib/format";
|
||||
|
||||
export function ProcessResourcesCard({ children }) {
|
||||
const totals = children?.totals ?? { memory: 0, cpu: 0 };
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { LuChevronDown, LuX } from "react-icons/lu";
|
||||
import { FormInput } from "../FormControls.jsx";
|
||||
|
||||
/**
|
||||
* Searchable dropdown: pick an auth to add; chips with X to remove.
|
||||
* YAML stores profile UUIDs; UI shows names.
|
||||
*/
|
||||
export function AuthPicker({ auths, selectedIds, inlineCount, disabled, onChange }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const rootRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
||||
const byId = useMemo(() => new Map(auths.map((a) => [a.id, a])), [auths]);
|
||||
|
||||
const available = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
return auths
|
||||
.filter((a) => !selectedSet.has(a.id))
|
||||
.filter((a) => {
|
||||
if (!q) return true;
|
||||
return (
|
||||
a.name.toLowerCase().includes(q) ||
|
||||
String(a.type).toLowerCase().includes(q) ||
|
||||
a.id.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}, [auths, selectedSet, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function onDoc(e) {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target)) {
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
}, [open]);
|
||||
|
||||
function addAuth(id) {
|
||||
if (selectedSet.has(id)) return;
|
||||
onChange([...selectedIds, id]);
|
||||
setQuery("");
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
function removeAuth(id) {
|
||||
onChange(selectedIds.filter((x) => x !== id));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-1" ref={rootRef}>
|
||||
<span className="text-sm font-medium opacity-80">Auth (any of)</span>
|
||||
|
||||
{selectedIds.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{selectedIds.map((id) => {
|
||||
const auth = byId.get(id);
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
className="badge badge-outline gap-1 h-7 px-2 font-normal"
|
||||
title={id}
|
||||
>
|
||||
<span className="max-w-[10rem] truncate">
|
||||
{auth ? (
|
||||
<>
|
||||
{auth.name}
|
||||
<span className="opacity-60"> · {auth.type}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="opacity-60">missing</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-xs btn-square -mr-1"
|
||||
title="Remove"
|
||||
aria-label={`Remove ${auth?.name ?? id}`}
|
||||
disabled={disabled}
|
||||
onClick={() => removeAuth(id)}
|
||||
>
|
||||
<LuX className="size-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="relative w-full">
|
||||
<div className="join w-full">
|
||||
<FormInput
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
className="join-item w-full min-w-0"
|
||||
placeholder={auths.length === 0 ? "No auth profiles yet" : "Add auth…"}
|
||||
value={query}
|
||||
disabled={disabled || auths.length === 0}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (available[0]) addAuth(available[0].id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm join-item btn-square"
|
||||
disabled={disabled || auths.length === 0}
|
||||
aria-label="Open auth list"
|
||||
onClick={() => {
|
||||
setOpen((v) => !v);
|
||||
if (!open) inputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<LuChevronDown className={`size-4 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
{open && !disabled && auths.length > 0 ? (
|
||||
<ul className="menu menu-sm absolute z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-box border border-base-300 bg-base-100 shadow-lg p-1">
|
||||
{available.length === 0 ? (
|
||||
<li className="disabled">
|
||||
<span className="opacity-60">
|
||||
{query.trim() ? "No matches" : "All profiles selected"}
|
||||
</span>
|
||||
</li>
|
||||
) : (
|
||||
available.map((a) => (
|
||||
<li key={a.id}>
|
||||
<button type="button" onClick={() => addAuth(a.id)}>
|
||||
<span className="font-mono text-sm">{a.name}</span>
|
||||
<span className="opacity-60 text-xs">{a.type}</span>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{inlineCount > 0 ? (
|
||||
<span className="text-xs opacity-60">
|
||||
+ {inlineCount} inline auth{inlineCount === 1 ? "" : "s"} (edit in YAML)
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,153 +1,12 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LuEye, LuEyeOff, LuExternalLink, LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
import { useVariables } from "../../api/hooks.js";
|
||||
import { LuPlus, LuTrash2 } from "react-icons/lu";
|
||||
import { triggerDestinations } from "../../lib/workflow-doc.js";
|
||||
import { prettyJson } from "../../lib/script.js";
|
||||
import { FormInput, FormSelect, FormTextarea } from "../FormControls.jsx";
|
||||
import { FieldLabel } from "./FieldHelp.jsx";
|
||||
import { ConfigRefHint } from "./ConfigRefHint.jsx";
|
||||
|
||||
const MULTILINE_KEYS = new Set(["expression", "jsonata"]);
|
||||
const VAR_PEEK_MAX = 48;
|
||||
|
||||
const CONFIG_REF_PREFIXES = [
|
||||
{ prefix: "$SECRET_", label: "secret" },
|
||||
{ prefix: "$CONTEXT_", label: "context" },
|
||||
{ prefix: "$VAR_", label: "variable" },
|
||||
];
|
||||
|
||||
function describeConfigRef(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
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), kind: prefix };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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 (
|
||||
<p className="text-xs opacity-60">
|
||||
from {ref.label} <span className="font-mono">{ref.name}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
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 = <span className="text-warning">(missing)</span>;
|
||||
} else if (lookup.status === "other_owner") {
|
||||
statusNote = (
|
||||
<span className="text-warning">
|
||||
(missing · other owner: {lookup.otherOwners.join(", ")})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const fullText = lookup.status === "found" ? formatVarDisplay(lookup.value) : "";
|
||||
const peek = truncatePeek(fullText);
|
||||
const masked = lookup.status === "found" ? "******" : null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs opacity-60">
|
||||
<span>
|
||||
from variable <span className="font-mono">{ref.name}</span>
|
||||
{lookup.status === "found" ? ":" : null}
|
||||
</span>
|
||||
{lookup.status === "found" ? (
|
||||
<>
|
||||
<span className="font-mono" title={revealed ? fullText : undefined}>
|
||||
{revealed ? peek : masked}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-xs btn-square h-5 min-h-0 w-5"
|
||||
aria-label={revealed ? "Hide value" : "Show value"}
|
||||
title={revealed ? "Hide value" : "Show value"}
|
||||
onClick={() => setRevealed((v) => !v)}
|
||||
>
|
||||
{revealed ? <LuEyeOff className="size-3" /> : <LuEye className="size-3" />}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{statusNote}
|
||||
{lookup.status === "loading" ? null : (
|
||||
<Link
|
||||
to={linkTo}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link link-hover inline-flex items-center gap-0.5"
|
||||
>
|
||||
{lookup.status === "found" ? "Open Variables" : "Create on Variables"}
|
||||
<LuExternalLink className="size-3" aria-hidden />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function fieldSpec(meta, key) {
|
||||
const spec = meta?.config?.[key];
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LuEye, LuEyeOff, LuExternalLink } from "react-icons/lu";
|
||||
import { useVariables } from "../../api/hooks.js";
|
||||
|
||||
const VAR_PEEK_MAX = 48;
|
||||
|
||||
const CONFIG_REF_PREFIXES = [
|
||||
{ prefix: "$SECRET_", label: "secret" },
|
||||
{ prefix: "$CONTEXT_", label: "context" },
|
||||
{ prefix: "$VAR_", label: "variable" },
|
||||
];
|
||||
|
||||
function describeConfigRef(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
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), kind: prefix };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
export 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 (
|
||||
<p className="text-xs opacity-60">
|
||||
from {ref.label} <span className="font-mono">{ref.name}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
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 = <span className="text-warning">(missing)</span>;
|
||||
} else if (lookup.status === "other_owner") {
|
||||
statusNote = (
|
||||
<span className="text-warning">
|
||||
(missing · other owner: {lookup.otherOwners.join(", ")})
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const fullText = lookup.status === "found" ? formatVarDisplay(lookup.value) : "";
|
||||
const peek = truncatePeek(fullText);
|
||||
const masked = lookup.status === "found" ? "******" : null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs opacity-60">
|
||||
<span>
|
||||
from variable <span className="font-mono">{ref.name}</span>
|
||||
{lookup.status === "found" ? ":" : null}
|
||||
</span>
|
||||
{lookup.status === "found" ? (
|
||||
<>
|
||||
<span className="font-mono" title={revealed ? fullText : undefined}>
|
||||
{revealed ? peek : masked}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-xs btn-square h-5 min-h-0 w-5"
|
||||
aria-label={revealed ? "Hide value" : "Show value"}
|
||||
title={revealed ? "Hide value" : "Show value"}
|
||||
onClick={() => setRevealed((v) => !v)}
|
||||
>
|
||||
{revealed ? <LuEyeOff className="size-3" /> : <LuEye className="size-3" />}
|
||||
</button>
|
||||
</>
|
||||
) : null}
|
||||
{statusNote}
|
||||
{lookup.status === "loading" ? null : (
|
||||
<Link
|
||||
to={linkTo}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link link-hover inline-flex items-center gap-0.5"
|
||||
>
|
||||
{lookup.status === "found" ? "Open Variables" : "Create on Variables"}
|
||||
<LuExternalLink className="size-3" aria-hidden />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { errorMessage } from "../../api/client.js";
|
||||
import { CodeEditor } from "../CodeEditor.jsx";
|
||||
import { FormInput, FormSelect, FormTextarea } from "../FormControls.jsx";
|
||||
import { LogViewer } from "../LogViewer.jsx";
|
||||
import { StatusBadge } from "../../lib/format.jsx";
|
||||
import { StatusBadge } from "../../lib/format";
|
||||
import { contextFromMeta, prettyJson } from "../../lib/script.js";
|
||||
import { ConfigFields } from "./ConfigFields.jsx";
|
||||
import { ConfigTooltip, configValueText, FieldLabel, previewConfigValue, SchemaTooltip } from "./FieldHelp.jsx";
|
||||
@@ -390,7 +390,7 @@ function needsMode(needs) {
|
||||
return "map";
|
||||
}
|
||||
|
||||
export function ScriptTryDialog({ script, config, meta, owner, onClose }) {
|
||||
function ScriptTryDialog({ script, config, meta, owner, onClose }) {
|
||||
const existing = useScript(script);
|
||||
const dryRun = useDryRunScript();
|
||||
const defaults = useMemo(() => contextFromMeta(meta), [meta]);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { LuChevronDown, LuCopy, LuGripVertical, LuTrash2, LuX } from "react-icons/lu";
|
||||
import { useState } from "react";
|
||||
import { LuChevronDown, LuCopy, LuGripVertical, LuTrash2 } from "react-icons/lu";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import cronstrue from "cronstrue";
|
||||
import { namespacedPath, HTTP_METHODS, triggerDestinations } from "../../lib/workflow-doc.js";
|
||||
import { CRON_CUSTOM, CRON_PRESETS, matchCronPreset, scheduleForPreset } from "../../lib/cron-presets.js";
|
||||
import { FormInput, FormSelect } from "../FormControls.jsx";
|
||||
import { AuthPicker } from "./AuthPicker.jsx";
|
||||
|
||||
export function TriggerCard({
|
||||
trigger,
|
||||
@@ -253,164 +254,6 @@ function HttpFields({ trigger, owner, disabled, onChange, auths, pages, alertDes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Searchable dropdown: pick an auth to add; chips with X to remove.
|
||||
* YAML stores profile UUIDs; UI shows names.
|
||||
*/
|
||||
function AuthPicker({ auths, selectedIds, inlineCount, disabled, onChange }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const rootRef = useRef(null);
|
||||
const inputRef = useRef(null);
|
||||
const selectedSet = useMemo(() => new Set(selectedIds), [selectedIds]);
|
||||
const byId = useMemo(() => new Map(auths.map((a) => [a.id, a])), [auths]);
|
||||
|
||||
const available = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
return auths
|
||||
.filter((a) => !selectedSet.has(a.id))
|
||||
.filter((a) => {
|
||||
if (!q) return true;
|
||||
return (
|
||||
a.name.toLowerCase().includes(q) ||
|
||||
String(a.type).toLowerCase().includes(q) ||
|
||||
a.id.toLowerCase().includes(q)
|
||||
);
|
||||
});
|
||||
}, [auths, selectedSet, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
function onDoc(e) {
|
||||
if (rootRef.current && !rootRef.current.contains(e.target)) {
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
}
|
||||
}
|
||||
document.addEventListener("mousedown", onDoc);
|
||||
return () => document.removeEventListener("mousedown", onDoc);
|
||||
}, [open]);
|
||||
|
||||
function addAuth(id) {
|
||||
if (selectedSet.has(id)) return;
|
||||
onChange([...selectedIds, id]);
|
||||
setQuery("");
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
function removeAuth(id) {
|
||||
onChange(selectedIds.filter((x) => x !== id));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-1" ref={rootRef}>
|
||||
<span className="text-sm font-medium opacity-80">Auth (any of)</span>
|
||||
|
||||
{selectedIds.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{selectedIds.map((id) => {
|
||||
const auth = byId.get(id);
|
||||
return (
|
||||
<span
|
||||
key={id}
|
||||
className="badge badge-outline gap-1 h-7 px-2 font-normal"
|
||||
title={id}
|
||||
>
|
||||
<span className="max-w-[10rem] truncate">
|
||||
{auth ? (
|
||||
<>
|
||||
{auth.name}
|
||||
<span className="opacity-60"> · {auth.type}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="opacity-60">missing</span>
|
||||
)}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost btn-xs btn-square -mr-1"
|
||||
title="Remove"
|
||||
aria-label={`Remove ${auth?.name ?? id}`}
|
||||
disabled={disabled}
|
||||
onClick={() => removeAuth(id)}
|
||||
>
|
||||
<LuX className="size-3.5" />
|
||||
</button>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="relative w-full">
|
||||
<div className="join w-full">
|
||||
<FormInput
|
||||
ref={inputRef}
|
||||
type="search"
|
||||
className="join-item w-full min-w-0"
|
||||
placeholder={auths.length === 0 ? "No auth profiles yet" : "Add auth…"}
|
||||
value={query}
|
||||
disabled={disabled || auths.length === 0}
|
||||
onChange={(e) => {
|
||||
setQuery(e.target.value);
|
||||
setOpen(true);
|
||||
}}
|
||||
onFocus={() => setOpen(true)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Escape") {
|
||||
setOpen(false);
|
||||
setQuery("");
|
||||
inputRef.current?.blur();
|
||||
}
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (available[0]) addAuth(available[0].id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm join-item btn-square"
|
||||
disabled={disabled || auths.length === 0}
|
||||
aria-label="Open auth list"
|
||||
onClick={() => {
|
||||
setOpen((v) => !v);
|
||||
if (!open) inputRef.current?.focus();
|
||||
}}
|
||||
>
|
||||
<LuChevronDown className={`size-4 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
</div>
|
||||
{open && !disabled && auths.length > 0 ? (
|
||||
<ul className="menu menu-sm absolute z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-box border border-base-300 bg-base-100 shadow-lg p-1">
|
||||
{available.length === 0 ? (
|
||||
<li className="disabled">
|
||||
<span className="opacity-60">
|
||||
{query.trim() ? "No matches" : "All profiles selected"}
|
||||
</span>
|
||||
</li>
|
||||
) : (
|
||||
available.map((a) => (
|
||||
<li key={a.id}>
|
||||
<button type="button" onClick={() => addAuth(a.id)}>
|
||||
<span className="font-mono text-sm">{a.name}</span>
|
||||
<span className="opacity-60 text-xs">{a.type}</span>
|
||||
</button>
|
||||
</li>
|
||||
))
|
||||
)}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{inlineCount > 0 ? (
|
||||
<span className="text-xs opacity-60">
|
||||
+ {inlineCount} inline auth{inlineCount === 1 ? "" : "s"} (edit in YAML)
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CronFields({ trigger, disabled, onChange, alertDestinations }) {
|
||||
const preset = matchCronPreset(trigger.schedule);
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useRevertWorkflowRevision,
|
||||
useWorkflowRevisions,
|
||||
} from "../../api/hooks.js";
|
||||
import { formatTime } from "../../lib/format.jsx";
|
||||
import { formatTime } from "../../lib/format";
|
||||
import { useNotifications } from "../../notifications.jsx";
|
||||
import {
|
||||
SaveWorkflowWarningsDialog,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { LuHistory } from "react-icons/lu";
|
||||
import { errorMessage } from "../../api/client.js";
|
||||
import { useRevertWorkflowRevision } from "../../api/hooks.js";
|
||||
import { formatTime } from "../../lib/format.jsx";
|
||||
import { formatTime } from "../../lib/format";
|
||||
import { useNotifications } from "../../notifications.jsx";
|
||||
import { ConfirmDialog } from "../ConfirmDialog.jsx";
|
||||
import {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LuMaximize2, LuMinimize2, LuPlay } from "react-icons/lu";
|
||||
import { api, errorMessage } from "../../api/client.js";
|
||||
import { useRunWorkflow } from "../../api/hooks.js";
|
||||
import { CodeEditor } from "../CodeEditor.jsx";
|
||||
import { StatusBadge } from "../../lib/format.jsx";
|
||||
import { StatusBadge } from "../../lib/format";
|
||||
import { prettyJson } from "../../lib/script.js";
|
||||
import {
|
||||
overlayWorkflowTestData,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { TriggersTab } from "./TriggersTab.jsx";
|
||||
import { YamlTab } from "./YamlTab.jsx";
|
||||
import { WorkflowTestPanel } from "./WorkflowTestPanel.jsx";
|
||||
|
||||
export function useYamlPreview(content) {
|
||||
function useYamlPreview(content) {
|
||||
return useMemo(() => {
|
||||
try {
|
||||
const parsedYaml = parseYaml(content);
|
||||
|
||||
Reference in New Issue
Block a user