refactor(ui/datatables): allow datatable to globally filter on object value [EE-5824] (#9955)

pull/10081/head
Chaim Lev-Ari 2023-09-04 10:33:07 +01:00 committed by GitHub
parent 440f4e8dda
commit cb7377ead6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 271 additions and 186 deletions

View File

@ -13,7 +13,6 @@ import {
getFacetedMinMaxValues, getFacetedMinMaxValues,
getExpandedRowModel, getExpandedRowModel,
TableOptions, TableOptions,
TableMeta,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { ReactNode, useMemo } from 'react'; import { ReactNode, useMemo } from 'react';
import clsx from 'clsx'; import clsx from 'clsx';
@ -28,7 +27,7 @@ import { DatatableFooter } from './DatatableFooter';
import { defaultGetRowId } from './defaultGetRowId'; import { defaultGetRowId } from './defaultGetRowId';
import { Table } from './Table'; import { Table } from './Table';
import { useGoToHighlightedRow } from './useGoToHighlightedRow'; import { useGoToHighlightedRow } from './useGoToHighlightedRow';
import { BasicTableSettings } from './types'; import { BasicTableSettings, DefaultType } from './types';
import { DatatableContent } from './DatatableContent'; import { DatatableContent } from './DatatableContent';
import { createSelectColumn } from './select-column'; import { createSelectColumn } from './select-column';
import { TableRow } from './TableRow'; import { TableRow } from './TableRow';
@ -48,10 +47,7 @@ export type PaginationProps =
onPageChange(page: number): void; onPageChange(page: number): void;
}; };
export interface Props< export interface Props<D extends DefaultType> extends AutomationTestingProps {
D extends Record<string, unknown>,
TMeta extends TableMeta<D> = TableMeta<D>
> extends AutomationTestingProps {
dataset: D[]; dataset: D[];
columns: TableOptions<D>['columns']; columns: TableOptions<D>['columns'];
renderTableSettings?(instance: TableInstance<D>): ReactNode; renderTableSettings?(instance: TableInstance<D>): ReactNode;
@ -70,13 +66,10 @@ export interface Props<
renderRow?(row: Row<D>, highlightedItemId?: string): ReactNode; renderRow?(row: Row<D>, highlightedItemId?: string): ReactNode;
getRowCanExpand?(row: Row<D>): boolean; getRowCanExpand?(row: Row<D>): boolean;
noWidget?: boolean; noWidget?: boolean;
meta?: TMeta; extendTableOptions?: (options: TableOptions<D>) => TableOptions<D>;
} }
export function Datatable< export function Datatable<D extends DefaultType>({
D extends Record<string, unknown>,
TMeta extends TableMeta<D> = TableMeta<D>
>({
columns, columns,
dataset, dataset,
renderTableSettings = () => null, renderTableSettings = () => null,
@ -96,12 +89,12 @@ export function Datatable<
noWidget, noWidget,
getRowCanExpand, getRowCanExpand,
'data-cy': dataCy, 'data-cy': dataCy,
meta,
onPageChange = () => {}, onPageChange = () => {},
page, page,
totalCount = dataset.length, totalCount = dataset.length,
isServerSidePagination = false, isServerSidePagination = false,
}: Props<D, TMeta> & PaginationProps) { extendTableOptions = (value) => value,
}: Props<D> & PaginationProps) {
const pageCount = useMemo( const pageCount = useMemo(
() => Math.ceil(totalCount / settings.pageSize), () => Math.ceil(totalCount / settings.pageSize),
[settings.pageSize, totalCount] [settings.pageSize, totalCount]
@ -117,44 +110,48 @@ export function Datatable<
[disableSelect, columns] [disableSelect, columns]
); );
const tableInstance = useReactTable<D>({ const tableInstance = useReactTable<D>(
columns: allColumns, extendTableOptions({
data: dataset, columns: allColumns,
initialState: { data: dataset,
pagination: { initialState: {
pageSize: settings.pageSize, pagination: {
pageIndex: page || 0, pageSize: settings.pageSize,
}, pageIndex: page || 0,
sorting: settings.sortBy ? [settings.sortBy] : [], },
globalFilter: settings.search, sorting: settings.sortBy ? [settings.sortBy] : [],
globalFilter: {
search: settings.search,
...initialTableState.globalFilter,
},
...initialTableState, ...initialTableState,
}, },
defaultColumn: { defaultColumn: {
enableColumnFilter: false, enableColumnFilter: false,
enableHiding: true, enableHiding: true,
sortingFn: 'alphanumeric', sortingFn: 'alphanumeric',
}, },
enableRowSelection, enableRowSelection,
autoResetExpanded: false, autoResetExpanded: false,
globalFilterFn, globalFilterFn: defaultGlobalFilterFn,
getRowId, getRowId,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
getFacetedRowModel: getFacetedRowModel(), getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(), getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues(), getFacetedMinMaxValues: getFacetedMinMaxValues(),
getExpandedRowModel: getExpandedRowModel(), getExpandedRowModel: getExpandedRowModel(),
getRowCanExpand, getRowCanExpand,
getColumnCanGlobalFilter, getColumnCanGlobalFilter,
...(isServerSidePagination ...(isServerSidePagination
? { manualPagination: true, pageCount } ? { manualPagination: true, pageCount }
: { : {
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
}), }),
meta, })
}); );
const tableState = tableInstance.getState(); const tableState = tableInstance.getState();
@ -201,9 +198,9 @@ export function Datatable<
</Table.Container> </Table.Container>
); );
function handleSearchBarChange(value: string) { function handleSearchBarChange(search: string) {
tableInstance.setGlobalFilter(value); tableInstance.setGlobalFilter({ search });
settings.setSearch(value); settings.setSearch(search);
} }
function handlePageChange(page: number) { function handlePageChange(page: number) {
@ -221,7 +218,7 @@ export function Datatable<
} }
} }
function defaultRenderRow<D extends Record<string, unknown>>( function defaultRenderRow<D extends DefaultType>(
row: Row<D>, row: Row<D>,
highlightedItemId?: string highlightedItemId?: string
) { ) {
@ -235,7 +232,7 @@ function defaultRenderRow<D extends Record<string, unknown>>(
); );
} }
function getIsSelectionEnabled<D extends Record<string, unknown>>( function getIsSelectionEnabled<D extends DefaultType>(
disabledSelect?: boolean, disabledSelect?: boolean,
isRowSelectable?: Props<D>['isRowSelectable'] isRowSelectable?: Props<D>['isRowSelectable']
) { ) {
@ -250,14 +247,14 @@ function getIsSelectionEnabled<D extends Record<string, unknown>>(
return true; return true;
} }
function globalFilterFn<D>( export function defaultGlobalFilterFn<D, TFilter extends { search: string }>(
row: Row<D>, row: Row<D>,
columnId: string, columnId: string,
filterValue: null | string filterValue: null | TFilter
): boolean { ): boolean {
const value = row.getValue(columnId); const value = row.getValue(columnId);
if (filterValue === null || filterValue === '') { if (filterValue === null || !filterValue.search) {
return true; return true;
} }
@ -265,7 +262,7 @@ function globalFilterFn<D>(
return false; return false;
} }
const filterValueLower = filterValue.toLowerCase(); const filterValueLower = filterValue.search.toLowerCase();
if ( if (
typeof value === 'string' || typeof value === 'string' ||

View File

@ -3,9 +3,9 @@ import { Row, Table as TableInstance } from '@tanstack/react-table';
import { AutomationTestingProps } from '@/types'; import { AutomationTestingProps } from '@/types';
import { Table } from './Table'; import { Table } from './Table';
import { DefaultType } from './types';
interface Props<D extends Record<string, unknown>> interface Props<D extends DefaultType> extends AutomationTestingProps {
extends AutomationTestingProps {
tableInstance: TableInstance<D>; tableInstance: TableInstance<D>;
renderRow(row: Row<D>): React.ReactNode; renderRow(row: Row<D>): React.ReactNode;
onSortChange?(colId: string, desc: boolean): void; onSortChange?(colId: string, desc: boolean): void;
@ -13,7 +13,7 @@ interface Props<D extends Record<string, unknown>>
emptyContentLabel?: string; emptyContentLabel?: string;
} }
export function DatatableContent<D extends Record<string, unknown>>({ export function DatatableContent<D extends DefaultType>({
tableInstance, tableInstance,
renderRow, renderRow,
onSortChange, onSortChange,

View File

@ -7,14 +7,15 @@ import {
Props as DatatableProps, Props as DatatableProps,
PaginationProps, PaginationProps,
} from './Datatable'; } from './Datatable';
import { DefaultType } from './types';
interface Props<D extends Record<string, unknown>> interface Props<D extends DefaultType>
extends Omit<DatatableProps<D>, 'renderRow' | 'expandable'> { extends Omit<DatatableProps<D>, 'renderRow' | 'expandable'> {
renderSubRow(row: Row<D>): ReactNode; renderSubRow(row: Row<D>): ReactNode;
expandOnRowClick?: boolean; expandOnRowClick?: boolean;
} }
export function ExpandableDatatable<D extends Record<string, unknown>>({ export function ExpandableDatatable<D extends DefaultType>({
renderSubRow, renderSubRow,
getRowCanExpand = () => true, getRowCanExpand = () => true,
expandOnRowClick, expandOnRowClick,

View File

@ -2,15 +2,16 @@ import { ReactNode } from 'react';
import { Row } from '@tanstack/react-table'; import { Row } from '@tanstack/react-table';
import { TableRow } from './TableRow'; import { TableRow } from './TableRow';
import { DefaultType } from './types';
interface Props<D extends Record<string, unknown>> { interface Props<D extends DefaultType> {
row: Row<D>; row: Row<D>;
disableSelect?: boolean; disableSelect?: boolean;
renderSubRow(row: Row<D>): ReactNode; renderSubRow(row: Row<D>): ReactNode;
expandOnClick?: boolean; expandOnClick?: boolean;
} }
export function ExpandableDatatableTableRow<D extends Record<string, unknown>>({ export function ExpandableDatatableTableRow<D extends DefaultType>({
row, row,
disableSelect, disableSelect,
renderSubRow, renderSubRow,

View File

@ -8,8 +8,10 @@ import { getValueAsArrayOfStrings } from '@/portainer/helpers/array';
import { Icon } from '@@/Icon'; import { Icon } from '@@/Icon';
import { DefaultType } from './types';
interface MultipleSelectionFilterProps { interface MultipleSelectionFilterProps {
options: string[]; options: Array<string> | ReadonlyArray<string>;
value: string[]; value: string[];
filterKey: string; filterKey: string;
onChange: (value: string[]) => void; onChange: (value: string[]) => void;
@ -28,12 +30,12 @@ export function MultipleSelectionFilter({
<div> <div>
<Menu> <Menu>
<MenuButton <MenuButton
className={clsx('table-filter', { 'filter-active': enabled })} className={clsx('table-filter flex items-center gap-1', {
'filter-active': enabled,
})}
> >
<div className="flex items-center gap-1"> Filter
Filter <Icon icon={enabled ? Check : Filter} />
<Icon icon={enabled ? Check : Filter} />
</div>
</MenuButton> </MenuButton>
<MenuPopover className="dropdown-menu"> <MenuPopover className="dropdown-menu">
<div className="tableMenu"> <div className="tableMenu">
@ -70,9 +72,7 @@ export function MultipleSelectionFilter({
} }
} }
export function filterHOC<TData extends Record<string, unknown>>( export function filterHOC<TData extends DefaultType>(menuTitle: string) {
menuTitle: string
) {
return function Filter({ return function Filter({
column: { getFilterValue, setFilterValue, getFacetedRowModel, id }, column: { getFilterValue, setFilterValue, getFacetedRowModel, id },
}: { }: {

View File

@ -12,9 +12,9 @@ import { defaultGetRowId } from './defaultGetRowId';
import { Table } from './Table'; import { Table } from './Table';
import { NestedTable } from './NestedTable'; import { NestedTable } from './NestedTable';
import { DatatableContent } from './DatatableContent'; import { DatatableContent } from './DatatableContent';
import { BasicTableSettings } from './types'; import { BasicTableSettings, DefaultType } from './types';
interface Props<D extends Record<string, unknown>> { interface Props<D extends DefaultType> {
dataset: D[]; dataset: D[];
columns: TableOptions<D>['columns']; columns: TableOptions<D>['columns'];
@ -25,7 +25,7 @@ interface Props<D extends Record<string, unknown>> {
initialSortBy?: BasicTableSettings['sortBy']; initialSortBy?: BasicTableSettings['sortBy'];
} }
export function NestedDatatable<D extends Record<string, unknown>>({ export function NestedDatatable<D extends DefaultType>({
columns, columns,
dataset, dataset,
getRowId = defaultGetRowId, getRowId = defaultGetRowId,

View File

@ -1,16 +1,16 @@
import { Fragment, PropsWithChildren } from 'react'; import { Fragment, PropsWithChildren } from 'react';
import { Row } from '@tanstack/react-table'; import { Row } from '@tanstack/react-table';
interface Props<T extends Record<string, unknown> = Record<string, unknown>> { import { DefaultType } from './types';
interface Props<T extends DefaultType = DefaultType> {
isLoading?: boolean; isLoading?: boolean;
rows: Row<T>[]; rows: Row<T>[];
emptyContent?: string; emptyContent?: string;
renderRow(row: Row<T>): React.ReactNode; renderRow(row: Row<T>): React.ReactNode;
} }
export function TableContent< export function TableContent<T extends DefaultType = DefaultType>({
T extends Record<string, unknown> = Record<string, unknown>
>({
isLoading = false, isLoading = false,
rows, rows,
emptyContent = 'No items available', emptyContent = 'No items available',

View File

@ -2,15 +2,17 @@ import { Header, flexRender } from '@tanstack/react-table';
import { filterHOC } from './Filter'; import { filterHOC } from './Filter';
import { TableHeaderCell } from './TableHeaderCell'; import { TableHeaderCell } from './TableHeaderCell';
import { DefaultType } from './types';
interface Props<D extends Record<string, unknown> = Record<string, unknown>> { interface Props<D extends DefaultType = DefaultType> {
headers: Header<D, unknown>[]; headers: Header<D, unknown>[];
onSortChange?(colId: string, desc: boolean): void; onSortChange?(colId: string, desc: boolean): void;
} }
export function TableHeaderRow< export function TableHeaderRow<D extends DefaultType = DefaultType>({
D extends Record<string, unknown> = Record<string, unknown> headers,
>({ headers, onSortChange }: Props<D>) { onSortChange,
}: Props<D>) {
return ( return (
<tr> <tr>
{headers.map((header) => { {headers.map((header) => {

View File

@ -1,15 +1,19 @@
import { Cell, flexRender } from '@tanstack/react-table'; import { Cell, flexRender } from '@tanstack/react-table';
import clsx from 'clsx'; import clsx from 'clsx';
interface Props<D extends Record<string, unknown> = Record<string, unknown>> { import { DefaultType } from './types';
interface Props<D extends DefaultType = DefaultType> {
cells: Cell<D, unknown>[]; cells: Cell<D, unknown>[];
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
} }
export function TableRow< export function TableRow<D extends DefaultType = DefaultType>({
D extends Record<string, unknown> = Record<string, unknown> cells,
>({ cells, className, onClick }: Props<D>) { className,
onClick,
}: Props<D>) {
return ( return (
<tr <tr
className={clsx(className, { 'cursor-pointer': !!onClick })} className={clsx(className, { 'cursor-pointer': !!onClick })}

View File

@ -2,13 +2,16 @@ import { ColumnDef, CellContext } from '@tanstack/react-table';
import { Link } from '@@/Link'; import { Link } from '@@/Link';
export function buildNameColumn<T extends Record<string, unknown>>( import { DefaultType } from './types';
import { defaultGetRowId } from './defaultGetRowId';
export function buildNameColumn<T extends DefaultType>(
nameKey: keyof T, nameKey: keyof T,
idKey: string,
path: string, path: string,
idParam = 'id' idParam = 'id',
idGetter: (row: T) => string = defaultGetRowId<T>
): ColumnDef<T> { ): ColumnDef<T> {
const cell = createCell<T>(); const cell = createCell();
return { return {
header: 'Name', header: 'Name',
@ -19,7 +22,7 @@ export function buildNameColumn<T extends Record<string, unknown>>(
enableHiding: false, enableHiding: false,
}; };
function createCell<T extends Record<string, unknown>>() { function createCell() {
return function NameCell({ renderValue, row }: CellContext<T, unknown>) { return function NameCell({ renderValue, row }: CellContext<T, unknown>) {
const name = renderValue() || ''; const name = renderValue() || '';
@ -30,7 +33,7 @@ export function buildNameColumn<T extends Record<string, unknown>>(
return ( return (
<Link <Link
to={path} to={path}
params={{ [idParam]: row.original[idKey] }} params={{ [idParam]: idGetter(row.original) }}
title={name} title={name}
> >
{name} {name}

View File

@ -1,16 +1,17 @@
export function defaultGetRowId<D extends Record<string, unknown>>( import { DefaultType } from './types';
row: D
): string {
if (row.id && (typeof row.id === 'string' || typeof row.id === 'number')) {
return row.id.toString();
}
if (row.Id && (typeof row.Id === 'string' || typeof row.Id === 'number')) { /**
return row.Id.toString(); * gets row id by looking for one of id, Id, or ID keys on the object
} */
export function defaultGetRowId<D extends DefaultType>(row: D): string {
const key = ['id', 'Id', 'ID'].find((key) =>
Object.hasOwn(row, key)
) as keyof D;
if (row.ID && (typeof row.ID === 'string' || typeof row.ID === 'number')) { const value = row[key];
return row.ID.toString();
if (typeof value === 'string' || typeof value === 'number') {
return value.toString();
} }
return ''; return '';

View File

@ -3,9 +3,9 @@ import { ColumnDef } from '@tanstack/react-table';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
export function buildExpandColumn< import { DefaultType } from './types';
T extends Record<string, unknown>
>(): ColumnDef<T> { export function buildExpandColumn<T extends DefaultType>(): ColumnDef<T> {
return { return {
id: 'expand', id: 'expand',
header: ({ table }) => { header: ({ table }) => {

View File

@ -0,0 +1,10 @@
import { TableOptions } from '@tanstack/react-table';
type OptionExtender<T> = (options: TableOptions<T>) => TableOptions<T>;
export function mergeOptions<T>(
...extenders: Array<OptionExtender<T>>
): OptionExtender<T> {
return (options: TableOptions<T>) =>
extenders.reduce((acc, option) => option(acc), options);
}

View File

@ -0,0 +1,37 @@
import {
RowSelectionState,
TableOptions,
Updater,
} from '@tanstack/react-table';
import { DefaultType } from '../types';
export function withControlledSelected<D extends DefaultType>(
onChange?: (value: string[]) => void,
value?: string[]
) {
return function extendTableOptions(options: TableOptions<D>) {
if (!onChange || !value) {
return options;
}
return {
...options,
state: {
...options.state,
rowSelection: Object.fromEntries(value.map((i) => [i, true])),
},
onRowSelectionChange(updater: Updater<RowSelectionState>) {
const newValue =
typeof updater !== 'function'
? updater
: updater(Object.fromEntries(value.map((i) => [i, true])));
onChange(
Object.entries(newValue)
.filter(([, selected]) => selected)
.map(([id]) => id)
);
},
};
};
}

View File

@ -0,0 +1,15 @@
import { TableOptions } from '@tanstack/react-table';
import { defaultGlobalFilterFn } from '../Datatable';
import { DefaultType } from '../types';
export function withGlobalFilter<D extends DefaultType>(
filterFn: typeof defaultGlobalFilterFn
) {
return function extendOptions(options: TableOptions<D>) {
return {
...options,
globalFilterFn: filterFn,
};
};
}

View File

@ -0,0 +1,15 @@
import { TableOptions } from '@tanstack/react-table';
import { DefaultType } from '../types';
export function withMeta<D extends DefaultType>(meta: Record<string, unknown>) {
return function extendOptions(options: TableOptions<D>) {
return {
...options,
meta: {
...options.meta,
...meta,
},
};
};
}

View File

@ -1,8 +1,12 @@
import { Row } from '@tanstack/react-table'; import { Row } from '@tanstack/react-table';
export function multiple< import { DefaultType } from './types';
D extends Record<string, unknown> = Record<string, unknown>
>({ getValue }: Row<D>, columnId: string, filterValue: string[]): boolean { export function multiple<D extends DefaultType = DefaultType>(
{ getValue }: Row<D>,
columnId: string,
filterValue: string[]
): boolean {
if (filterValue.length === 0) { if (filterValue.length === 0) {
return true; return true;
} }

View File

@ -3,6 +3,8 @@ import { persist } from 'zustand/middleware';
import { keyBuilder } from '@/react/hooks/useLocalStorage'; import { keyBuilder } from '@/react/hooks/useLocalStorage';
export type DefaultType = object;
export interface PaginationTableSettings { export interface PaginationTableSettings {
pageSize: number; pageSize: number;
setPageSize: (pageSize: number) => void; setPageSize: (pageSize: number) => void;
@ -23,21 +25,24 @@ export function paginationSettings<T extends PaginationTableSettings>(
} }
export interface SortableTableSettings { export interface SortableTableSettings {
sortBy: { id: string; desc: boolean }; sortBy: { id: string; desc: boolean } | undefined;
setSortBy: (id: string, desc: boolean) => void; setSortBy: (id: string | undefined, desc: boolean) => void;
} }
export function sortableSettings<T extends SortableTableSettings>( export function sortableSettings<T extends SortableTableSettings>(
set: ZustandSetFunc<T>, set: ZustandSetFunc<T>,
initialSortBy: string | { id: string; desc: boolean } initialSortBy?: string | { id: string; desc: boolean }
): SortableTableSettings { ): SortableTableSettings {
return { return {
sortBy: sortBy:
typeof initialSortBy === 'string' typeof initialSortBy === 'string'
? { id: initialSortBy, desc: false } ? { id: initialSortBy, desc: false }
: initialSortBy, : initialSortBy,
setSortBy: (id: string, desc: boolean) => setSortBy: (id: string | undefined, desc: boolean) =>
set((s) => ({ ...s, sortBy: { id, desc } })), set((s) => ({
...s,
sortBy: typeof id === 'string' ? { id, desc } : id,
})),
}; };
} }
@ -77,7 +82,7 @@ export interface BasicTableSettings
export function createPersistedStore<T extends BasicTableSettings>( export function createPersistedStore<T extends BasicTableSettings>(
storageKey: string, storageKey: string,
initialSortBy: string | { id: string; desc: boolean } = 'name', initialSortBy?: string | { id: string; desc: boolean },
create: (set: ZustandSetFunc<T>) => Omit<T, keyof BasicTableSettings> = () => create: (set: ZustandSetFunc<T>) => Omit<T, keyof BasicTableSettings> = () =>
({} as T) ({} as T)
) { ) {
@ -85,11 +90,8 @@ export function createPersistedStore<T extends BasicTableSettings>(
persist( persist(
(set) => (set) =>
({ ({
...sortableSettings( ...sortableSettings<T>(set, initialSortBy),
set as ZustandSetFunc<SortableTableSettings>, ...paginationSettings<T>(set),
initialSortBy
),
...paginationSettings(set as ZustandSetFunc<PaginationTableSettings>),
...create(set), ...create(set),
} as T), } as T),
{ {
@ -98,18 +100,3 @@ export function createPersistedStore<T extends BasicTableSettings>(
) )
); );
} }
/** this class is just a dummy class to get return type of createPersistedStore
* can be fixed after upgrade to ts 4.7+
* https://stackoverflow.com/a/64919133
*/
class Wrapper<T extends BasicTableSettings> {
// eslint-disable-next-line class-methods-use-this
wrapped() {
return createPersistedStore<T>('', '');
}
}
export type CreatePersistedStoreReturn<
T extends BasicTableSettings = BasicTableSettings
> = ReturnType<Wrapper<T>['wrapped']>;

View File

@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { useStore } from 'zustand'; import { useStore } from 'zustand';
import { useSearchBarState } from './SearchBar'; import { useSearchBarState } from './SearchBar';
import { BasicTableSettings, CreatePersistedStoreReturn } from './types'; import { BasicTableSettings, createPersistedStore } from './types';
export type TableState<TSettings extends BasicTableSettings> = TSettings & { export type TableState<TSettings extends BasicTableSettings> = TSettings & {
setSearch: (search: string) => void; setSearch: (search: string) => void;
@ -11,7 +11,10 @@ export type TableState<TSettings extends BasicTableSettings> = TSettings & {
export function useTableState< export function useTableState<
TSettings extends BasicTableSettings = BasicTableSettings TSettings extends BasicTableSettings = BasicTableSettings
>(store: CreatePersistedStoreReturn<TSettings>, storageKey: string) { >(
store: ReturnType<typeof createPersistedStore<TSettings>>,
storageKey: string
) {
const settings = useStore(store); const settings = useStore(store);
const [search, setSearch] = useSearchBarState(storageKey); const [search, setSearch] = useSearchBarState(storageKey);
@ -23,21 +26,24 @@ export function useTableState<
} }
export function useTableStateWithoutStorage( export function useTableStateWithoutStorage(
defaultSortKey: string defaultSortKey?: string
): BasicTableSettings & { ): BasicTableSettings & {
setSearch: (search: string) => void; setSearch: (search: string) => void;
search: string; search: string;
} { } {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [sortBy, setSortBy] = useState({ id: defaultSortKey, desc: false }); const [sortBy, setSortBy] = useState(
defaultSortKey ? { id: defaultSortKey, desc: false } : undefined
);
return { return {
search, search,
setSearch, setSearch,
pageSize, pageSize,
setPageSize, setPageSize,
setSortBy: (id: string, desc: boolean) => setSortBy({ id, desc }), setSortBy: (id: string | undefined, desc: boolean) =>
setSortBy(id ? { id, desc } : undefined),
sortBy, sortBy,
}; };
} }

View File

@ -7,8 +7,9 @@ import { Datatable } from '@@/datatables';
import { BasicTableSettings } from '@@/datatables/types'; import { BasicTableSettings } from '@@/datatables/types';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
import { TableState } from '@@/datatables/useTableState'; import { TableState } from '@@/datatables/useTableState';
import { withMeta } from '@@/datatables/extend-options/withMeta';
import { FileData, FilesTableMeta } from './types'; import { FileData } from './types';
import { columns } from './columns'; import { columns } from './columns';
interface Props { interface Props {
@ -72,14 +73,19 @@ export function FilesTable({
} }
return ( return (
<Datatable<FileData, FilesTableMeta> <Datatable<FileData>
title={title} title={title}
titleIcon={FileIcon} titleIcon={FileIcon}
dataset={isRoot ? dataset : [goToParent(onGoToParent), ...dataset]} dataset={isRoot ? dataset : [goToParent(onGoToParent), ...dataset]}
settingsManager={tableState} settingsManager={tableState}
columns={columns} columns={columns}
getRowId={(row) => row.Name} getRowId={(row) => row.Name}
meta={{ initialTableState={{
columnVisibility: {
Dir: false,
},
}}
extendTableOptions={withMeta({
table: 'files', table: 'files',
isEdit, isEdit,
setIsEdit, setIsEdit,
@ -87,12 +93,7 @@ export function FilesTable({
onBrowse, onBrowse,
onDownload, onDownload,
onDelete, onDelete,
}} })}
initialTableState={{
columnVisibility: {
Dir: false,
},
}}
disableSelect disableSelect
renderTableActions={() => { renderTableActions={() => {
if (!isUploadAllowed) { if (!isUploadAllowed) {

View File

@ -3,14 +3,14 @@ import { createColumnHelper } from '@tanstack/react-table';
import { isoDate } from '@/portainer/filters/filters'; import { isoDate } from '@/portainer/filters/filters';
import { createOwnershipColumn } from '@/react/docker/components/datatable-helpers/createOwnershipColumn'; import { createOwnershipColumn } from '@/react/docker/components/datatable-helpers/createOwnershipColumn';
import { buildNameColumn } from '@@/datatables/NameCell'; import { buildNameColumn } from '@@/datatables/buildNameColumn';
import { DockerConfig } from '../../types'; import { DockerConfig } from '../../types';
const columnHelper = createColumnHelper<DockerConfig>(); const columnHelper = createColumnHelper<DockerConfig>();
export const columns = [ export const columns = [
buildNameColumn<DockerConfig>('Name', 'Id', 'docker.configs.config'), buildNameColumn<DockerConfig>('Name', 'docker.configs.config'),
columnHelper.accessor('CreatedAt', { columnHelper.accessor('CreatedAt', {
header: 'Creation Date', header: 'Creation Date',
cell: ({ getValue }) => { cell: ({ getValue }) => {

View File

@ -56,8 +56,8 @@ export function EdgeGroupAssociationTable({
pageLimit: tableState.pageSize, pageLimit: tableState.pageSize,
page: page + 1, page: page + 1,
search: tableState.search, search: tableState.search,
sort: tableState.sortBy.id as 'Group' | 'Name', sort: tableState.sortBy?.id as 'Group' | 'Name',
order: tableState.sortBy.desc ? 'desc' : 'asc', order: tableState.sortBy?.desc ? 'desc' : 'asc',
...query, ...query,
}); });
const groupsQuery = useGroups({ const groupsQuery = useGroups({

View File

@ -9,7 +9,7 @@ import { useEnvironments } from './useEnvironments';
const storageKey = 'edge-devices-waiting-room'; const storageKey = 'edge-devices-waiting-room';
const settingsStore = createPersistedStore(storageKey); const settingsStore = createPersistedStore(storageKey, 'name');
export function Datatable() { export function Datatable() {
const tableState = useTableState(settingsStore, storageKey); const tableState = useTableState(settingsStore, storageKey);

View File

@ -44,8 +44,8 @@ export function EnvironmentsDatatable() {
pageLimit: tableState.pageSize, pageLimit: tableState.pageSize,
page: page + 1, page: page + 1,
search: tableState.search, search: tableState.search,
sort: tableState.sortBy.id as 'Group' | 'Name', sort: tableState.sortBy?.id as 'Group' | 'Name',
order: tableState.sortBy.desc ? 'desc' : 'asc', order: tableState.sortBy?.desc ? 'desc' : 'asc',
edgeStackId: stackId, edgeStackId: stackId,
edgeStackStatus: statusFilter, edgeStackStatus: statusFilter,
}); });

View File

@ -4,7 +4,7 @@ import _ from 'lodash';
import { isoDateFromTimestamp } from '@/portainer/filters/filters'; import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { isBE } from '@/react/portainer/feature-flags/feature-flags.service'; import { isBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { buildNameColumn } from '@@/datatables/NameCell'; import { buildNameColumn } from '@@/datatables/buildNameColumn';
import { Link } from '@@/Link'; import { Link } from '@@/Link';
import { StatusType } from '../../types'; import { StatusType } from '../../types';
@ -16,12 +16,7 @@ import { DeploymentCounter } from './DeploymentCounter';
const columnHelper = createColumnHelper<DecoratedEdgeStack>(); const columnHelper = createColumnHelper<DecoratedEdgeStack>();
export const columns = _.compact([ export const columns = _.compact([
buildNameColumn<DecoratedEdgeStack>( buildNameColumn<DecoratedEdgeStack>('Name', 'edge.stacks.edit', 'stackId'),
'Name',
'Id',
'edge.stacks.edit',
'stackId'
),
columnHelper.accessor( columnHelper.accessor(
(item) => item.aggregatedStatus[StatusType.Acknowledged] || 0, (item) => item.aggregatedStatus[StatusType.Acknowledged] || 0,
{ {

View File

@ -15,7 +15,7 @@ import { IngressControllerClassMap } from '../types';
import { columns } from './columns'; import { columns } from './columns';
const storageKey = 'ingressClasses'; const storageKey = 'ingressClasses';
const settingsStore = createPersistedStore(storageKey); const settingsStore = createPersistedStore(storageKey, 'name');
interface Props { interface Props {
onChangeControllers: ( onChangeControllers: (

View File

@ -37,8 +37,10 @@ export function EnvironmentsDatatable({
excludeSnapshots: true, excludeSnapshots: true,
page: page + 1, page: page + 1,
pageLimit: tableState.pageSize, pageLimit: tableState.pageSize,
sort: isSortType(tableState.sortBy.id) ? tableState.sortBy.id : undefined, sort: isSortType(tableState.sortBy?.id)
order: tableState.sortBy.desc ? 'desc' : 'asc', ? tableState.sortBy?.id
: undefined,
order: tableState.sortBy?.desc ? 'desc' : 'asc',
}, },
{ enabled: groupsQuery.isSuccess, refetchInterval: 30 * 1000 } { enabled: groupsQuery.isSuccess, refetchInterval: 30 * 1000 }
); );

View File

@ -38,8 +38,8 @@ export function GroupAssociationTable({
pageLimit: tableState.pageSize, pageLimit: tableState.pageSize,
page: page + 1, page: page + 1,
search: tableState.search, search: tableState.search,
sort: tableState.sortBy.id as 'Name', sort: tableState.sortBy?.id as 'Name',
order: tableState.sortBy.desc ? 'desc' : 'asc', order: tableState.sortBy?.desc ? 'desc' : 'asc',
...query, ...query,
}); });

View File

@ -14,7 +14,7 @@ export const ENVIRONMENTS_POLLING_INTERVAL = 30000; // in ms
export const SortOptions = ['Name', 'Group', 'Status'] as const; export const SortOptions = ['Name', 'Group', 'Status'] as const;
export type SortType = (typeof SortOptions)[number]; export type SortType = (typeof SortOptions)[number];
export function isSortType(value: string): value is SortType { export function isSortType(value?: string): value is SortType {
return SortOptions.includes(value as SortType); return SortOptions.includes(value as SortType);
} }

View File

@ -1,4 +1,4 @@
import { buildNameColumn } from '@@/datatables/NameCell'; import { buildNameColumn } from '@@/datatables/buildNameColumn';
import { EdgeUpdateListItemResponse } from '../../queries/list'; import { EdgeUpdateListItemResponse } from '../../queries/list';
@ -9,7 +9,7 @@ import { scheduledTime } from './scheduled-time';
import { scheduleType } from './type'; import { scheduleType } from './type';
export const columns = [ export const columns = [
buildNameColumn<EdgeUpdateListItemResponse>('name', 'id', '.item'), buildNameColumn<EdgeUpdateListItemResponse>('name', '.item'),
scheduledTime, scheduledTime,
groups, groups,
scheduleType, scheduleType,

View File

@ -1,10 +1,10 @@
import { Profile } from '@/portainer/hostmanagement/fdo/model'; import { Profile } from '@/portainer/hostmanagement/fdo/model';
import { buildNameColumn } from '@@/datatables/NameCell'; import { buildNameColumn } from '@@/datatables/buildNameColumn';
import { created } from './created'; import { created } from './created';
export const columns = [ export const columns = [
buildNameColumn<Profile>('name', 'id', 'portainer.endpoints.profile.edit'), buildNameColumn<Profile>('name', 'portainer.endpoints.profile.edit'),
created, created,
]; ];

View File

@ -33,7 +33,9 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const [sortBy, setSortBy] = useState({ id: 'name', desc: false }); const [sortBy, setSortBy] = useState<
{ id: string; desc: boolean } | undefined
>({ id: 'name', desc: false });
const { isAdmin } = useUser(); const { isAdmin } = useUser();
@ -79,8 +81,8 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
</RowProvider> </RowProvider>
); );
function handleSetSort(colId: string, desc: boolean) { function handleSetSort(colId: string | undefined, desc: boolean) {
setSortBy({ id: colId, desc }); setSortBy(colId ? { id: colId, desc } : undefined);
} }
function handleRemoveMembers(userIds: UserId[]) { function handleRemoveMembers(userIds: UserId[]) {

View File

@ -25,7 +25,9 @@ export function UsersList({ users, disabled, teamId }: Props) {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [pageSize, setPageSize] = useState(10); const [pageSize, setPageSize] = useState(10);
const addMemberMutation = useAddMemberMutation(teamId); const addMemberMutation = useAddMemberMutation(teamId);
const [sortBy, setSortBy] = useState({ id: 'name', desc: false }); const [sortBy, setSortBy] = useState<
{ id: string; desc: boolean } | undefined
>({ id: 'name', desc: false });
const { isAdmin } = useUser(); const { isAdmin } = useUser();
@ -62,8 +64,8 @@ export function UsersList({ users, disabled, teamId }: Props) {
</RowProvider> </RowProvider>
); );
function handleSetSort(colId: string, desc: boolean) { function handleSetSort(colId: string | undefined, desc: boolean) {
setSortBy({ id: colId, desc }); setSortBy(colId ? { id: colId, desc } : undefined);
} }
function handleAddAllMembers(userIds: UserId[]) { function handleAddAllMembers(userIds: UserId[]) {

View File

@ -10,14 +10,14 @@ import { deleteTeam } from '@/react/portainer/users/teams/teams.service';
import { confirmDelete } from '@@/modals/confirm'; import { confirmDelete } from '@@/modals/confirm';
import { Datatable } from '@@/datatables'; import { Datatable } from '@@/datatables';
import { Button } from '@@/buttons'; import { Button } from '@@/buttons';
import { buildNameColumn } from '@@/datatables/NameCell'; import { buildNameColumn } from '@@/datatables/buildNameColumn';
import { createPersistedStore } from '@@/datatables/types'; import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState'; import { useTableState } from '@@/datatables/useTableState';
const storageKey = 'teams'; const storageKey = 'teams';
const columns: ColumnDef<Team>[] = [ const columns: ColumnDef<Team>[] = [
buildNameColumn<Team>('Name', 'Id', 'portainer.teams.team'), buildNameColumn<Team>('Name', 'portainer.teams.team'),
]; ];
interface Props { interface Props {
@ -25,7 +25,7 @@ interface Props {
isAdmin: boolean; isAdmin: boolean;
} }
const settingsStore = createPersistedStore(storageKey); const settingsStore = createPersistedStore(storageKey, 'name');
export function TeamsDatatable({ teams, isAdmin }: Props) { export function TeamsDatatable({ teams, isAdmin }: Props) {
const { handleRemove } = useRemoveMutation(); const { handleRemove } = useRemoveMutation();