From e4c3fc24d306b1a25d7c7afdc01e5380538ee8a1 Mon Sep 17 00:00:00 2001 From: mrhid6 Date: Tue, 21 Jul 2026 11:43:42 +0100 Subject: [PATCH] feat(web): standalone Steps management page --- web/app/steps/page.tsx | 214 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 214 insertions(+) create mode 100644 web/app/steps/page.tsx diff --git a/web/app/steps/page.tsx b/web/app/steps/page.tsx new file mode 100644 index 0000000..2ee2c22 --- /dev/null +++ b/web/app/steps/page.tsx @@ -0,0 +1,214 @@ +"use client"; + +import { useMemo, useRef, useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { api, WorkflowStep } from "@/lib/api"; +import { Button } from "@/components/ui"; +import { EditStepModal } from "@/components/workflows/EditStepModal"; + +type Tab = "all" | "bash" | "powershell" | "default" | "shared"; + +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"; + +function ShellBadge({ interpreter }: { interpreter: "bash" | "powershell" }) { + const isBash = interpreter === "bash"; + return ( + + {isBash ? "bash" : "pwsh"} + + ); +} + +export default function StepsPage() { + const qc = useQueryClient(); + const { data: steps } = useQuery({ queryKey: ["steps"], queryFn: api.listSteps }); + const { data: usage } = useQuery({ queryKey: ["step-usage"], queryFn: api.stepUsage }); + + const [search, setSearch] = useState(""); + const [tab, setTab] = useState("all"); + const [editOpen, setEditOpen] = useState(false); + const [editing, setEditing] = useState(null); + const [importing, setImporting] = useState(false); + const [syncing, setSyncing] = useState(false); + const [error, setError] = useState(null); + const [notice, setNotice] = useState(null); + const fileRef = useRef(null); + + const rows = useMemo(() => { + const q = search.toLowerCase(); + return (steps ?? []).filter((s) => { + const matchesText = s.name.toLowerCase().includes(q) || (s.description ?? "").toLowerCase().includes(q); + const matchesTab = + tab === "all" || + (tab === "bash" && s.interpreter === "bash") || + (tab === "powershell" && s.interpreter === "powershell") || + (tab === "default" && s.source === "default") || + (tab === "shared" && s.source !== "default"); + return matchesText && matchesTab; + }); + }, [steps, search, tab]); + + const openNew = () => { + setEditing(null); + setEditOpen(true); + }; + const openEdit = (s: WorkflowStep) => { + setEditing(s); + setEditOpen(true); + }; + + const onImport = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + setImporting(true); + setError(null); + try { + const doc = JSON.parse(await file.text()); + await api.importStep(doc); + qc.invalidateQueries({ queryKey: ["steps"] }); + setNotice("Step imported."); + } catch (err) { + setError((err as Error).message); + } finally { + setImporting(false); + e.target.value = ""; + } + }; + + const onSync = async () => { + setSyncing(true); + setError(null); + try { + const { created, updated } = await api.seedDefaults(); + qc.invalidateQueries({ queryKey: ["steps"] }); + setNotice(`${created} created, ${updated} updated`); + } catch (err) { + setError((err as Error).message); + } finally { + setSyncing(false); + } + }; + + return ( +
+
+
+

Steps

+

Reusable steps shared across all workflows.

+
+
+ + + + +
+
+ + {error &&
{error}
} + {notice &&
{notice}
} + +
+ setSearch(e.target.value)} /> +
+ {(["all", "bash", "powershell", "default", "shared"] as Tab[]).map((t) => ( + + ))} +
+
+ +
+ + + + + + + + + + + + + {rows.map((s) => { + const count = usage?.[s.step_id] ?? 0; + return ( + + + + + + + + + ); + })} + {rows.length === 0 && ( + + + + )} + +
NameShellSourceOutputsUsed byActions
+
{s.name}
+ {s.description &&
{s.description}
} +
+ + + + {s.source === "default" ? "default" : "shared"} + + +
+ {(s.declared_outputs ?? []).map((o) => ( + + {o} + + ))} +
+
+ {count === 0 ? "—" : `${count} workflow${count === 1 ? "" : "s"}`} + +
+ + + Export + + +
+
+ No steps found. +
+
+ + { + setEditOpen(false); + qc.invalidateQueries({ queryKey: ["steps"] }); + qc.invalidateQueries({ queryKey: ["step-usage"] }); + }} + /> +
+ ); +}