RPCN - Pipelines listing page speed + UX improvements - #2593
Conversation
* Full-screen page mode for SQL and RPCN editors, console-owned layout - Footer pins to the viewport bottom on short pages (CSS flex chain in standalone, measured min-height in embedded) and keeps centering to the content column; bottom padding 8px -> 16px. - Topics and security-tab pages drop ListLayout's forced min-h-screen (min-h-0 override), removing large dead whitespace. - Embedded Console cancels the Cloud UI host gutters with measured negative margins and owns its page gutter (px-12) — deploy-order-safe with cloud-ui removing its p-10 later. - New expanded-page mode: data-page-expanded on <html> (utils/page-expanded) + useExpandedPageMode hook release every shell's horizontal constraints via global CSS while the page stays in document flow, footer below. The SQL studio's fixed-overlay fullscreen is replaced by this in-flow mode, and the RPCN pipeline editor gains the same toggle; both place the shared ExpandedPageToggle at the top-right of their work surface, clear of Save. - /sql becomes a normal route; new breadcrumbOnlyHeader staticData flag keeps the app header breadcrumb-only for pages with their own title bar. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Comment reduction pass * some Pr feedback * Code review and cleanup passes * Small improvements from review * More changes from code review * More simplification --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
# Conflicts: # frontend/src/components/layout/header.tsx # frontend/src/components/pages/rp-connect/pipeline/index.tsx
🚨 Registry drift detectedApp:
Components needing attention
Refresh command: bunx shadcn@latest add @redpanda/sidebar --overwrite🎨 Off-token colours (palette literals)Use semantic tokens (
🔢 Ad-hoc utility classes (arbitrary values)These bypass the design tokens. Prefer a named scale entry or add a new token.
Generated by lookout audit-changes. |
|
@claude review |
This comment was marked as resolved.
This comment was marked as resolved.
| render={ | ||
| <Button | ||
| aria-label={isActive ? 'Pause auto refresh' : 'Start auto refresh'} | ||
| className="size-7" |
There was a problem hiding this comment.
iirc button has a child svg selector that specifies icon size, so if you're trying to make the svg bigger may want to >svg (can't remember syntax) here.
| <span> | ||
| Refresh the data shown on this page. When switching pages, any data older than{' '} | ||
| {prettyMilliseconds(REST_CACHE_DURATION_SEC * 1000)} is refreshed automatically. | ||
| </span> |
| > | ||
| {connectors.map((c) => ( | ||
| <Badge key={c.name} variant="neutral-inverted"> | ||
| <ConnectorLogo className="size-3.5" fallback={Box} name={c.name as ComponentName} /> |
There was a problem hiding this comment.
[shower thought]: i'd love to explore having classNames for icon sizes that at t-shirt sized, or add to our text utility classNames svg sizing so when we wrap an icon + label in a type util we know they will be similarly sized
| const isModifiedClick = (event: MouseEvent<HTMLElement>) => | ||
| event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0; |
There was a problem hiding this comment.
nit seems better fit for a keyboard util file
| const ListStatusAnnouncements = ({ | ||
| isLoadingMorePages, | ||
| listErrorMessage, | ||
| }: { | ||
| isLoadingMorePages: boolean; | ||
| listErrorMessage: string | null; | ||
| }) => ( | ||
| <> | ||
| <div aria-live="polite" className="sr-only"> | ||
| {isLoadingMorePages ? 'Loading more pipelines' : ''} | ||
| </div> | ||
| <div className="sr-only" role="alert"> | ||
| {listErrorMessage ?? ''} | ||
| </div> | ||
| </> | ||
| ); |
There was a problem hiding this comment.
[shower thought]: would love to upstream this to the pagination component in registry
| // Any token already requested, not just the last one: keyset tokens only move forward, so a | ||
| // repeat means the server sent us backwards (A→A or a longer A→B→A cycle) and the drain would | ||
| // loop forever, adding a page per round. O(pages) per step, ~20 for 10k pipelines. | ||
| if (!nextPageToken || allPageParams.some((param) => param?.pageToken === nextPageToken)) { |
There was a problem hiding this comment.
[minor]: this makes me think we should fix pushdown filters for pipelines list... code smell when I see so much sorting/deduping on the frontend especially wrt pagination
| * inline hints. Exit is quicker than enter, `initial={false}` animates changes but not | ||
| * the first render, and reduced-motion collapses the movement to a pure fade. | ||
| */ | ||
| export function FadePresence({ |
There was a problem hiding this comment.
[shower thought]: would love to see this upstreamed to ui-registry and start to explore tokenized motion with that effort
| /** | ||
| * Pipeline state options for filtering. | ||
| */ | ||
| export const PIPELINE_STATE_OPTIONS = [ |
There was a problem hiding this comment.
[follow]: I would be curious if this kind of tab/filter approach resonates with users. worth doing some usability testing with real customers
| <ThroughputPlaceholder | ||
| action={ | ||
| <Button className="mt-2" onClick={onRetry} size="sm" variant="outline"> | ||
| Try again | ||
| </Button> | ||
| } | ||
| description="The metrics service didn't respond. Data will appear once it's reachable." | ||
| title="Throughput metrics aren't available right now" | ||
| /> |
There was a problem hiding this comment.
[nit]: could potentially use ui-registry Empty component here, although it may be overengineered for this case. iirc, we may also have a chart-placeholder component 🤔 but that may just be something I've seen in cloud-ui
| // parseConfigComponents is a full YAML parse, and the transform re-runs over every row on each | ||
| // drain step and poll tick. Memoized per config text to keep that pass O(n). | ||
| const configComponentsCache = new Map<string, ReturnType<typeof parseConfigComponents>>(); | ||
| const CONFIG_COMPONENTS_CACHE_LIMIT = 10_000; | ||
|
|
||
| const parseConfigComponentsCached = (configYaml: string): ReturnType<typeof parseConfigComponents> => { | ||
| const cached = configComponentsCache.get(configYaml); | ||
| if (cached) { | ||
| return cached; | ||
| } | ||
| if (configComponentsCache.size >= CONFIG_COMPONENTS_CACHE_LIMIT) { | ||
| // Oldest half (Map preserves insertion order) — clearing all would reparse everything next refresh. | ||
| let surplus = CONFIG_COMPONENTS_CACHE_LIMIT / 2; | ||
| for (const key of configComponentsCache.keys()) { | ||
| configComponentsCache.delete(key); | ||
| surplus -= 1; | ||
| if (surplus <= 0) { | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| const parsed = parseConfigComponents(configYaml); | ||
| configComponentsCache.set(configYaml, parsed); | ||
| return parsed; | ||
| }; |
There was a problem hiding this comment.
[nit]: would rather this live in a utils file rather than .tsx
eblairmckee
left a comment
There was a problem hiding this comment.
left some nits, but otherwise I love the pagination improvements, a11y considerations, and cleaner "list" presentation of pipelines
Rebuild the Redpanda Connect pipelines list for large clusters
The pipeline list was built for a handful of pipelines. On clusters with hundreds it was slow to
render, offered no way to narrow down what you were looking for, and buried broken pipelines
wherever the server happened to return them. This reworks the page around finding one pipeline in
a long list, and fixes the cost of getting that list in the first place.
List page
with their destination (starting counts as running, stopping as stopped). Counts come from the
status column's faceted row model, so each tab shows what selecting it would yield under the
current search and filters.
Clear filtersappearsonly when something is actually filtered — the status tabs are views, not filters, so they aren't
swept up by it — and it tracks the input directly rather than lagging 200ms behind the debounce.
transitions above healthy pipelines, idle at the bottom), so a broken pipeline lands on page 1 of a
large cluster instead of wherever the server put it.
DataTableuses: clicks on portaledcontent (open menus, the delete-confirm backdrop) and on interactive descendants don't navigate,
and neither does a click that ends a text selection — the pipeline ID is
select-all, so one clickgrabs the whole thing for copying. ⌘/Ctrl/Shift-click and middle-click are left to the browser, so
"open in a new tab" still means that.
redpanda ×2rather than repeating the same logoacross the column.
renders as soon as the first page has rows and streams the rest in behind the table, with a
distinct line for "still loading pages" vs "background refresh failed" — partial data and stale
data read differently now.
keepMounted— Base UIpanels unmount by default).
DataTableFilterstack from this page.Accessibility
panels), so without an
aria-controlstarget a screen reader announced "tab, 1 of 4" with nowhereto move into. Each tab now points at the table region, which is labelled by the active tab.
without user action, so each has an always-mounted
sr-onlylive region (polite for the drain,role="alert"for the failure) — the visible lines animate in and out, and a live region onlyannounces changes made while it's already in the DOM.
tab stop per row would just duplicate it.
Fetch and render cost
Page size 500 instead of
MAX_PAGE_SIZE(which is 25) — 20× fewer sequential round trips todrain. The server does the same work per call at any page size: it lists everything and slices.
Deduplicate the drain by pipeline ID. The dataplane's keyset page token names the first ID of
the next page; when that pipeline is deleted mid-drain, a server resolving the token by exact match
restarts at page one and replays rows we already have.
Stop draining on any token the drain already requested. Keyset tokens only move forward, so a
repeat means the server sent us backwards. The first cut only caught an immediate
A → Arepeat,which a
A → B → Acycle walks straight past — the drain then alternates forever, adding a page tothe query cache every round. Now checked against every prior page param, with a test for each shape.
Memoize the YAML parse per config text. The transform re-ran a full parse for every row on every
drain step and poll tick; it's now O(new rows), with a bounded cache that evicts its oldest half.
Row identity keyed on pipeline ID (not row index) and
autoResetPageIndex: false, so streamingpages don't yank you back to page 1 or repaint a shifted window of rows. Filter and sort changes
still reset to page 1, and a shrinking row set is clamped before paint.
Cache the facet icon component per connector name, so logos in an open filter popover don't remount
and flash on every poll; count the status tabs in a single pass; and memoize the per-row connector
aggregation so cells don't re-derive it on every keystroke.
Video
Screen.Recording.2026-08-11.at.7.52.02.AM.mov