Files
app/frontend/src/routes/_layout/index.tsx
T
stroblme 449a1472cd
Playwright Tests / test-playwright (1, 2) (push) Canceled after 0s
Playwright Tests / test-playwright (2, 2) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Compose Smoke Test / test-compose (push) Canceled after 0s
Playwright Tests / merge-reports (push) Canceled after 0s
gc
Signed-off-by: stroblme <stroblme@posteo.de>
2026-08-17 20:07:56 +02:00

149 lines
5.0 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { createFileRoute, Link } from "@tanstack/react-router"
import { AlertCircle, Workflow } from "lucide-react"
import { useState } from "react"
import { type FlowSummary, FlowsService } from "@/client"
import { DEFAULT_RANGE } from "@/components/Common/RangePicker"
import { BrainView } from "@/components/Flow/BrainView"
import { flowKeys, flowsQueryOptions } from "@/components/Flow/queries"
import { HealthActivity } from "@/components/Health/HealthActivity"
import { HealthOverview } from "@/components/Health/HealthOverview"
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: Dashboard,
head: () => ({
meta: [
{
title: "Dashboard - Fluksio",
},
],
}),
})
/** Another tab can stop a flow, and the engine can fail one on its own. */
const REFRESH_INTERVAL = 10_000
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="dashboard-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.
*/
function Dashboard() {
// 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,
})
const flows = data?.data ?? []
return (
// `[&>*]:min-w-0`: a grid item's automatic minimum is its content, so one
// long name or wide table widens the whole column and the page with it.
// Every section here is free to shrink instead. See DESIGN-GUIDELINES.md
// → Responsive.
<div className="grid gap-6 [&>*]:min-w-0">
<BrainView />
<Card className="gap-0 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>
<HealthOverview range={range} onRangeChange={setRange} />
<HealthActivity range={range} />
</div>
)
}