Act on a run, and read Home top-down
Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m18s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m49s
pre-commit / pre-commit (push) Failing after 2m5s
Test Backend / test-backend (push) Successful in 2m39s
Compose Smoke Test / test-compose (push) Successful in 33s
Playwright Tests / merge-reports (push) Successful in 1m11s
Docs / docs (push) Successful in 23s
Playwright Tests / test-playwright (1, 2) (push) Successful in 3m18s
Playwright Tests / test-playwright (2, 2) (push) Successful in 1m49s
pre-commit / pre-commit (push) Failing after 2m5s
Test Backend / test-backend (push) Successful in 2m39s
Compose Smoke Test / test-compose (push) Successful in 33s
Playwright Tests / merge-reports (push) Successful in 1m11s
Runs: a run can now be deleted (DELETE /runs/{id}, cancelling a live one
first), exported as csv from the screen's own filters, and its flow label
opens the flow. Its "Parameters" panel became "Inputs" and lists every
input the flow declares, marking the ones that took the flow's own value
rather than the run's — the comparison table resolves the same defaults
instead of printing "unset".
Home reads brain, dashboards, health, flows: the mosaic is one full-width
scrolling strip, and the flows list and the flow-activity rollups merged
into a single left-joined table so a flow's state and its numbers sit on
one row.
Charts take a drag to narrow the x window and a double click or tap to
come back out. UplotChart holds the scale and passes resetScales:false
while a window is held, which is what the old comment said made this
impossible.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LrWVRguqbk33YzfEeUx5W
This commit is contained in:
@@ -1,22 +1,17 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { createFileRoute, Link } from "@tanstack/react-router"
|
||||
import { AlertCircle, Workflow } from "lucide-react"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import { useState } from "react"
|
||||
|
||||
import { type FlowSummary, FlowsService } from "@/client"
|
||||
import { byRecency, DashboardMosaic } from "@/components/Common/DashboardMosaic"
|
||||
import { DEFAULT_RANGE } from "@/components/Common/RangePicker"
|
||||
import { dashboardsQueryOptions } from "@/components/Dashboard/queries"
|
||||
import { BrainView } from "@/components/Flow/BrainView"
|
||||
import { flowKeys, flowsQueryOptions } from "@/components/Flow/queries"
|
||||
import { flowsQueryOptions } from "@/components/Flow/queries"
|
||||
import { FlowTable } from "@/components/Health/FlowTable"
|
||||
import { HealthActivity } from "@/components/Health/HealthActivity"
|
||||
import { HealthOverview } from "@/components/Health/HealthOverview"
|
||||
import { LiveIndicator } from "@/components/Health/LiveIndicator"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card } from "@/components/ui/card"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import useCustomToast from "@/hooks/useCustomToast"
|
||||
|
||||
export const Route = createFileRoute("/_layout/")({
|
||||
component: Home,
|
||||
@@ -29,102 +24,29 @@ export const Route = createFileRoute("/_layout/")({
|
||||
}),
|
||||
})
|
||||
|
||||
/** Another tab can stop a flow, and the engine can fail one on its own. */
|
||||
const REFRESH_INTERVAL = 10_000
|
||||
|
||||
/**
|
||||
* How tall the two lists beside each other are allowed to get: about six flow
|
||||
* rows, and whatever the mosaic fits in the same space. Past it each column
|
||||
* scrolls on its own rather than pushing the health block off the screen.
|
||||
*/
|
||||
const LISTS = "grid max-h-96 grid-rows-[auto_minmax(0,1fr)] gap-2"
|
||||
|
||||
/** DESIGN-GUIDELINES.md → Typography, the canonical section header. */
|
||||
const HEADER =
|
||||
"text-xs font-medium uppercase tracking-[0.5px] text-muted-foreground"
|
||||
|
||||
function FlowRow({ flow }: { flow: FlowSummary }) {
|
||||
const queryClient = useQueryClient()
|
||||
const { showErrorToast } = useCustomToast()
|
||||
const enabled = flow.enabled ?? true
|
||||
|
||||
const toggle = useMutation({
|
||||
mutationFn: (next: boolean) =>
|
||||
next
|
||||
? FlowsService.startFlow({ name: flow.name })
|
||||
: FlowsService.stopFlow({ name: flow.name }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: flowKeys.all })
|
||||
queryClient.invalidateQueries({ queryKey: flowKeys.detail(flow.name) })
|
||||
},
|
||||
onError: () => showErrorToast("The flow could not be started or stopped."),
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center gap-3 border-b border-border px-5 py-3 last:border-b-0"
|
||||
data-testid="home-flow-row"
|
||||
>
|
||||
<Link
|
||||
to="/flows/$flowName"
|
||||
params={{ flowName: flow.name }}
|
||||
className="min-w-0 flex-1"
|
||||
>
|
||||
<p className="truncate font-medium">{flow.title || flow.name}</p>
|
||||
<p className="truncate text-sm text-muted-foreground">
|
||||
{flow.node_count === 1 ? "1 node" : `${flow.node_count} nodes`}
|
||||
{flow.has_draft ? " · unpublished changes" : ""}
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
{(flow.error_count ?? 0) > 0 ? (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<AlertCircle />
|
||||
{flow.error_count}
|
||||
</Badge>
|
||||
) : null}
|
||||
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={
|
||||
enabled
|
||||
? flow.paused
|
||||
? "border-transparent bg-primary/15 text-primary"
|
||||
: "border-transparent bg-status-success/15 text-status-success"
|
||||
: "text-muted-foreground"
|
||||
}
|
||||
>
|
||||
{enabled ? (flow.paused ? "Paused" : "Running") : "Stopped"}
|
||||
</Badge>
|
||||
|
||||
<Switch
|
||||
checked={enabled}
|
||||
disabled={toggle.isPending}
|
||||
onCheckedChange={(next) => toggle.mutate(next)}
|
||||
aria-label={`Run ${flow.title || flow.name}`}
|
||||
data-testid="flow-enabled-switch"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The one overview: what the engine is wired up as, what is running, and how
|
||||
* it has been doing. The brain and the health screens compose in here rather
|
||||
* than living at routes of their own.
|
||||
*
|
||||
* Top to bottom it is a widening lens: the whole installation as a graph, what
|
||||
* has been built on it, whether it is well, then flow by flow and finally
|
||||
* moment by moment.
|
||||
*/
|
||||
function Home() {
|
||||
// The health block's window: one choice, read by the tiles, the flow table,
|
||||
// the charts and the lists under them.
|
||||
const [range, setRange] = useState(DEFAULT_RANGE)
|
||||
const { data, isPending } = useQuery({
|
||||
...flowsQueryOptions(),
|
||||
refetchInterval: REFRESH_INTERVAL,
|
||||
})
|
||||
// Its own query beside the flows one, so neither list waits for the other.
|
||||
const boards = useQuery(dashboardsQueryOptions())
|
||||
// Only to know whether the brain has anything to draw; the table below runs
|
||||
// its own copy of the same query.
|
||||
const { data } = useQuery(flowsQueryOptions())
|
||||
|
||||
const flows = [...(data?.data ?? [])].sort(byRecency)
|
||||
const flows = data?.data ?? []
|
||||
const dashboards = [...(boards.data?.data ?? [])].sort(byRecency)
|
||||
|
||||
return (
|
||||
@@ -138,53 +60,30 @@ function Home() {
|
||||
<LiveIndicator />
|
||||
|
||||
{/* Nothing wired up yet means nothing to draw, and the band would still
|
||||
hold a screenful of empty space above the flows card. Node count
|
||||
hold a screenful of empty space above the flows table. Node count
|
||||
rather than flow count: a flow made a minute ago has none. */}
|
||||
{flows.some((flow) => (flow.node_count ?? 0) > 0) ? <BrainView /> : null}
|
||||
|
||||
{/* One grid row holding both: a grid item stretches to the row, so the
|
||||
two columns come out exactly as tall as each other whatever is in
|
||||
them — and with one flow and one dashboard that is simply the taller
|
||||
of the two, which is the floor the cap never goes under. */}
|
||||
<div className="grid gap-6 [&>*]:min-w-0 lg:grid-cols-2">
|
||||
<section className={LISTS}>
|
||||
<h2 className={HEADER}>Flows</h2>
|
||||
<Card className="gap-0 overflow-y-auto py-0">
|
||||
{isPending ? (
|
||||
<div className="grid gap-3 p-5">
|
||||
<Skeleton className="h-5 w-40" />
|
||||
<Skeleton className="h-5 w-28" />
|
||||
</div>
|
||||
) : flows.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3 px-5 py-10 text-center">
|
||||
<span className="flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground">
|
||||
<Workflow className="size-5" />
|
||||
</span>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Flows you build show up here, with what they are doing.
|
||||
</p>
|
||||
<Link to="/flows" className="text-sm font-medium underline">
|
||||
Go to flows
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
flows.map((flow) => <FlowRow key={flow.name} flow={flow} />)
|
||||
)}
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<section className={LISTS}>
|
||||
<h2 className={HEADER}>Dashboards</h2>
|
||||
<Card className="gap-0 overflow-y-auto py-0">
|
||||
<DashboardMosaic
|
||||
dashboards={dashboards}
|
||||
isPending={boards.isPending}
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
</div>
|
||||
{/* One row across the full width rather than a column beside the flows:
|
||||
a dashboard tile is a picture, and a picture wants to be wide. Past
|
||||
what fits, the strip scrolls sideways on its own — the page must not,
|
||||
which is what the `min-w-0` above is holding. */}
|
||||
<section className="grid gap-2">
|
||||
<h2 className={HEADER}>Dashboards</h2>
|
||||
{/* `min-w-0`: the strip inside scrolls, but this card is a grid item
|
||||
whose automatic minimum is its content — without this the tiles
|
||||
widen it, and the page with it. */}
|
||||
<Card className="min-w-0 gap-0 py-0">
|
||||
<DashboardMosaic
|
||||
dashboards={dashboards}
|
||||
isPending={boards.isPending}
|
||||
row
|
||||
/>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<HealthOverview range={range} onRangeChange={setRange} />
|
||||
<FlowTable range={range} />
|
||||
<HealthActivity range={range} />
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user