mirror of https://github.com/portainer/portainer
refactor(ui/datatables): allow datatable to globally filter on object value [EE-5824] (#9955)
parent
440f4e8dda
commit
cb7377ead6
|
@ -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' ||
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 },
|
||||||
}: {
|
}: {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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 })}
|
||||||
|
|
|
@ -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}
|
|
@ -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 '';
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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']>;
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
|
@ -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,
|
||||||
{
|
{
|
||||||
|
|
|
@ -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: (
|
||||||
|
|
|
@ -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 }
|
||||||
);
|
);
|
||||||
|
|
|
@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
];
|
];
|
||||||
|
|
|
@ -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[]) {
|
||||||
|
|
|
@ -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[]) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
Loading…
Reference in New Issue