Home mosaic, multi-select delete, offline banner and loading states
- Home puts the dashboards beside the flows: two equal-height columns, capped and scrollable, most recently worked on first. Each tile is a schematic footprint built from the stored widget placements. - Flows and dashboards can be picked by long press or ctrl-click; the create button becomes a trash and one dialog covers the batch. - The offline banner is drawn on the body so it centres on the viewport, and the live socket now releases the offline latch a stray 503 set. - A boot spinner before React's first commit, a router pending screen for code-split pages, and skeletons where an empty list used to flash. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018tULRZJUkZsw7rMJ3h4xvu
This commit is contained in:
@@ -4,7 +4,9 @@ import { AlertCircle, Workflow } from "lucide-react"
|
||||
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 { HealthActivity } from "@/components/Health/HealthActivity"
|
||||
@@ -30,6 +32,17 @@ 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()
|
||||
@@ -108,8 +121,11 @@ function Home() {
|
||||
...flowsQueryOptions(),
|
||||
refetchInterval: REFRESH_INTERVAL,
|
||||
})
|
||||
// Its own query beside the flows one, so neither list waits for the other.
|
||||
const boards = useQuery(dashboardsQueryOptions())
|
||||
|
||||
const flows = data?.data ?? []
|
||||
const flows = [...(data?.data ?? [])].sort(byRecency)
|
||||
const dashboards = [...(boards.data?.data ?? [])].sort(byRecency)
|
||||
|
||||
return (
|
||||
// `[&>*]:min-w-0`: a grid item's automatic minimum is its content, so one
|
||||
@@ -126,28 +142,47 @@ function Home() {
|
||||
rather than flow count: a flow made a minute ago has none. */}
|
||||
{flows.some((flow) => (flow.node_count ?? 0) > 0) ? <BrainView /> : null}
|
||||
|
||||
<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>
|
||||
{/* 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>
|
||||
|
||||
<HealthOverview range={range} onRangeChange={setRange} />
|
||||
<HealthActivity range={range} />
|
||||
|
||||
Reference in New Issue
Block a user