From ffa92cd23ddbaea7ad3572ed98e9c4da5183a65c Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Mon, 23 Feb 2026 13:39:28 -0500 Subject: [PATCH] Add GET /pipelines endpoint and pipeline list UI Adds a list endpoint that returns all in-memory pipelines with their status, and a "Recent Pipelines" section in the start form that polls every 3s so users can navigate to any pipeline started since server launch. Co-Authored-By: Claude Opus 4.6 --- attractor-web/src/StartForm.tsx | 76 +++++ attractor-web/src/api.ts | 6 + attractor-web/src/index.css | 504 ++++++++++++++++++++++++++++++++ crates/attractor/src/server.rs | 62 +++- 4 files changed, 647 insertions(+), 1 deletion(-) create mode 100644 attractor-web/src/StartForm.tsx create mode 100644 attractor-web/src/index.css diff --git a/attractor-web/src/StartForm.tsx b/attractor-web/src/StartForm.tsx new file mode 100644 index 000000000..e27545c0b --- /dev/null +++ b/attractor-web/src/StartForm.tsx @@ -0,0 +1,76 @@ +import { useState, useCallback, type FormEvent } from "react"; +import { startPipeline, listPipelines, type PipelineStatusResponse } from "./api"; +import { usePolling } from "./hooks"; + +const PLACEHOLDER = `digraph Example { + graph [goal="Run a simple pipeline"] + start [shape=Mdiamond] + exit [shape=Msquare] + start -> exit +}`; + +interface StartFormProps { + onStart: (id: string) => void; +} + +export function StartForm({ onStart }: StartFormProps) { + const [submitting, setSubmitting] = useState(false); + const [error, setError] = useState(null); + + const fetcher = useCallback(() => listPipelines(), []); + const { data: pipelines } = usePolling(fetcher, 3000, true); + + async function handleSubmit(e: FormEvent) { + e.preventDefault(); + const form = e.currentTarget; + const dotSource = new FormData(form).get("dot_source") as string; + if (!dotSource.trim()) return; + + setSubmitting(true); + setError(null); + + try { + const { id } = await startPipeline(dotSource); + onStart(id); + } catch (err) { + setError(String(err)); + } finally { + setSubmitting(false); + } + } + + return ( +
+ +