diff --git a/src/features/instance/config/users/constants/tableDefinition.test.tsx b/src/features/instance/config/users/constants/tableDefinition.test.tsx
new file mode 100644
index 000000000..439018298
--- /dev/null
+++ b/src/features/instance/config/users/constants/tableDefinition.test.tsx
@@ -0,0 +1,79 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { SimpleBrowseDataTable } from '@/components/SimpleBrowseDataTable';
+import { LocalUser } from '@/integrations/api/api.patch';
+import { act, cleanup, fireEvent, render, screen } from '@testing-library/react';
+import { afterEach, describe, expect, it } from 'vitest';
+import { dataTableColumns } from './tableDefinition';
+
+afterEach(() => cleanup());
+
+const CREATED = 1_700_000_000_000;
+
+function user(username: string, role: string, createdOffset = 0): LocalUser {
+ return {
+ username,
+ active: true,
+ role: { id: `role-${role}`, role, permission: {}, __createdtime__: CREATED, __updatedtime__: CREATED },
+ __createdtime__: CREATED + createdOffset,
+ __updatedtime__: CREATED + createdOffset,
+ };
+}
+
+function renderedUsernames() {
+ return Array.from(document.querySelectorAll('tbody tr')).map((row) => row.querySelector('td')?.textContent);
+}
+
+/** As the Users page renders it, including its initial username sort. */
+function renderUsers(users: LocalUser[]) {
+ render(
+ ,
+ );
+}
+
+async function typeFilter(text: string) {
+ await act(async () => {
+ fireEvent.change(screen.getByRole('searchbox', { name: 'Filter users' }), { target: { value: text } });
+ });
+}
+
+describe('instance users table columns', () => {
+ it('filters by username and role, but not by the active flag or the timestamps', async () => {
+ renderUsers([user('admin', 'super_user'), user('reader', 'read_only'), user('etl-bot', 'writer')]);
+
+ await typeFilter('READ');
+ expect(renderedUsernames()).toEqual(['reader']);
+
+ await typeFilter('writer');
+ expect(renderedUsernames()).toEqual(['etl-bot']);
+
+ // Every user is active and shares one timestamp prefix, so either would match the whole list.
+ await typeFilter('true');
+ expect(screen.getByText('No results.')).toBeTruthy();
+ await typeFilter(String(CREATED).slice(0, 6));
+ expect(screen.getByText('No results.')).toBeTruthy();
+ });
+
+ it('sorts by role, and by when the user was created', () => {
+ renderUsers([
+ user('carol', 'writer', 2_000),
+ user('alice', 'super_user', 3_000),
+ user('bob', 'read_only', 1_000),
+ ]);
+
+ expect(renderedUsernames()).toEqual(['alice', 'bob', 'carol']);
+
+ fireEvent.click(screen.getByRole('button', { name: 'Role' }));
+ expect(renderedUsernames()).toEqual(['bob', 'alice', 'carol']);
+
+ fireEvent.click(screen.getByRole('button', { name: 'Created' }));
+ expect(renderedUsernames()).toEqual(['bob', 'carol', 'alice']);
+ });
+});
diff --git a/src/features/instance/config/users/constants/tableDefinition.ts b/src/features/instance/config/users/constants/tableDefinition.ts
index b4c2cc3cc..e52174b5d 100644
--- a/src/features/instance/config/users/constants/tableDefinition.ts
+++ b/src/features/instance/config/users/constants/tableDefinition.ts
@@ -1,40 +1,40 @@
import { LocalUser } from '@/integrations/api/api.patch';
-import { ColumnDef, createColumnHelper } from '@/lib/table';
+import { ColumnDef } from '@/lib/table';
import { translateSecondsToAgo } from '@/lib/translateSecondsToAgo';
-const columnHelper = createColumnHelper();
-
export const dataTableColumns: Array> = [
{
header: 'Username',
accessorKey: 'username',
- enableSorting: false,
+ enableSorting: true,
},
- columnHelper.display({
+ {
header: 'Role',
- enableSorting: false,
id: 'role',
- cell: (props) => props.row.original.role.role,
- }),
+ accessorFn: (user) => user.role.role,
+ enableSorting: true,
+ },
{
header: 'Active',
accessorKey: 'active',
- enableSorting: false,
+ enableSorting: true,
},
- columnHelper.display({
+ {
header: 'Created',
- enableSorting: false,
- id: '__createdtime__',
+ accessorKey: '__createdtime__',
+ enableSorting: true,
+ enableGlobalFilter: false,
cell: (props) =>
translateSecondsToAgo(
(Date.now() - props.row.original.__createdtime__) / 1000,
props.row.original.__createdtime__,
),
- }),
+ },
{
header: 'Updated',
accessorKey: '__updatedtime__',
- enableSorting: false,
+ enableSorting: true,
+ enableGlobalFilter: false,
cell: (props) =>
translateSecondsToAgo(
(Date.now() - props.row.original.__updatedtime__) / 1000,
diff --git a/src/features/instance/config/users/index.tsx b/src/features/instance/config/users/index.tsx
index 82e47ec72..c3964d8f6 100644
--- a/src/features/instance/config/users/index.tsx
+++ b/src/features/instance/config/users/index.tsx
@@ -86,6 +86,8 @@ export function ConfigUsersIndex() {
columns={dataTableColumns}
onRowClick={onRowClick}
sortingState={sortingState}
+ filter={{ label: 'Filter users', placeholder: 'Filter by username or role' }}
+ paginated
>
{/**/}
{/*disabled={tableDataFetching}*/}