feat: edit target servers and tags together in the workflow modal
This commit is contained in:
@@ -66,11 +66,6 @@ export default function WorkflowBuilder() {
|
||||
const [editWorkflowOpen, setEditWorkflowOpen] = useState(false);
|
||||
const [dragOverZone, setDragOverZone] = useState<number | null>(null);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
// Rows rather than a map so a half-typed pair (a key with no value yet)
|
||||
// survives a keystroke. Only complete pairs are written into wf.target_tags,
|
||||
// which is what the debounced save persists.
|
||||
const [tagRows, setTagRows] = useState<[string, string][]>([]);
|
||||
const tagRowsSeeded = useRef(false);
|
||||
|
||||
const { data: loaded } = useQuery({
|
||||
queryKey: ["workflow", id],
|
||||
@@ -80,7 +75,6 @@ export default function WorkflowBuilder() {
|
||||
// The whole fleet, so the "runs on N servers" readout can be computed in the
|
||||
// browser rather than asking the server to resolve targets on every keystroke.
|
||||
const { data: servers } = useQuery({ queryKey: ["servers"], queryFn: () => api.listServers() });
|
||||
const { data: knownTags } = useQuery({ queryKey: ["server-tags"], queryFn: () => api.listKnownTags(), staleTime: 60_000 });
|
||||
const { data: secretGroups } = useQuery({
|
||||
queryKey: ["secret-groups"],
|
||||
queryFn: api.listSecretGroups,
|
||||
@@ -91,11 +85,6 @@ export default function WorkflowBuilder() {
|
||||
setWf(loaded);
|
||||
savedSnapshotRef.current = snapshotOf(loaded);
|
||||
}
|
||||
if (loaded && !tagRowsSeeded.current) {
|
||||
tagRowsSeeded.current = true;
|
||||
setTagRows(Object.entries(loaded.target_tags ?? {}));
|
||||
}
|
||||
|
||||
}, [loaded]);
|
||||
|
||||
|
||||
@@ -151,14 +140,6 @@ export default function WorkflowBuilder() {
|
||||
|
||||
const targetTags = wf.target_tags ?? {};
|
||||
|
||||
// Rows are the editing surface; the map is what is saved. Incomplete rows
|
||||
// are dropped rather than saved half-written, which is also what keeps the
|
||||
// readout below honest while someone is still typing a key.
|
||||
const commitTagRows = (rows: [string, string][]) => {
|
||||
setTagRows(rows);
|
||||
setWf({ ...wf, target_tags: Object.fromEntries(rows.filter(([k, v]) => k && v)) });
|
||||
};
|
||||
|
||||
/*
|
||||
* This is the other half of a deliberate duplication: the authority is
|
||||
* UnionTargets/MatchesTags in server/internal/services/targets.go, and this
|
||||
@@ -379,63 +360,30 @@ export default function WorkflowBuilder() {
|
||||
<div className="mb-2 w-full rounded border border-border bg-surface p-3">
|
||||
<div className="mb-2 text-[11px] font-bold uppercase tracking-wide text-text-secondary">Targets</div>
|
||||
|
||||
{/* Read-only. Both halves of the selector are edited in
|
||||
EditWorkflowModal so there is one place to change what
|
||||
a workflow touches; this panel only reports the result. */}
|
||||
<p className="mb-2 text-xs text-text-secondary">
|
||||
{wf.target_server_ids.length} named in <button type="button" onClick={() => setEditWorkflowOpen(true)} className="text-signal hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-signal">Edit</button>, plus anything matching every tag below.
|
||||
</p>
|
||||
|
||||
<datalist id="workflow-tag-keys">
|
||||
{Object.keys(knownTags ?? {}).map((k) => (
|
||||
<option key={k} value={k} />
|
||||
))}
|
||||
</datalist>
|
||||
<datalist id="workflow-tag-values">
|
||||
{Object.values(knownTags ?? {})
|
||||
.flat()
|
||||
.map((v) => (
|
||||
<option key={v} value={v} />
|
||||
))}
|
||||
</datalist>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
{tagRows.map(([k, v], i) => (
|
||||
<div key={i} className="flex items-center gap-2">
|
||||
<input
|
||||
list="workflow-tag-keys"
|
||||
value={k}
|
||||
onChange={(e) => commitTagRows(tagRows.map((row, j): [string, string] => (j === i ? [e.target.value, row[1]] : row)))}
|
||||
placeholder="env"
|
||||
className="w-28 rounded-lg border border-border bg-surface-2 px-2 py-1 font-mono text-xs text-text-primary focus:border-signal focus:outline-none"
|
||||
/>
|
||||
<span className="font-mono text-text-tertiary">:</span>
|
||||
<input
|
||||
list="workflow-tag-values"
|
||||
value={v}
|
||||
onChange={(e) => commitTagRows(tagRows.map((row, j): [string, string] => (j === i ? [row[0], e.target.value] : row)))}
|
||||
placeholder="prod"
|
||||
className="w-32 rounded-lg border border-border bg-surface-2 px-2 py-1 font-mono text-xs text-text-primary focus:border-signal focus:outline-none"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => commitTagRows(tagRows.filter((_, j) => j !== i))}
|
||||
className="text-xs text-text-tertiary hover:text-danger focus:outline-none focus-visible:ring-2 focus-visible:ring-signal"
|
||||
aria-label={`Remove ${k || "tag"}`}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{tagRows.length === 0 && <p className="text-xs text-text-secondary">No tag selector — only the named servers will run.</p>}
|
||||
</div>
|
||||
|
||||
{tagRows.length < 20 && (
|
||||
{wf.target_server_ids.length} named{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => commitTagRows([...tagRows, ["", ""] as [string, string]])}
|
||||
className="mt-2 text-xs text-signal hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-signal"
|
||||
onClick={() => setEditWorkflowOpen(true)}
|
||||
className="text-signal hover:underline focus:outline-none focus-visible:ring-2 focus-visible:ring-signal"
|
||||
>
|
||||
Add tag
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{Object.entries(targetTags).map(([k, v]) => (
|
||||
<span key={k} className="rounded-sm border border-border bg-surface-2 px-1.5 py-0.5 font-mono text-[11px]">
|
||||
<span className="text-text-tertiary">{k}</span>
|
||||
<span className="text-text-tertiary">:</span>
|
||||
<span className="text-text-secondary">{v}</span>
|
||||
</span>
|
||||
))}
|
||||
{Object.keys(targetTags).length === 0 && <p className="text-xs text-text-secondary">No tag selector — only the named servers will run.</p>}
|
||||
</div>
|
||||
|
||||
<p className="mt-3 font-mono text-xs text-text-secondary" title={matched.map((s) => s.hostname).join("\n")}>
|
||||
Runs on {matched.length} {matched.length === 1 ? "server" : "servers"}
|
||||
@@ -696,8 +644,8 @@ export default function WorkflowBuilder() {
|
||||
open={editWorkflowOpen}
|
||||
workflow={wf}
|
||||
onSaved={(w) => {
|
||||
|
||||
|
||||
// Snapshot first: the modal has just persisted name, targets
|
||||
// and tags, so the debounced autosave must not re-send them.
|
||||
savedSnapshotRef.current = snapshotOf(w);
|
||||
setWf(w);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user