feat(consumers): migrate consumer groups to UI Registry#2545
Conversation
Migrate the consumer groups list, detail page, and the edit/delete offset modals from legacy @redpanda-data/ui to the Registry components. List (group-list.tsx): - TanStack useReactTable + Registry Table inside ListLayout - URL-persisted page/pageSize/sort/search (nuqs), sortable columns, pagination, and a State faceted filter (all states always available) - New useLegacyListConsumerGroupsFullQuery hook (Connect/REST) returning enriched GroupDescription (lagSum/isInUse/perms) Detail (group-details.tsx): - Registry Tabs (Topics/ACL, tab persisted in URL), Card stats bar, Accordion per topic, and a sortable+paginated PartitionTable - Disabled Edit/Delete buttons now show an explanatory tooltip - Shared ConsumerGroupStateCell (icon + state) reused by list and detail Modals (modals.tsx): - DeleteOffsetsModal -> Registry AlertDialog - EditOffsetsModal -> Registry Dialog/Select/RadioGroup/Input/Accordion + useReactTable preview tables; toasts moved to sonner - KowlTimePicker kept (no Registry datetime picker yet) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
✅ Clean — no registry drift, off-token colours, or ad-hoc classesApp:
Generated by lookout audit-changes. |
base-ui SelectValue renders the raw selected value, so the "All
Topics/Partitions" sentinel ('__all__') and the strategy keys
(endOffset, ...) leaked into the trigger. Map value -> label via a
SelectValue render function, matching the add-acl-dialog pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Empty groups have no protocol/protocolType, which rendered blank stat values, and the legacy ProtocolType helper duplicated protocolType under a "Protocol" label. Show the actual group.protocol field (matching the list column) and group.protocolType with an em-dash fallback, and drop the redundant ProtocolType component. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Coordinator ID copy button used size="icon" (size-9, 36px), which was much taller than the stat label text; the flex row stretched every stat to that height, inflating the card and leaving gaps under the other labels. Shrink the copy button to size-5 and add items-start so the row hugs its natural content height. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The migrated partition action buttons use the registry (base-ui) tooltip,
which renders a `[data-slot="tooltip-content"]` popup without
role="tooltip" (the old Chakra tooltip had it). Match on the slot + text
instead of getByRole('tooltip'). No app code changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Local e2e runs remap every host port via E2E_PORTS_OVERRIDE, but the enterprise config hardcoded shadowBackendURL to http://localhost:3101, so the shadowlink tests hit ERR_CONNECTION_REFUSED (the dest backend was on a random port). Resolve the shadow backend port from the override (falling back to 3101 for CI/static-port runs). No app code changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Move DisabledReasonButton out of group-details.tsx into src/components/ui/ so it can be reused outside consumer group details. Behavior unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two layout fixes surfaced by the group list page: - group-list rendered its table inside ListLayoutContent (flex-1), which grew to fill the min-h-screen layout and pushed the pagination bar to the bottom of the viewport. Render <Table> directly like the other list pages so pagination sits under the last row. - ListLayoutPagination did not stretch its child, so DataTablePagination collapsed to content width and its internal justify-between clustered the controls on the left. Add [&>*]:w-full so the controls span the row (also fixes topics/quotas/users/roles list pages). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y drift The pagination full-width fix edited the vendored list-layout registry component in-place, which the UI audit flags as locally-modified drift. Revert list-layout to its released bytes and apply [&>*]:w-full via className at the ListLayoutPagination call site instead — same render, no registry drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Console's dialog and tabs were pinned to registry 2.3.0's predecessor
(2.2.0), which the frontend UI audit flags as outdated once the consumer
pages use them. Sync both to the 2.3.0 release:
- dialog: scroll-shadow refactor — useScrollShadow now takes the container
ref and returns axis-relative { start, end } edges (was sentinel refs +
{ top, bottom }).
- use-scroll-shadow: rewritten to the 2.3.0 geometry-based API (scroll +
ResizeObserver/MutationObserver, horizontal orientation support). Only
caller in console is DialogBody; rp-connect only mentions it in a comment.
- tabs: add ScrollableTabsList, pulling in the new drag-scroll-area dep.
- drag-scroll-area: new component from 2.3.0.
Audit now reports dialog + tabs up-to-date at 2.3.0; 0 drift.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| // Registry (base-ui) tooltip renders a `[data-slot="tooltip-content"]` popup | ||
| // without role="tooltip", so match on the slot + text instead of the role. |
There was a problem hiding this comment.
sounds like we should fix this so we can have simpler test assertions to write
| // without role="tooltip", so match on the slot + text instead of the role. | ||
| await expect( | ||
| page.locator('[data-slot="tooltip-content"]').filter({ hasText: 'No committed offset' }).first() | ||
| ).toBeVisible({ timeout: 5000 }); |
There was a problem hiding this comment.
I would refrain from using timeouts if possible
| * | ||
| * Uses the legacy REST API (same reason as {@link useLegacyListConsumerGroupsQuery}): | ||
| * authorization is only possible with Console v3 and above. | ||
| * TODO: Remove once Console v3 is released. |
There was a problem hiding this comment.
it looks like we are still using this despite console v3 being released a long time ago, should we clean it up?
There was a problem hiding this comment.
There is a separate ticket for this.
| variant?: 'ghost' | 'outline' | 'link'; | ||
| size?: 'icon-sm' | 'sm' | 'md'; |
There was a problem hiding this comment.
I would just map it from registry so it never gets out of sync, using proper types
| }, | ||
| }); | ||
|
|
||
| const dialogScrollShadow = 'pointer-events-none sticky z-10 h-0 transition-opacity duration-150'; |
There was a problem hiding this comment.
are all of these properly upstreamed?
There was a problem hiding this comment.
It's update from the UI registry (see commits)
Base UI's tooltip popup sets no ARIA role, so the disabled-action tooltip was only reachable via a [data-slot] selector. Set role="tooltip" at the call site (keeping the registry component free of drift) so it is exposed as a tooltip to assistive tech and to getByRole in tests. Derive the button variant/size props from the registry ButtonProps so they cannot drift from the Button they are forwarded to. Drop the per-assertion timeouts in the e2e spec: the Playwright config already sets a 60s expect timeout, so they were only shortening the default. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tooltip reads "No committed offsets" (plural). Switching the assertion from a substring filter to toHaveText made the exact text matter, so the singular string no longer matched and the test timed out. Pull the string into a constant so it stays tied to group-details.tsx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| const StatItem = ({ label, value }: { label: ReactNode; value: ReactNode }) => ( | ||
| <div className="flex flex-col gap-0.5"> | ||
| <div className="font-semibold text-lg tabular-nums">{value}</div> | ||
| <Text className="text-muted-foreground text-sm">{label}</Text> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
we have a Stat component now https://redpanda-ui-registry.netlify.app/docs/stat
| </div> | ||
| </Section> | ||
| <Card className="gap-0 px-6 py-4" size="full" variant="standard"> | ||
| <CardContent className="flex flex-wrap items-start gap-x-12 gap-y-4"> |
There was a problem hiding this comment.
| <CardContent className="flex flex-wrap items-start gap-x-12 gap-y-4"> | |
| <CardContent className="flex flex-wrap items-start justify-between gap-4"> |
instead of the massive gap-x-12
| /> | ||
| </Section> | ||
| <Tabs onValueChange={(value) => onSearchChange({ tab: value as GroupTab })} value={activeTab}> | ||
| <TabsList activeClassName="after:bg-foreground" className="w-fit" variant="underline"> |
There was a problem hiding this comment.
is there a reason for this? activeClassName="after:bg-foreground" seems to break from intended styling
| <ListLayoutSearchInput | ||
| className={quickSearch ? 'pr-8' : 'pl-8'} | ||
| onChange={(e) => { | ||
| setQuickSearch(e.target.value); | ||
| onSearchChange({ q: e.target.value }); | ||
| }} | ||
| placeholder="Filter by member" | ||
| value={quickSearch} | ||
| /> |
There was a problem hiding this comment.
Can we use registry Input + InputStart (see docs for how to use) --it'll handle the padding and positioning for you
| <button | ||
| className="absolute top-1/2 right-2 -translate-y-1/2 text-muted-foreground hover:text-foreground" | ||
| onClick={() => { | ||
| setQuickSearch(''); | ||
| onSearchChange({ q: '' }); | ||
| }} | ||
| type="button" | ||
| > | ||
| <X className="h-4 w-4" /> | ||
| </button> |
There was a problem hiding this comment.
this looks like filters? maybe use data-table-filter from registry? or tag input?
| <label className="flex cursor-pointer items-center gap-2 text-sm"> | ||
| <Checkbox | ||
| checked={showWithLagOnly} | ||
| onCheckedChange={(checked) => onSearchChange({ withLag: checked === true })} | ||
| /> | ||
| Only show topics with lag | ||
| </label> |
There was a problem hiding this comment.
can we use registry Label here. can pass htmlFor to associate with the checkbox
| {table.getHeaderGroups().map((headerGroup) => ( | ||
| <TableRow key={headerGroup.id}> | ||
| {headerGroup.headers.map((header) => { | ||
| type Meta = { align?: 'right'; headWidth?: 'auto' | 'sm' | 'md' | 'lg' | 'xl' | 'fit' | 'full' }; |
There was a problem hiding this comment.
why are we declaring a type within a render?
|
|
||
| const isFiltered = columnFilters.length > 0; | ||
| return ( | ||
| <TableRow className="hover:bg-transparent"> |
There was a problem hiding this comment.
pretty sure there's also a selected: background color, too
| <Text className="font-semibold text-2xl tabular-nums">{statistics.total}</Text> | ||
| <Text className="text-muted-foreground text-sm">Total groups</Text> |
| {statistics.byState.map(({ state, count }) => ( | ||
| <div className="flex flex-col gap-0.5" key={state}> | ||
| <Text className="font-semibold text-2xl tabular-nums">{count}</Text> | ||
| <Text className="text-muted-foreground text-sm">{state}</Text> | ||
| </div> | ||
| ))} |
| <span | ||
| className="pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-muted-foreground" | ||
| data-testid="search-field-search-icon" | ||
| > | ||
| <Search className="h-4 w-4" /> | ||
| </span> |
There was a problem hiding this comment.
same comment as earlier about using registry components > these custom ones
Adopt the registry Stat component for the group list and detail stats bars, replacing the hand-rolled label/value stacks. Swap both search fields to registry Input + InputStart/InputEnd so the padding and icon positioning come from the component instead of ad-hoc absolute positioning, and associate the "topics with lag" checkbox with a registry Label via htmlFor. Drop the activeClassName="after:bg-foreground" override on the tabs so the underline uses the registry's intended after:bg-selected, and remove the hover:bg-transparent override on the empty-state row: TableRow has no hover background, only data-[state=selected], which this row never gets. Hoist the column meta type out of render into a shared module, derived from the registry's tableHeadVariants so it cannot drift, and compose the disabled trigger in DisabledReasonButton from buttonVariants rather than hardcoding h-8 w-8. stat.tsx is vendored from ui-registry v2.3.0 to match the tagged release the audit classifies against. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Migrates the consumer groups feature (list, detail, and the edit/delete offset modals) from the legacy
@redpanda-data/uicomponents to the Registry components — matching the already-migrated Topics/Security pages.List (
group-list.tsx)useReactTable+ RegistryTableinsideListLayoutnuqs), sortable columns, paginationuseLegacyListConsumerGroupsFullQueryhook returning enrichedGroupDescription(lagSum/isInUse/perms)Detail (
group-details.tsx)Tabs(Topics / ACL, active tab persisted in the URL),Cardstats bar,Accordionper topic, sortable + paginatedPartitionTableConsumerGroupStateCell(icon + state) reused by both list and detailModals (
modals.tsx)DeleteOffsetsModal→ RegistryAlertDialogEditOffsetsModal→ RegistryDialog/Select/RadioGroup/Input/Accordion+useReactTablepreview tables; toasts moved tosonner.KowlTimePickerkept (no Registry datetime picker yet).modals.tsxno longer imports@redpanda-data/uiat all.Test plan
bun run type:check— passbun run lint— passbun run test:integration— all consumer-group tests pass (group-details2/2); full suite green apart from one unrelated flakyobservability-pagetest that passes in isolation🤖 Generated with Claude Code