Skip to content

Give a task's bindings their own section, polished - #2053

Open
SeanWhelan wants to merge 12 commits into
mainfrom
sean/task-bindings-section
Open

Give a task's bindings their own section, polished#2053
SeanWhelan wants to merge 12 commits into
mainfrom
sean/task-bindings-section

Conversation

@SeanWhelan

@SeanWhelan SeanWhelan commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Changes

  • Added a dedicated Bindings section to the task Overview tab (capture and materialization), with a sortable/searchable/paginated table replacing the old chip-list-in-a-rail treatment.
  • Row density, hover/selection state, empty state, loading skeletons, and dark-mode card border brought in line with the rest of the design system.
  • Status column now distinguishes enabled / disabled / no-data-in-range at a glance. A task-wide connector error shows once as a header chip instead of repainting every enabled row's status pill red.
  • Volume and "last data" columns right-align their digits in a fixed-width column so they line up regardless of unit-suffix length (e.g. "5 minutes ago" vs "1 hour ago").
  • Search field no longer relies on the browser's native type="search" clear glyph — swapped for an Xmark icon consistent with the rest of the app.
  • CSV export goes through the existing CsvDownload/ExportButton pattern (generateFileName, tableExportSeparator) instead of a hand-rolled Blob/anchor download.
  • Collection name cell drops its own link styling (color/underline/tooltip) when the whole row is already the click target — new opt-in plain prop on EntityNameDetailsLink/LinkWrapper — while staying a real, keyboard-focusable anchor.
  • CardWrapperProps.sx is now typed as SxProps<Theme> (was bare SxProps), removing an as SxProps cast at its one call site.

Tests

Manually tested

  • Capture and materialization variants, light and dark mode, in Storybook (empty state, loading state, task-erroring state, 1200-binding pagination with mixed enabled/disabled/no-data rows).
  • Search filter + clear button, status chip filters, column sorting, CSV export.
  • Verified against a real local backend (VM stack) — login, no console errors, TypeScript watch clean.

Automated tests

  • npm run typecheck, npm run lint, npm run format all clean.
  • npx vitest run — 364/364 passing, including existing coverage for Bindings/shared.ts.

Playwright tests ran locally

  • Admin
  • Captures
  • Collections
  • HomePage
  • Login
  • Materialization

Screenshots

Before ("Writes To" in side card):
image

After (bindings card):
image

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.
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

⚪ Code Health

No change to the dead-code surface.

48 Unused files

File imported nowhere — delete (or import) it.

     src/hooks/useDelay.ts
     src/hooks/useDraft.ts
     src/pages/NoGrants.tsx
     src/pages/OAuth.tsx
     src/services/encryption.ts
     src/types/global.ts
     src/types/vitest.ts
     src/components/graphs/TaskHoursByMonthGraph.tsx
     src/components/tables/Link.tsx
     src/context/LoopIndex/index.tsx
…and 38 more

66 Unused exports

Exported symbol with no references outside its own file — un-export it, or delete it if unused entirely

     src/context/Theme.tsx : logoColors
     src/context/Theme.tsx : intensifiedOutlineThick
     src/context/Theme.tsx : tableAlternateRowsSx
     src/context/Theme.tsx : draggableChipIconSx
     src/context/Theme.tsx : hiddenButAccessibleInput
     src/context/Theme.tsx : primaryColoredBackground_hovered
     src/context/Theme.tsx : detailsPanelBgColor
     src/context/Theme.tsx : menuBackgroundColor
     src/context/Theme.tsx : flexGrowToSiblingsSx
     src/context/Theme.tsx : shardTableRow
…and 56 more

30 Unused exported types

Exported type with no references outside its own file — un-export it, or delete it if unused entirely

     src/utils/billing-utils.ts : FREE_GB_BY_TIER
     src/types/index.ts : InferredSchemas
     src/types/index.ts : Shard
     src/components/shared/WizardDialog/index.ts : WizardStep
     src/api/dataPlanes.ts : AwsDnsEntry
     src/stores/ShardDetail/types.ts : TaskShardDetailsWithShard
     src/stores/ShardDetail/types.ts : ShardDetails
     src/components/tables/Logs/types.ts : RefreshLogsFunction
     src/types/schemaModels.ts : CollectionSchema
     src/components/editor/Bindings/types.ts : SelectedCollectionChangeData
…and 20 more

14 Unused exported enum members

An enum member referenced nowhere

     src/services/supabase.ts : CONNECTOR_TAGS
     src/services/supabase.ts : DRAFTS_EXT
     src/services/supabase.ts : TASKS_BY_DAY
     src/stores/Tables/hooks.ts : accessGrants
     src/stores/Tables/hooks.ts : accessLinks
     src/stores/Tables/hooks.ts : billing
     src/stores/Tables/hooks.ts : connectors
     src/stores/Tables/hooks.ts : entitySelector
     src/stores/Tables/hooks.ts : prefixes
     src/stores/Tables/hooks.ts : prefixAlerts
…and 4 more

5 Unused dependencies

In package.json but never imported

     package.json : @mui/lab
     package.json : @testing-library/jest-dom
     package.json : @urql/exchange-retry
     package.json : logrocket-react
     package.json : stripe

3 Unused devDependencies

In package.json devDependencies but never used

     package.json : @types/logrocket-react
     package.json : @types/react-inspector
     package.json : sharp

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.
@SeanWhelan
SeanWhelan requested a review from GregorShear August 13, 2026 22:13
@SeanWhelan
SeanWhelan marked this pull request as ready for review August 13, 2026 22:13
@SeanWhelan
SeanWhelan requested a review from a team as a code owner August 13, 2026 22:13
TASK_NAME and ShardStatusCode in the new story fixture were only ever
referenced within their own file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant