Give a task's bindings their own section, polished - #2053
Conversation
Reads `taskStats` off the row the page already fetches, so freshness and volume cost no extra request and cover every binding rather than a sample. Freshness comes from `capture[c].lastPublishedAt` and `materialize[c].lastSourcePublishedAt` — the same `catalog_stats` row the OpenMetrics endpoint serves, so nothing is gained by going there instead. Reported as a max, never a mean: on a healthy 15-binding capture the max reads 272s while the mean reads 9.1 hours, because two thirds are reference tables. An average makes a healthy task look dead.
Searchable, sortable, paginated at 25, with a volume bar in the data column and per-binding freshness in Last data. Filter chips restate the enabled/disabled split, which is why the strip above no longer carries a collection count. Every figure is scoped to the timeframe selected at the top of the page; the chip in the card header says so on hover.
The bindings were a chip list in the quarter-width details rail, with everything past the first few behind an "N more" toggle. They are the most-clicked thing on the page, so they move to a full-width section below the chart: searchable, sortable by volume, paginated, with per-binding volume and last-data figures. The rail no longer lists them. "Writes To" and "Reads From" are dropped from ENTITY_SETTINGS for captures and materializations. A collection keeps its rail rows, because its related entities are other tasks rather than bindings. The section's figures follow the chart's own range picker, and the heading carries a chip stating that window — the table is far enough down the page that a total with no stated range gets misread.
The name is the one thing on this page people paste elsewhere — into flowctl, a ticket, a Slack thread — and it is also the thing truncation takes away from them. The toolbar is shared, so captures, materializations and collections all get it. CopyToClipboardButton's icon-only branch had no accessible name at all, so it takes an optional label. The name and the button share a flex row with minWidth 0. A flex item's floor is its content by default, so without it a long catalog name pushes Edit and Materialize off the right edge instead of ellipsing.
…unds Cleanup pass over the bindings work. No behaviour change. Filter, sort and paging state moves into `useBindingsTableState`, shared by the page and the Storybook harness. The harness had its own copy and the two had already drifted — it fused the filter and sort memos — so a story could not have shown the behaviour the page has, which is the one thing the harness exists for. `getRangeLabelDescriptor` joins the range picker and the chip that restates its window, which duplicated the `selectedLabelKey` fallback. `getRangeBounds` joins the two `catalog_stats` queries that cover "the same window" and each computed it themselves. Both are cases where the two copies agreeing is the whole point. `getVolumeTotals` replaces `getTotalBytes`, which had no callers because both sites hand-rolled the same reduce. The capture and materialization column arrays are built from one base; they differed in two places and repeated four definitions. Dead code out: `getTaskFreshness`, `REPORTING_FLOOR_SECONDS` (its 270s measurement stays, as prose on `getElapsed`, which is the only thing it documented) and `useEntityStatusStore_autoDiscoverNextAt`, which belongs to the status strip and rode along by mistake. `useBindings` drops the hand-rolled `useRef` cache retention for SWR's own `keepPreviousData` plus `isLoading`. Verified equivalent against a cold request key with a 2.5s injected delay: skeletons hold over the volume columns for the whole load window while row order never moves.
… tokens Acts on a two-axis review of the branch. Correctness. The card's total counted each *row*, but `catalog_stats` breaks volume down per collection, not per binding — so two bindings on one collection, which a materialization writing one collection to two tables has, each carried that collection's whole figure and the header reported more than the task moved. It now counts each collection once. The row figures cannot be split any finer: there is no per-binding breakdown to attribute. A timestamp now only counts from an interval that moved data. An interval can stamp a frontier while the binding moved nothing in it, which put a "last data" time beside a zero — exactly what the column promises not to do, and what PROJECT.md flagged about materialization freshness. The `maximize` comment on MaterializeBindingStats read as contradicting accumulateBindingStats. They answer different questions — all time versus a fixed window — so both say which. Standards. `cardHeaderSx_emphasized` joins `cardHeaderSx` in Theme.tsx rather than living as a private sx object; RangeChip's border uses `defaultOutline`; StatusCell's dot moves from a raw `style` to `sx`. The heading, the table's accessible name and the subtitle's plural all come from the existing `terms.bindings` keys instead of restating the word three more times. `getColumns` was renamed `getSharedColumns` — it sat beside a `getBindingColumns` in the same file. `getElapsed` narrows back to `DateTime`: the string branch had no caller but the tests. Fixture tuples get a named `StreamFixture` type. BindingsCard splits the markup from the fetch, so the Storybook harness renders the production card instead of a copy of its JSX. The state hook already stopped those two disagreeing about behaviour; this stops them disagreeing about layout.
…d reuse - Row density/hover, empty & loading states, and dark-mode borders brought in line with the rest of the design system. - Status column distinguishes enabled/disabled/no-data at a glance; a task-wide connector error now shows as a header chip instead of repainting every row's status pill. - Volume and last-data columns align their digits in a fixed column, independent of unit-suffix length. - Search field drops the native browser clear glyph for an Xmark icon consistent with the rest of the app; toolbar CSV export now goes through the existing CsvDownload/ExportButton pattern instead of a hand-rolled Blob/anchor download. - Collection name cell drops its own link styling when the whole row is already the click target (new opt-in `plain` prop on EntityNameDetailsLink/LinkWrapper), while staying a real anchor for keyboard/middle-click. - CardWrapperProps.sx is now properly typed as SxProps<Theme>, removing a cast at the one call site that needed it.
⚪ Code HealthNo change to the dead-code surface. 48 Unused files
66 Unused exports
30 Unused exported types
14 Unused exported enum members
5 Unused dependencies
3 Unused devDependencies
|
PREFIX, LARGE_TASK_BINDING_COUNT, readLastPublishedAt, and DocsAndBytes were each only ever referenced within their own file.
Clicking a row and coming back left it highlighted with an accent border, which read as "this binding is selected" even though nothing about it actually was — it just meant "you clicked here once." Rows now only highlight on hover, like every other row.
People rely on shard status to flag an unhealthy task, so when it's anything but running, that card now jumps above the Bindings section instead of sitting below it. The reorder logic lives in a small new ShardAwareSectionOrder component (with a Storybook story seeding the real ShardDetail store) so it's demonstrable without dragging in the whole Overview page's usage chart, details rail, and live binding data. Drops the "Connector error" chip from the Bindings header that used to carry this signal — Shard Information is now the one place it's shown. Also default bindings-per-page to 10 instead of 25.
TASK_NAME and ShardStatusCode in the new story fixture were only ever referenced within their own file.
Changes
type="search"clear glyph — swapped for anXmarkicon consistent with the rest of the app.CsvDownload/ExportButtonpattern (generateFileName,tableExportSeparator) instead of a hand-rolled Blob/anchor download.plainprop onEntityNameDetailsLink/LinkWrapper— while staying a real, keyboard-focusable anchor.CardWrapperProps.sxis now typed asSxProps<Theme>(was bareSxProps), removing anas SxPropscast at its one call site.Tests
Manually tested
Automated tests
npm run typecheck,npm run lint,npm run formatall clean.npx vitest run— 364/364 passing, including existing coverage forBindings/shared.ts.Playwright tests ran locally
Screenshots
Before ("Writes To" in side card):

After (bindings card):
