fix: count tag-matched servers in the workflows list

This commit is contained in:
2026-08-04 17:28:01 +01:00
parent 3388d2f895
commit 50a9ac5fdc
4 changed files with 63 additions and 17 deletions
+2 -9
View File
@@ -8,6 +8,7 @@ import { api, Workflow, WorkflowStep, WorkflowStepRef, SecretGroupSummary } from
import { Button } from "@/components/ui";
import { EditWorkflowModal } from "@/components/workflows/EditWorkflowModal";
import { StepPickerModal } from "@/components/workflows/StepPickerModal";
import { resolveTargets } from "@/lib/targets";
const inputClass = "w-full rounded-lg border border-border bg-surface-2 px-3 py-2 text-sm text-text-primary placeholder-text-secondary/50 focus:border-signal focus:outline-none focus:ring-1 focus:ring-signal";
@@ -125,15 +126,7 @@ export default function WorkflowBuilder() {
const targetTags = wf.target_tags ?? {};
/*
* This is the other half of a deliberate duplication: the authority is
* UnionTargets/MatchesTags in server/internal/services/targets.go, and this
* only exists so the designer can answer "how many servers?" without a
* round trip. It must stay identical in meaning — an EMPTY selector matches
* NOTHING (a cleared field must not become a fleet-wide run), and multiple
* tag keys AND together. Change one, change both.
*/
const matched = (servers ?? []).filter((s) => wf.target_server_ids.includes(s.server_id) || (Object.keys(targetTags).length > 0 && Object.entries(targetTags).every(([k, v]) => s.tags?.[k] === v)));
const matched = resolveTargets(servers ?? [], wf.target_server_ids, targetTags);
const sortedSteps = [...wf.steps].sort((a, b) => a.order - b.order);
const selectedRef = selected !== null ? sortedSteps[selected] : null;
+25 -3
View File
@@ -7,6 +7,7 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api, Workflow } from "@/lib/api";
import { Button, Card } from "@/components/ui";
import { Table, Thead, Tbody, Tr, Th, Td } from "@/components/ui";
import { resolveTargets } from "@/lib/targets";
export default function WorkflowsPage() {
const qc = useQueryClient();
@@ -22,6 +23,11 @@ export default function WorkflowsPage() {
queryFn: api.listWorkflows,
});
// The fleet, so a tag-only workflow reports the servers it actually reaches.
// target_server_ids.length alone reads 0 for one, which is the count of the
// half of the selector it does not use.
const { data: servers } = useQuery({ queryKey: ["servers"], queryFn: () => api.listServers() });
const { mutate: create, isPending } = useMutation({
mutationFn: () => api.createWorkflow({ name: "Untitled workflow", target_server_ids: [], steps: [] }),
onSuccess: (workflow) => {
@@ -75,9 +81,25 @@ export default function WorkflowsPage() {
<span className="font-medium text-text-primary">{w.name}</span>
</Td>
<Td label="Targets">
<span className="text-text-secondary">
{w.target_server_ids.length} server{w.target_server_ids.length !== 1 ? "s" : ""}
</span>
{servers ? (
(() => {
const matched = resolveTargets(servers, w.target_server_ids, w.target_tags ?? {});
return (
<span className="flex flex-wrap items-center gap-1.5">
<span className={matched.length === 0 ? "text-danger" : "text-text-secondary"} title={matched.map((s) => s.hostname).join("\n")}>
{matched.length} server{matched.length !== 1 ? "s" : ""}
</span>
{Object.entries(w.target_tags ?? {}).map(([k, v]) => (
<span key={k} className="rounded-sm border border-border px-1.5 py-0.5 font-mono text-[10px] text-text-tertiary">
{k}:{v}
</span>
))}
</span>
);
})()
) : (
<span className="text-text-tertiary"></span>
)}
</Td>
<Td label="Steps">
<span className="text-text-secondary">{w.steps.length}</span>