Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
34cd134
Move components to UI registry for topic detail
jvorcak Jun 5, 2026
4d64615
test(e2e): capture backend container logs when start() fails in shado…
jvorcak Jun 8, 2026
c27da9c
test(e2e): buffer backend logs via log consumer to survive container …
jvorcak Jun 8, 2026
c32443f
Add empty placeholderst ot he tab-consumers and tab-partitions
jvorcak Jun 8, 2026
50ec785
Migrate expanded message components to UI registry
jvorcak Jun 8, 2026
bc3b054
Add grouped, navigable topic Configuration tab behind enableNewTopicPage
jvorcak Jun 9, 2026
7ca290b
test(e2e): assert grouped Configuration layout (sidebar + section hea…
jvorcak Jun 9, 2026
4932bce
Remove TopicConfiguration.scss
jvorcak Jun 9, 2026
ed337aa
Addressed comments from the PR
jvorcak Jun 15, 2026
8b6b1bd
Merge remote-tracking branch 'origin/master' into UX-998-migrate-topi…
jvorcak Jun 16, 2026
f027a7b
fix(data-table): always render pagination footer
jvorcak Jun 16, 2026
16deb8f
fix(ui): make DataTablePagination span full width so controls align r…
jvorcak Jun 16, 2026
0872b73
fix(topics): make config category sidebar scroll to sections instead …
jvorcak Jun 16, 2026
25c78b0
fix(topics): use registry Button for partition error popover trigger
jvorcak Jun 16, 2026
1fd08a7
fix(topics): migrate JS filter modal to UI registry and polish messag…
jvorcak Jun 16, 2026
b1f2993
Merge branch 'master' into UX-998-migrate-topic-detail-to-ui-registry
jvorcak Jun 19, 2026
4935579
Improves padding in topic configuration modal
jvorcak Jun 23, 2026
6d11f2c
Remove enableNewTopicPage FF
jvorcak Jun 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion frontend/src/components/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export const FEATURE_FLAGS = {
enableConnectSlashMenu: false,
enableNewSecurityPage: true,
enableTeamsBridge: false,
enableNewTopicPage: true,
};

// Cloud-managed tag keys for service account integration
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/components/pages/topics/Tab.Acl/acl-list.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,18 @@
*/

import { render, screen } from '@testing-library/react';
import { NuqsTestingAdapter } from 'nuqs/adapters/testing';
import { vi } from 'vitest';

import AclList from './acl-list';

vi.mock('@tanstack/react-router', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-router')>();
return { ...actual, useLocation: () => ({ searchStr: '' }) };
});

const renderWithAdapter = (ui: React.ReactElement) => render(<NuqsTestingAdapter>{ui}</NuqsTestingAdapter>);

import type {
AclStrOperation,
AclStrPermission,
Expand All @@ -26,7 +36,7 @@ describe('AclList', () => {
aclResources: [],
};

render(<AclList acl={store} />);
renderWithAdapter(<AclList acl={store} />);
expect(screen.getByText('No data found')).toBeInTheDocument();
});

Expand All @@ -50,7 +60,7 @@ describe('AclList', () => {
],
} as GetAclOverviewResponse;

render(<AclList acl={store} />);
renderWithAdapter(<AclList acl={store} />);

expect(screen.getByText('Topic')).toBeInTheDocument();
expect(screen.getByText('Test Topic')).toBeInTheDocument();
Expand All @@ -60,7 +70,7 @@ describe('AclList', () => {
});

test('informs user about missing permission to view ACLs', () => {
render(<AclList acl={null} />);
renderWithAdapter(<AclList acl={null} />);
expect(screen.getByText('You do not have the necessary permissions to view ACLs')).toBeInTheDocument();
});

Expand All @@ -70,7 +80,7 @@ describe('AclList', () => {
aclResources: [],
};

render(<AclList acl={store} />);
renderWithAdapter(<AclList acl={store} />);
expect(screen.getByText("There's no authorizer configured in your Kafka cluster")).toBeInTheDocument();
});
});
183 changes: 116 additions & 67 deletions frontend/src/components/pages/topics/Tab.Acl/acl-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,16 @@
* by the Apache License, Version 2.0
*/

import { Alert, AlertIcon, DataTable } from '@redpanda-data/ui';
import {
type ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable,
} from '@tanstack/react-table';

import { useUrlTableState } from '../../../../hooks/use-url-table-state';
import type {
AclRule,
AclStrOperation,
Expand All @@ -19,91 +27,132 @@ import type {
AclStrResourceType,
GetAclOverviewResponse,
} from '../../../../state/rest-interfaces';
import { uiSettings } from '../../../../state/ui';
import { toJson } from '../../../../utils/json-utils';
import { Alert, AlertDescription } from '../../../redpanda-ui/components/alert';
import { DataTableColumnHeader, DataTablePagination } from '../../../redpanda-ui/components/data-table';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../../redpanda-ui/components/table';

type Acls = GetAclOverviewResponse | null | undefined;

type AclListProps = {
acl: Acls;
type AclFlatResource = {
eqKey: string;
principal: string;
host: string;
operation: AclStrOperation;
permissionType: AclStrPermission;
resourceType: AclStrResourceType;
resourceName: string;
resourcePatternType: AclStrResourcePatternType;
acls: AclRule[];
};

function flatResourceList(store: Acls) {
const acls = store;
if (!acls || acls.aclResources === null) {
function flatResourceList(store: Acls): AclFlatResource[] {
if (!store || store.aclResources === null) {
return [];
}
const flatResources = acls.aclResources
return store.aclResources
.flatMap((res) => res.acls.map((rule) => ({ ...res, ...rule })))
.map((x) => ({ ...x, eqKey: toJson(x) }));
return flatResources;
}

export default ({ acl }: AclListProps) => {
const columns: ColumnDef<AclFlatResource>[] = [
{
accessorKey: 'resourceType',
header: ({ column }) => <DataTableColumnHeader column={column} title="Resource" />,
},
{
accessorKey: 'permissionType',
header: ({ column }) => <DataTableColumnHeader column={column} title="Permission" />,
},
{
accessorKey: 'principal',
header: ({ column }) => <DataTableColumnHeader column={column} title="Principal" />,
},
{
accessorKey: 'operation',
header: ({ column }) => <DataTableColumnHeader column={column} title="Operation" />,
},
{
accessorKey: 'resourcePatternType',
header: ({ column }) => <DataTableColumnHeader column={column} title="PatternType" />,
},
{
accessorKey: 'resourceName',
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
},
{
accessorKey: 'host',
header: ({ column }) => <DataTableColumnHeader column={column} title="Host" />,
},
];

const AclList = ({ acl }: { acl: Acls }) => {
const resources = flatResourceList(acl);

const { sorting, pagination, onSortingChange, onPaginationChange } = useUrlTableState({
keyPrefix: 'acl',
settings: uiSettings.topicAclList,
rowCount: resources.length,
});

const table = useReactTable({
data: resources,
columns,
state: { sorting, pagination },
onSortingChange,
onPaginationChange,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
autoResetPageIndex: false,
});

return (
<>
{acl === null ? (
<Alert status="warning" style={{ marginBottom: '1em' }}>
<AlertIcon />
You do not have the necessary permissions to view ACLs
{acl === null && (
<Alert className="mb-4" variant="warning">
<AlertDescription>You do not have the necessary permissions to view ACLs</AlertDescription>
</Alert>
) : null}
{acl?.isAuthorizerEnabled ? null : (
<Alert status="warning" style={{ marginBottom: '1em' }}>
<AlertIcon />
There's no authorizer configured in your Kafka cluster
)}
{acl?.isAuthorizerEnabled === false && (
<Alert className="mb-4" variant="warning">
<AlertDescription>There&apos;s no authorizer configured in your Kafka cluster</AlertDescription>
</Alert>
)}
<DataTable<{
eqKey: string;
principal: string;
host: string;
operation: AclStrOperation;
permissionType: AclStrPermission;
resourceType: AclStrResourceType;
resourceName: string;
resourcePatternType: AclStrResourcePatternType;
acls: AclRule[];
}>
columns={[
{
size: 120,
header: 'Resource',
accessorKey: 'resourceType',
},
{
size: 120,
header: 'Permission',
accessorKey: 'permissionType',
},
{
header: 'Principal',
accessorKey: 'principal',
},
{
size: 160,
header: 'Operation',
accessorKey: 'operation',
},
{
header: 'PatternType',
accessorKey: 'resourcePatternType',
},
{
header: 'Name',
accessorKey: 'resourceName',
},
{
size: 120,
header: 'Host',
accessorKey: 'host',
},
]}
data={resources}
pagination
sorting
/>
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell className="text-center" colSpan={columns.length}>
No data found
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
<DataTablePagination table={table} />
</>
);
};

export default AclList;
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
* by the Apache License, Version 2.0
*/

import { Button, Tooltip } from '@redpanda-data/ui';
import { Button } from 'components/redpanda-ui/components/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'components/redpanda-ui/components/tooltip';

import type { TopicAction } from '../../../../../state/rest-interfaces';
import { getDeleteErrorText, isDeleteEnabled } from '../helpers';
Expand All @@ -22,18 +23,24 @@ export function DeleteRecordsMenuItem(
const isEnabled = isDeleteEnabled(isCompacted, allowedActions);
const errorText = getDeleteErrorText(isCompacted, allowedActions);

let content: JSX.Element | string = 'Delete Records';
const button = (
<Button disabled={!isEnabled} onClick={onClick} variant="destructive-outline">
Delete Records
</Button>
);

if (errorText) {
content = (
<Tooltip hasArrow label={errorText} placement="top">
{content}
</Tooltip>
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<span>{button}</span>
</TooltipTrigger>
<TooltipContent side="top">{errorText}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}

return (
<Button isDisabled={!isEnabled} onClick={onClick} variant="outline">
{content}
</Button>
);
return button;
}
Loading
Loading