Give a task's bindings their own section, polished - #2053
Conversation
⚪ 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
|
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.
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.
Adds bytes-behind and time-behind-source columns to the materialization bindings table, sourced from the existing backlog/time-lag stats queries. Time behind replaces Last data for materializations (a source-relative answer supersedes a source-blind one); captures keep Last data since they have no upstream frontier to measure against. Also switches this table's user-facing copy from "bindings" to "collections" (reusing the existing terms.collections keys), fixes a column-width imbalance where surplus table width landed on whichever column had the longest header text, and adds start-anchored ellipsis truncation for long collection names so a shared tenant/prefix doesn't crowd out the differentiating tail.
…lumn
Follow-up review notes on the bindings section, all presentational.
The search field had no hover styling of its own, so it inherited MUI's
default outlined treatment: an opaque `text.primary` border against a
0.23 resting one, which read as a flash rather than an affordance. It now
lifts to 0.35, and excludes the focused state so the primary focus ring
still wins once the field is active.
The two lag tooltips explained the delta-sum mechanics at a length
nobody reads on hover. They keep the two claims that change how you read
the number — it is from the task's last stats reading rather than the
selected range, and it is directional — and leave the mechanics to the
comment already sitting above the strings.
The collection name's tooltip is gone. It only ever restated the cell,
which truncates from the start: the distinctive tail is already visible,
and the name is a link to the entity itself.
Unit suffixes ("GB", "hours", "ago") and "Caught up" no longer dim
themselves. Dimming stayed where it marks a state — a silent binding, a
zero-doc cell, a disabled row — rather than decorating a suffix.
The volume bar is dropped: scaled to the busiest binding, it invited a
reading it could not support. That makes `maxBytes` dead from
`getVolumeTotals` down through the card, table, and cell, so the whole
thread goes with it; the tooltip's share-of-total figure is now the only
relative measure, and it was always the more answerable one.
…e StatusCell colors Also ignore .superpowers/ scratch directory.
4bff249 to
38fdbb6
Compare
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):
