Skip to content

Commit 9a82b80

Browse files
committed
Move components to UI registry for topic detail
1 parent 8957908 commit 9a82b80

18 files changed

Lines changed: 1662 additions & 1171 deletions

File tree

frontend/src/components/pages/topics/Tab.Acl/acl-list.test.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,18 @@
1010
*/
1111

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

1416
import AclList from './acl-list';
17+
18+
vi.mock('@tanstack/react-router', async (importOriginal) => {
19+
const actual = await importOriginal<typeof import('@tanstack/react-router')>();
20+
return { ...actual, useLocation: () => ({ searchStr: '' }) };
21+
});
22+
23+
const renderWithAdapter = (ui: React.ReactElement) => render(<NuqsTestingAdapter>{ui}</NuqsTestingAdapter>);
24+
1525
import type {
1626
AclStrOperation,
1727
AclStrPermission,
@@ -26,7 +36,7 @@ describe('AclList', () => {
2636
aclResources: [],
2737
};
2838

29-
render(<AclList acl={store} />);
39+
renderWithAdapter(<AclList acl={store} />);
3040
expect(screen.getByText('No data found')).toBeInTheDocument();
3141
});
3242

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

53-
render(<AclList acl={store} />);
63+
renderWithAdapter(<AclList acl={store} />);
5464

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

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

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

73-
render(<AclList acl={store} />);
83+
renderWithAdapter(<AclList acl={store} />);
7484
expect(screen.getByText("There's no authorizer configured in your Kafka cluster")).toBeInTheDocument();
7585
});
7686
});

frontend/src/components/pages/topics/Tab.Acl/acl-list.tsx

Lines changed: 171 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,20 @@
99
* by the Apache License, Version 2.0
1010
*/
1111

12-
import { Alert, AlertIcon, DataTable } from '@redpanda-data/ui';
12+
import {
13+
type ColumnDef,
14+
flexRender,
15+
getCoreRowModel,
16+
getPaginationRowModel,
17+
getSortedRowModel,
18+
type PaginationState,
19+
type SortingState,
20+
type Updater,
21+
useReactTable,
22+
} from '@tanstack/react-table';
23+
import { parseAsBoolean, parseAsInteger, parseAsString, useQueryState } from 'nuqs';
1324

25+
import { useQueryStateWithCallback } from '../../../../hooks/use-query-state-with-callback';
1426
import type {
1527
AclRule,
1628
AclStrOperation,
@@ -19,91 +31,183 @@ import type {
1931
AclStrResourceType,
2032
GetAclOverviewResponse,
2133
} from '../../../../state/rest-interfaces';
34+
import { uiSettings } from '../../../../state/ui';
2235
import { toJson } from '../../../../utils/json-utils';
36+
import { DEFAULT_TABLE_PAGE_SIZE } from '../../../constants';
37+
import { Alert, AlertDescription } from '../../../redpanda-ui/components/alert';
38+
import { DataTableColumnHeader, DataTablePagination } from '../../../redpanda-ui/components/data-table';
39+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../../redpanda-ui/components/table';
2340

2441
type Acls = GetAclOverviewResponse | null | undefined;
2542

26-
type AclListProps = {
27-
acl: Acls;
43+
type AclFlatResource = {
44+
eqKey: string;
45+
principal: string;
46+
host: string;
47+
operation: AclStrOperation;
48+
permissionType: AclStrPermission;
49+
resourceType: AclStrResourceType;
50+
resourceName: string;
51+
resourcePatternType: AclStrResourcePatternType;
52+
acls: AclRule[];
2853
};
2954

30-
function flatResourceList(store: Acls) {
31-
const acls = store;
32-
if (!acls || acls.aclResources === null) {
55+
function flatResourceList(store: Acls): AclFlatResource[] {
56+
if (!store || store.aclResources === null) {
3357
return [];
3458
}
35-
const flatResources = acls.aclResources
59+
return store.aclResources
3660
.flatMap((res) => res.acls.map((rule) => ({ ...res, ...rule })))
3761
.map((x) => ({ ...x, eqKey: toJson(x) }));
38-
return flatResources;
3962
}
4063

41-
export default ({ acl }: AclListProps) => {
64+
const columns: ColumnDef<AclFlatResource>[] = [
65+
{
66+
accessorKey: 'resourceType',
67+
header: ({ column }) => <DataTableColumnHeader column={column} title="Resource" />,
68+
},
69+
{
70+
accessorKey: 'permissionType',
71+
header: ({ column }) => <DataTableColumnHeader column={column} title="Permission" />,
72+
},
73+
{
74+
accessorKey: 'principal',
75+
header: ({ column }) => <DataTableColumnHeader column={column} title="Principal" />,
76+
},
77+
{
78+
accessorKey: 'operation',
79+
header: ({ column }) => <DataTableColumnHeader column={column} title="Operation" />,
80+
},
81+
{
82+
accessorKey: 'resourcePatternType',
83+
header: ({ column }) => <DataTableColumnHeader column={column} title="PatternType" />,
84+
},
85+
{
86+
accessorKey: 'resourceName',
87+
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
88+
},
89+
{
90+
accessorKey: 'host',
91+
header: ({ column }) => <DataTableColumnHeader column={column} title="Host" />,
92+
},
93+
];
94+
95+
const AclList = ({ acl }: { acl: Acls }) => {
4296
const resources = flatResourceList(acl);
4397

98+
const [pageIndex, setPageIndex] = useQueryState('aclPage', parseAsInteger.withDefault(0));
99+
100+
const [pageSize, setPageSize] = useQueryStateWithCallback<number>(
101+
{
102+
onUpdate: (val) => {
103+
uiSettings.topicAclList.pageSize = val;
104+
},
105+
getDefaultValue: () => uiSettings.topicAclList.pageSize,
106+
},
107+
'aclPageSize',
108+
parseAsInteger.withDefault(DEFAULT_TABLE_PAGE_SIZE)
109+
);
110+
111+
const [sortId, setSortId] = useQueryStateWithCallback<string>(
112+
{
113+
onUpdate: (val) => {
114+
uiSettings.topicAclList.sortId = val;
115+
},
116+
getDefaultValue: () => uiSettings.topicAclList.sortId,
117+
},
118+
'aclSortId',
119+
parseAsString.withDefault('')
120+
);
121+
122+
const [sortDesc, setSortDesc] = useQueryStateWithCallback<boolean>(
123+
{
124+
onUpdate: (val) => {
125+
uiSettings.topicAclList.sortDesc = val;
126+
},
127+
getDefaultValue: () => uiSettings.topicAclList.sortDesc,
128+
},
129+
'aclSortDesc',
130+
parseAsBoolean.withDefault(false)
131+
);
132+
133+
const sorting: SortingState = sortId ? [{ id: sortId, desc: sortDesc }] : [];
134+
const pagination: PaginationState = { pageIndex, pageSize };
135+
136+
const handleSortingChange = (updater: Updater<SortingState>) => {
137+
const next = typeof updater === 'function' ? updater(sorting) : updater;
138+
if (next.length > 0) {
139+
setSortId(next[0].id);
140+
setSortDesc(next[0].desc);
141+
} else {
142+
setSortId('');
143+
setSortDesc(false);
144+
}
145+
void setPageIndex(0);
146+
};
147+
148+
const handlePaginationChange = (updater: Updater<PaginationState>) => {
149+
const next = typeof updater === 'function' ? updater(pagination) : updater;
150+
void setPageIndex(next.pageIndex);
151+
setPageSize(next.pageSize);
152+
};
153+
154+
const table = useReactTable({
155+
data: resources,
156+
columns,
157+
state: { sorting, pagination },
158+
onSortingChange: handleSortingChange,
159+
onPaginationChange: handlePaginationChange,
160+
getCoreRowModel: getCoreRowModel(),
161+
getSortedRowModel: getSortedRowModel(),
162+
getPaginationRowModel: getPaginationRowModel(),
163+
autoResetPageIndex: false,
164+
});
165+
44166
return (
45167
<>
46-
{acl === null ? (
47-
<Alert status="warning" style={{ marginBottom: '1em' }}>
48-
<AlertIcon />
49-
You do not have the necessary permissions to view ACLs
168+
{acl === null && (
169+
<Alert className="mb-4" variant="warning">
170+
<AlertDescription>You do not have the necessary permissions to view ACLs</AlertDescription>
50171
</Alert>
51-
) : null}
52-
{acl?.isAuthorizerEnabled ? null : (
53-
<Alert status="warning" style={{ marginBottom: '1em' }}>
54-
<AlertIcon />
55-
There's no authorizer configured in your Kafka cluster
172+
)}
173+
{acl?.isAuthorizerEnabled === false && (
174+
<Alert className="mb-4" variant="warning">
175+
<AlertDescription>There&apos;s no authorizer configured in your Kafka cluster</AlertDescription>
56176
</Alert>
57177
)}
58-
<DataTable<{
59-
eqKey: string;
60-
principal: string;
61-
host: string;
62-
operation: AclStrOperation;
63-
permissionType: AclStrPermission;
64-
resourceType: AclStrResourceType;
65-
resourceName: string;
66-
resourcePatternType: AclStrResourcePatternType;
67-
acls: AclRule[];
68-
}>
69-
columns={[
70-
{
71-
size: 120,
72-
header: 'Resource',
73-
accessorKey: 'resourceType',
74-
},
75-
{
76-
size: 120,
77-
header: 'Permission',
78-
accessorKey: 'permissionType',
79-
},
80-
{
81-
header: 'Principal',
82-
accessorKey: 'principal',
83-
},
84-
{
85-
size: 160,
86-
header: 'Operation',
87-
accessorKey: 'operation',
88-
},
89-
{
90-
header: 'PatternType',
91-
accessorKey: 'resourcePatternType',
92-
},
93-
{
94-
header: 'Name',
95-
accessorKey: 'resourceName',
96-
},
97-
{
98-
size: 120,
99-
header: 'Host',
100-
accessorKey: 'host',
101-
},
102-
]}
103-
data={resources}
104-
pagination
105-
sorting
106-
/>
178+
<Table>
179+
<TableHeader>
180+
{table.getHeaderGroups().map((headerGroup) => (
181+
<TableRow key={headerGroup.id}>
182+
{headerGroup.headers.map((header) => (
183+
<TableHead key={header.id}>
184+
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
185+
</TableHead>
186+
))}
187+
</TableRow>
188+
))}
189+
</TableHeader>
190+
<TableBody>
191+
{table.getRowModel().rows.length === 0 ? (
192+
<TableRow>
193+
<TableCell className="text-center" colSpan={columns.length}>
194+
No data found
195+
</TableCell>
196+
</TableRow>
197+
) : (
198+
table.getRowModel().rows.map((row) => (
199+
<TableRow key={row.id}>
200+
{row.getVisibleCells().map((cell) => (
201+
<TableCell key={cell.id}>{flexRender(cell.column.columnDef.cell, cell.getContext())}</TableCell>
202+
))}
203+
</TableRow>
204+
))
205+
)}
206+
</TableBody>
207+
</Table>
208+
<DataTablePagination table={table} />
107209
</>
108210
);
109211
};
212+
213+
export default AclList;

frontend/src/components/pages/topics/Tab.Messages/common/delete-records-menu-item.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
* by the Apache License, Version 2.0
1010
*/
1111

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

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

25-
let content: JSX.Element | string = 'Delete Records';
26+
const button = (
27+
<Button disabled={!isEnabled} onClick={onClick} variant="destructive-outline">
28+
Delete Records
29+
</Button>
30+
);
31+
2632
if (errorText) {
27-
content = (
28-
<Tooltip hasArrow label={errorText} placement="top">
29-
{content}
30-
</Tooltip>
33+
return (
34+
<TooltipProvider>
35+
<Tooltip>
36+
<TooltipTrigger asChild>
37+
<span>{button}</span>
38+
</TooltipTrigger>
39+
<TooltipContent side="top">{errorText}</TooltipContent>
40+
</Tooltip>
41+
</TooltipProvider>
3142
);
3243
}
3344

34-
return (
35-
<Button isDisabled={!isEnabled} onClick={onClick} variant="outline">
36-
{content}
37-
</Button>
38-
);
45+
return button;
3946
}

0 commit comments

Comments
 (0)