Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
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