Skip to content

RPCN - Pipelines listing page speed + UX improvements - #2593

Merged
SpicyPete merged 25 commits into
masterfrom
rpcn/many-many-pipelines-listings
Aug 11, 2026
Merged

RPCN - Pipelines listing page speed + UX improvements#2593
SpicyPete merged 25 commits into
masterfrom
rpcn/many-many-pipelines-listings

Conversation

@SpicyPete

@SpicyPete SpicyPete commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

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

  • Status tabs — All / Running / Stopped / Error, each with a live count. Transitional states ride
    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.
  • Search by name or ID, debounced, matching case-insensitively against both.
  • Faceted filters for input, output, and tag, with per-option counts. Clear filters appears
    only 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.
  • Sortable Pipeline and Status columns. Status sorts attention-first by default (errors and
    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.
  • Rows are clickable, with the same guard the registry DataTable uses: clicks on portaled
    content (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 click
    grabs 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.
  • Connector badges collapse duplicates into redpanda ×2 rather than repeating the same logo
    across the column.
  • Progressive rendering. The list used to wait for every page before showing anything; it now
    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.
  • Search, facets and page survive a trip to the Kafka Connect tab and back (keepMounted — Base UI
    panels unmount by default).
  • Dropped the Processors column and the old DataTableFilter stack from this page.

Accessibility

  • The status tabs own a panel. They sit outside the table (one table filtered per tab, not four
    panels), so without an aria-controls target a screen reader announced "tab, 1 of 4" with nowhere
    to move into. Each tab now points at the table region, which is labelled by the active tab.
  • Async status is announced. "Loading more pipelines" and "Couldn't refresh pipelines" appear
    without user action, so each has an always-mounted sr-only live region (polite for the drain,
    role="alert" for the failure) — the visible lines animate in and out, and a live region only
    announces changes made while it's already in the DOM.
  • Row click stays a pointer shortcut with no row tab stop: the name cell is already a real link, so a
    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 to
    drain. 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 → A repeat,
    which a A → B → A cycle walks straight past — the drain then alternates forever, adding a page to
    the 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 streaming
    pages 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

SpicyPete and others added 13 commits July 28, 2026 11:38
* 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
@SpicyPete
SpicyPete requested review from a team and eblairmckee August 5, 2026 15:00
@SpicyPete SpicyPete self-assigned this Aug 5, 2026
@SpicyPete
SpicyPete requested review from Mateoc, datamali and yougotashovel and removed request for a team August 5, 2026 15:00
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🚨 Registry drift detected

App: frontend · Scope: diff vs origin/master · Files: 34

Count
⚠️ Outdated registry components 1
🛠 Locally-modified components 0
❓ Unknown to registry 0
🎨 Off-token palette colours 68
🔢 Ad-hoc utility classes 1
Components needing attention
Status Component Uses Detail
⚠️ outdated sidebar installed 2.4.1 → latest 2.5.1

Refresh command:

bunx shadcn@latest add @redpanda/sidebar --overwrite
🎨 Off-token colours (palette literals)

Use semantic tokens (primary, muted-foreground, border, …) instead of raw palette names.

Class Uses Files
indigo-400 14 1
indigo-500 9 1
indigo-300 7 1
indigo-alpha-200 7 1
blue-400 6 1
indigo-600 6 1
indigo-800 6 1
blue-900 5 1
green-500 5 1
indigo-100 5 1
orange-400 5 1
red-600 5 1
blue-500 4 1
blue-alpha-200 4 1
green-400 4 1
indigo-200 4 1
indigo-900 4 1
indigo-alpha-100 4 1
indigo-alpha-300 4 1
orange-500 4 1
red-400 4 1
red-500 4 1
red-alpha-200 4 1
blue-600 3 1
blue-800 3 1
green-300 3 1
green-600 3 1
orange-200 3 1
blue-100 2 1
blue-200 2 1
blue-300 2 1
blue-alpha-100 2 1
blue-alpha-300 2 1
blue-alpha-500 2 1
green-100 2 1
green-50 2 1
green-700 2 1
green-800 2 1
green-900 2 1
indigo-50 2 1
indigo-700 2 1
indigo-alpha-500 2 1
orange-100 2 1
orange-300 2 1
orange-800 2 1
orange-900 2 1
red-200 2 1
red-300 2 1
red-50 2 1
red-alpha-100 2 1
red-alpha-300 2 1
red-alpha-500 2 1
blue-alpha-400 1 1
green-200 1 1
green-alpha-100 1 1
green-alpha-200 1 1
indigo-alpha-400 1 1
orange-50 1 1
orange-600 1 1
orange-700 1 1
orange-950 1 1
purple-500 1 1
purple-600 1 1
red-100 1 1
red-700 1 1
red-800 1 1
red-950 1 1
red-alpha-400 1 1
🔢 Ad-hoc utility classes (arbitrary values)

These bypass the design tokens. Prefer a named scale entry or add a new token.

Class Category Uses Files
rounded-[4px] 🔢 radius 1 1

Generated by lookout audit-changes.

@SpicyPete

Copy link
Copy Markdown
Contributor Author

@claude review

@github-actions

This comment was marked as resolved.

@redpanda-data redpanda-data deleted a comment from github-actions Bot Aug 10, 2026
render={
<Button
aria-label={isActive ? 'Pause auto refresh' : 'Start auto refresh'}
className="size-7"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +127 to +130
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice

>
{connectors.map((c) => (
<Badge key={c.name} variant="neutral-inverted">
<ConnectorLogo className="size-3.5" fallback={Box} name={c.name as ComponentName} />

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment on lines +215 to +216
const isModifiedClick = (event: MouseEvent<HTMLElement>) =>
event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || event.button !== 0;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit seems better fit for a keyboard util file

Comment on lines +227 to +242
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>
</>
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[shower thought]: would love to upstream this to the pagination component in registry

Comment on lines +116 to +119
// 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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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 = [

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[follow]: I would be curious if this kind of tab/filter approach resonates with users. worth doing some usability testing with real customers

Comment on lines +90 to +98
<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"
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment on lines +108 to +132
// 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;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit]: would rather this live in a utils file rather than .tsx

@eblairmckee eblairmckee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left some nits, but otherwise I love the pagination improvements, a11y considerations, and cleaner "list" presentation of pipelines

@SpicyPete
SpicyPete enabled auto-merge (squash) August 11, 2026 21:05
@SpicyPete
SpicyPete merged commit 409d6a2 into master Aug 11, 2026
16 checks passed
@SpicyPete
SpicyPete deleted the rpcn/many-many-pipelines-listings branch August 11, 2026 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants