diff --git a/app/react/components/datatables/Datatable.tsx b/app/react/components/datatables/Datatable.tsx index 8f5c6d178..f0c7383b6 100644 --- a/app/react/components/datatables/Datatable.tsx +++ b/app/react/components/datatables/Datatable.tsx @@ -13,7 +13,6 @@ import { getFacetedMinMaxValues, getExpandedRowModel, TableOptions, - TableMeta, } from '@tanstack/react-table'; import { ReactNode, useMemo } from 'react'; import clsx from 'clsx'; @@ -28,7 +27,7 @@ import { DatatableFooter } from './DatatableFooter'; import { defaultGetRowId } from './defaultGetRowId'; import { Table } from './Table'; import { useGoToHighlightedRow } from './useGoToHighlightedRow'; -import { BasicTableSettings } from './types'; +import { BasicTableSettings, DefaultType } from './types'; import { DatatableContent } from './DatatableContent'; import { createSelectColumn } from './select-column'; import { TableRow } from './TableRow'; @@ -48,10 +47,7 @@ export type PaginationProps = onPageChange(page: number): void; }; -export interface Props< - D extends Record, - TMeta extends TableMeta = TableMeta -> extends AutomationTestingProps { +export interface Props extends AutomationTestingProps { dataset: D[]; columns: TableOptions['columns']; renderTableSettings?(instance: TableInstance): ReactNode; @@ -70,13 +66,10 @@ export interface Props< renderRow?(row: Row, highlightedItemId?: string): ReactNode; getRowCanExpand?(row: Row): boolean; noWidget?: boolean; - meta?: TMeta; + extendTableOptions?: (options: TableOptions) => TableOptions; } -export function Datatable< - D extends Record, - TMeta extends TableMeta = TableMeta ->({ +export function Datatable({ columns, dataset, renderTableSettings = () => null, @@ -96,12 +89,12 @@ export function Datatable< noWidget, getRowCanExpand, 'data-cy': dataCy, - meta, onPageChange = () => {}, page, totalCount = dataset.length, isServerSidePagination = false, -}: Props & PaginationProps) { + extendTableOptions = (value) => value, +}: Props & PaginationProps) { const pageCount = useMemo( () => Math.ceil(totalCount / settings.pageSize), [settings.pageSize, totalCount] @@ -117,44 +110,48 @@ export function Datatable< [disableSelect, columns] ); - const tableInstance = useReactTable({ - columns: allColumns, - data: dataset, - initialState: { - pagination: { - pageSize: settings.pageSize, - pageIndex: page || 0, + const tableInstance = useReactTable( + extendTableOptions({ + columns: allColumns, + data: dataset, + initialState: { + pagination: { + pageSize: settings.pageSize, + pageIndex: page || 0, + }, + sorting: settings.sortBy ? [settings.sortBy] : [], + globalFilter: { + search: settings.search, + ...initialTableState.globalFilter, + }, + + ...initialTableState, }, - sorting: settings.sortBy ? [settings.sortBy] : [], - globalFilter: settings.search, - - ...initialTableState, - }, - defaultColumn: { - enableColumnFilter: false, - enableHiding: true, - sortingFn: 'alphanumeric', - }, - enableRowSelection, - autoResetExpanded: false, - globalFilterFn, - getRowId, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getPaginationRowModel: getPaginationRowModel(), - getFacetedRowModel: getFacetedRowModel(), - getFacetedUniqueValues: getFacetedUniqueValues(), - getFacetedMinMaxValues: getFacetedMinMaxValues(), - getExpandedRowModel: getExpandedRowModel(), - getRowCanExpand, - getColumnCanGlobalFilter, - ...(isServerSidePagination - ? { manualPagination: true, pageCount } - : { - getSortedRowModel: getSortedRowModel(), - }), - meta, - }); + defaultColumn: { + enableColumnFilter: false, + enableHiding: true, + sortingFn: 'alphanumeric', + }, + enableRowSelection, + autoResetExpanded: false, + globalFilterFn: defaultGlobalFilterFn, + getRowId, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getFacetedRowModel: getFacetedRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + getFacetedMinMaxValues: getFacetedMinMaxValues(), + getExpandedRowModel: getExpandedRowModel(), + getRowCanExpand, + getColumnCanGlobalFilter, + ...(isServerSidePagination + ? { manualPagination: true, pageCount } + : { + getSortedRowModel: getSortedRowModel(), + }), + }) + ); const tableState = tableInstance.getState(); @@ -201,9 +198,9 @@ export function Datatable< ); - function handleSearchBarChange(value: string) { - tableInstance.setGlobalFilter(value); - settings.setSearch(value); + function handleSearchBarChange(search: string) { + tableInstance.setGlobalFilter({ search }); + settings.setSearch(search); } function handlePageChange(page: number) { @@ -221,7 +218,7 @@ export function Datatable< } } -function defaultRenderRow>( +function defaultRenderRow( row: Row, highlightedItemId?: string ) { @@ -235,7 +232,7 @@ function defaultRenderRow>( ); } -function getIsSelectionEnabled>( +function getIsSelectionEnabled( disabledSelect?: boolean, isRowSelectable?: Props['isRowSelectable'] ) { @@ -250,14 +247,14 @@ function getIsSelectionEnabled>( return true; } -function globalFilterFn( +export function defaultGlobalFilterFn( row: Row, columnId: string, - filterValue: null | string + filterValue: null | TFilter ): boolean { const value = row.getValue(columnId); - if (filterValue === null || filterValue === '') { + if (filterValue === null || !filterValue.search) { return true; } @@ -265,7 +262,7 @@ function globalFilterFn( return false; } - const filterValueLower = filterValue.toLowerCase(); + const filterValueLower = filterValue.search.toLowerCase(); if ( typeof value === 'string' || diff --git a/app/react/components/datatables/DatatableContent.tsx b/app/react/components/datatables/DatatableContent.tsx index d435a919a..605f1f4b2 100644 --- a/app/react/components/datatables/DatatableContent.tsx +++ b/app/react/components/datatables/DatatableContent.tsx @@ -3,9 +3,9 @@ import { Row, Table as TableInstance } from '@tanstack/react-table'; import { AutomationTestingProps } from '@/types'; import { Table } from './Table'; +import { DefaultType } from './types'; -interface Props> - extends AutomationTestingProps { +interface Props extends AutomationTestingProps { tableInstance: TableInstance; renderRow(row: Row): React.ReactNode; onSortChange?(colId: string, desc: boolean): void; @@ -13,7 +13,7 @@ interface Props> emptyContentLabel?: string; } -export function DatatableContent>({ +export function DatatableContent({ tableInstance, renderRow, onSortChange, diff --git a/app/react/components/datatables/ExpandableDatatable.tsx b/app/react/components/datatables/ExpandableDatatable.tsx index 654266684..56ab2e394 100644 --- a/app/react/components/datatables/ExpandableDatatable.tsx +++ b/app/react/components/datatables/ExpandableDatatable.tsx @@ -7,14 +7,15 @@ import { Props as DatatableProps, PaginationProps, } from './Datatable'; +import { DefaultType } from './types'; -interface Props> +interface Props extends Omit, 'renderRow' | 'expandable'> { renderSubRow(row: Row): ReactNode; expandOnRowClick?: boolean; } -export function ExpandableDatatable>({ +export function ExpandableDatatable({ renderSubRow, getRowCanExpand = () => true, expandOnRowClick, diff --git a/app/react/components/datatables/ExpandableDatatableRow.tsx b/app/react/components/datatables/ExpandableDatatableRow.tsx index 6edc3e821..bcf9da237 100644 --- a/app/react/components/datatables/ExpandableDatatableRow.tsx +++ b/app/react/components/datatables/ExpandableDatatableRow.tsx @@ -2,15 +2,16 @@ import { ReactNode } from 'react'; import { Row } from '@tanstack/react-table'; import { TableRow } from './TableRow'; +import { DefaultType } from './types'; -interface Props> { +interface Props { row: Row; disableSelect?: boolean; renderSubRow(row: Row): ReactNode; expandOnClick?: boolean; } -export function ExpandableDatatableTableRow>({ +export function ExpandableDatatableTableRow({ row, disableSelect, renderSubRow, diff --git a/app/react/components/datatables/Filter.tsx b/app/react/components/datatables/Filter.tsx index c47eabf30..7a763b047 100644 --- a/app/react/components/datatables/Filter.tsx +++ b/app/react/components/datatables/Filter.tsx @@ -8,8 +8,10 @@ import { getValueAsArrayOfStrings } from '@/portainer/helpers/array'; import { Icon } from '@@/Icon'; +import { DefaultType } from './types'; + interface MultipleSelectionFilterProps { - options: string[]; + options: Array | ReadonlyArray; value: string[]; filterKey: string; onChange: (value: string[]) => void; @@ -28,12 +30,12 @@ export function MultipleSelectionFilter({
-
- Filter - -
+ Filter +
@@ -70,9 +72,7 @@ export function MultipleSelectionFilter({ } } -export function filterHOC>( - menuTitle: string -) { +export function filterHOC(menuTitle: string) { return function Filter({ column: { getFilterValue, setFilterValue, getFacetedRowModel, id }, }: { diff --git a/app/react/components/datatables/NestedDatatable.tsx b/app/react/components/datatables/NestedDatatable.tsx index dff600a68..1b0eaaa3b 100644 --- a/app/react/components/datatables/NestedDatatable.tsx +++ b/app/react/components/datatables/NestedDatatable.tsx @@ -12,9 +12,9 @@ import { defaultGetRowId } from './defaultGetRowId'; import { Table } from './Table'; import { NestedTable } from './NestedTable'; import { DatatableContent } from './DatatableContent'; -import { BasicTableSettings } from './types'; +import { BasicTableSettings, DefaultType } from './types'; -interface Props> { +interface Props { dataset: D[]; columns: TableOptions['columns']; @@ -25,7 +25,7 @@ interface Props> { initialSortBy?: BasicTableSettings['sortBy']; } -export function NestedDatatable>({ +export function NestedDatatable({ columns, dataset, getRowId = defaultGetRowId, diff --git a/app/react/components/datatables/TableContent.tsx b/app/react/components/datatables/TableContent.tsx index 80f692dc5..a76dc8ea6 100644 --- a/app/react/components/datatables/TableContent.tsx +++ b/app/react/components/datatables/TableContent.tsx @@ -1,16 +1,16 @@ import { Fragment, PropsWithChildren } from 'react'; import { Row } from '@tanstack/react-table'; -interface Props = Record> { +import { DefaultType } from './types'; + +interface Props { isLoading?: boolean; rows: Row[]; emptyContent?: string; renderRow(row: Row): React.ReactNode; } -export function TableContent< - T extends Record = Record ->({ +export function TableContent({ isLoading = false, rows, emptyContent = 'No items available', diff --git a/app/react/components/datatables/TableHeaderRow.tsx b/app/react/components/datatables/TableHeaderRow.tsx index b3fdb3b42..56fe28fdc 100644 --- a/app/react/components/datatables/TableHeaderRow.tsx +++ b/app/react/components/datatables/TableHeaderRow.tsx @@ -2,15 +2,17 @@ import { Header, flexRender } from '@tanstack/react-table'; import { filterHOC } from './Filter'; import { TableHeaderCell } from './TableHeaderCell'; +import { DefaultType } from './types'; -interface Props = Record> { +interface Props { headers: Header[]; onSortChange?(colId: string, desc: boolean): void; } -export function TableHeaderRow< - D extends Record = Record ->({ headers, onSortChange }: Props) { +export function TableHeaderRow({ + headers, + onSortChange, +}: Props) { return ( {headers.map((header) => { diff --git a/app/react/components/datatables/TableRow.tsx b/app/react/components/datatables/TableRow.tsx index 2b3bda61e..de7426448 100644 --- a/app/react/components/datatables/TableRow.tsx +++ b/app/react/components/datatables/TableRow.tsx @@ -1,15 +1,19 @@ import { Cell, flexRender } from '@tanstack/react-table'; import clsx from 'clsx'; -interface Props = Record> { +import { DefaultType } from './types'; + +interface Props { cells: Cell[]; className?: string; onClick?: () => void; } -export function TableRow< - D extends Record = Record ->({ cells, className, onClick }: Props) { +export function TableRow({ + cells, + className, + onClick, +}: Props) { return ( >( +import { DefaultType } from './types'; +import { defaultGetRowId } from './defaultGetRowId'; + +export function buildNameColumn( nameKey: keyof T, - idKey: string, path: string, - idParam = 'id' + idParam = 'id', + idGetter: (row: T) => string = defaultGetRowId ): ColumnDef { - const cell = createCell(); + const cell = createCell(); return { header: 'Name', @@ -19,7 +22,7 @@ export function buildNameColumn>( enableHiding: false, }; - function createCell>() { + function createCell() { return function NameCell({ renderValue, row }: CellContext) { const name = renderValue() || ''; @@ -30,7 +33,7 @@ export function buildNameColumn>( return ( {name} diff --git a/app/react/components/datatables/defaultGetRowId.ts b/app/react/components/datatables/defaultGetRowId.ts index dbde4b085..1c2f63735 100644 --- a/app/react/components/datatables/defaultGetRowId.ts +++ b/app/react/components/datatables/defaultGetRowId.ts @@ -1,16 +1,17 @@ -export function defaultGetRowId>( - row: D -): string { - if (row.id && (typeof row.id === 'string' || typeof row.id === 'number')) { - return row.id.toString(); - } +import { DefaultType } from './types'; - 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(row: D): string { + const key = ['id', 'Id', 'ID'].find((key) => + Object.hasOwn(row, key) + ) as keyof D; + + const value = row[key]; - if (row.ID && (typeof row.ID === 'string' || typeof row.ID === 'number')) { - return row.ID.toString(); + if (typeof value === 'string' || typeof value === 'number') { + return value.toString(); } return ''; diff --git a/app/react/components/datatables/expand-column.tsx b/app/react/components/datatables/expand-column.tsx index 33a86d681..ce011991c 100644 --- a/app/react/components/datatables/expand-column.tsx +++ b/app/react/components/datatables/expand-column.tsx @@ -3,9 +3,9 @@ import { ColumnDef } from '@tanstack/react-table'; import { Button } from '@@/buttons'; -export function buildExpandColumn< - T extends Record ->(): ColumnDef { +import { DefaultType } from './types'; + +export function buildExpandColumn(): ColumnDef { return { id: 'expand', header: ({ table }) => { diff --git a/app/react/components/datatables/extend-options/mergeOptions.ts b/app/react/components/datatables/extend-options/mergeOptions.ts new file mode 100644 index 000000000..0c10ac314 --- /dev/null +++ b/app/react/components/datatables/extend-options/mergeOptions.ts @@ -0,0 +1,10 @@ +import { TableOptions } from '@tanstack/react-table'; + +type OptionExtender = (options: TableOptions) => TableOptions; + +export function mergeOptions( + ...extenders: Array> +): OptionExtender { + return (options: TableOptions) => + extenders.reduce((acc, option) => option(acc), options); +} diff --git a/app/react/components/datatables/extend-options/withControlledSelected.tsx b/app/react/components/datatables/extend-options/withControlledSelected.tsx new file mode 100644 index 000000000..58003ee05 --- /dev/null +++ b/app/react/components/datatables/extend-options/withControlledSelected.tsx @@ -0,0 +1,37 @@ +import { + RowSelectionState, + TableOptions, + Updater, +} from '@tanstack/react-table'; + +import { DefaultType } from '../types'; + +export function withControlledSelected( + onChange?: (value: string[]) => void, + value?: string[] +) { + return function extendTableOptions(options: TableOptions) { + if (!onChange || !value) { + return options; + } + + return { + ...options, + state: { + ...options.state, + rowSelection: Object.fromEntries(value.map((i) => [i, true])), + }, + onRowSelectionChange(updater: Updater) { + const newValue = + typeof updater !== 'function' + ? updater + : updater(Object.fromEntries(value.map((i) => [i, true]))); + onChange( + Object.entries(newValue) + .filter(([, selected]) => selected) + .map(([id]) => id) + ); + }, + }; + }; +} diff --git a/app/react/components/datatables/extend-options/withGlobalFilter.ts b/app/react/components/datatables/extend-options/withGlobalFilter.ts new file mode 100644 index 000000000..22090838e --- /dev/null +++ b/app/react/components/datatables/extend-options/withGlobalFilter.ts @@ -0,0 +1,15 @@ +import { TableOptions } from '@tanstack/react-table'; + +import { defaultGlobalFilterFn } from '../Datatable'; +import { DefaultType } from '../types'; + +export function withGlobalFilter( + filterFn: typeof defaultGlobalFilterFn +) { + return function extendOptions(options: TableOptions) { + return { + ...options, + globalFilterFn: filterFn, + }; + }; +} diff --git a/app/react/components/datatables/extend-options/withMeta.ts b/app/react/components/datatables/extend-options/withMeta.ts new file mode 100644 index 000000000..cddf02c3f --- /dev/null +++ b/app/react/components/datatables/extend-options/withMeta.ts @@ -0,0 +1,15 @@ +import { TableOptions } from '@tanstack/react-table'; + +import { DefaultType } from '../types'; + +export function withMeta(meta: Record) { + return function extendOptions(options: TableOptions) { + return { + ...options, + meta: { + ...options.meta, + ...meta, + }, + }; + }; +} diff --git a/app/react/components/datatables/filter-types.ts b/app/react/components/datatables/filter-types.ts index 6ab102d4d..4be2bf297 100644 --- a/app/react/components/datatables/filter-types.ts +++ b/app/react/components/datatables/filter-types.ts @@ -1,8 +1,12 @@ import { Row } from '@tanstack/react-table'; -export function multiple< - D extends Record = Record ->({ getValue }: Row, columnId: string, filterValue: string[]): boolean { +import { DefaultType } from './types'; + +export function multiple( + { getValue }: Row, + columnId: string, + filterValue: string[] +): boolean { if (filterValue.length === 0) { return true; } diff --git a/app/react/components/datatables/types.ts b/app/react/components/datatables/types.ts index 321802b7a..c9952641c 100644 --- a/app/react/components/datatables/types.ts +++ b/app/react/components/datatables/types.ts @@ -3,6 +3,8 @@ import { persist } from 'zustand/middleware'; import { keyBuilder } from '@/react/hooks/useLocalStorage'; +export type DefaultType = object; + export interface PaginationTableSettings { pageSize: number; setPageSize: (pageSize: number) => void; @@ -23,21 +25,24 @@ export function paginationSettings( } export interface SortableTableSettings { - sortBy: { id: string; desc: boolean }; - setSortBy: (id: string, desc: boolean) => void; + sortBy: { id: string; desc: boolean } | undefined; + setSortBy: (id: string | undefined, desc: boolean) => void; } export function sortableSettings( set: ZustandSetFunc, - initialSortBy: string | { id: string; desc: boolean } + initialSortBy?: string | { id: string; desc: boolean } ): SortableTableSettings { return { sortBy: typeof initialSortBy === 'string' ? { id: initialSortBy, desc: false } : initialSortBy, - setSortBy: (id: string, desc: boolean) => - set((s) => ({ ...s, sortBy: { id, desc } })), + setSortBy: (id: string | undefined, desc: boolean) => + set((s) => ({ + ...s, + sortBy: typeof id === 'string' ? { id, desc } : id, + })), }; } @@ -77,7 +82,7 @@ export interface BasicTableSettings export function createPersistedStore( storageKey: string, - initialSortBy: string | { id: string; desc: boolean } = 'name', + initialSortBy?: string | { id: string; desc: boolean }, create: (set: ZustandSetFunc) => Omit = () => ({} as T) ) { @@ -85,11 +90,8 @@ export function createPersistedStore( persist( (set) => ({ - ...sortableSettings( - set as ZustandSetFunc, - initialSortBy - ), - ...paginationSettings(set as ZustandSetFunc), + ...sortableSettings(set, initialSortBy), + ...paginationSettings(set), ...create(set), } as T), { @@ -98,18 +100,3 @@ export function createPersistedStore( ) ); } - -/** 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 { - // eslint-disable-next-line class-methods-use-this - wrapped() { - return createPersistedStore('', ''); - } -} - -export type CreatePersistedStoreReturn< - T extends BasicTableSettings = BasicTableSettings -> = ReturnType['wrapped']>; diff --git a/app/react/components/datatables/useTableState.ts b/app/react/components/datatables/useTableState.ts index 0bd74c9a8..20d8cceb3 100644 --- a/app/react/components/datatables/useTableState.ts +++ b/app/react/components/datatables/useTableState.ts @@ -2,7 +2,7 @@ import { useMemo, useState } from 'react'; import { useStore } from 'zustand'; import { useSearchBarState } from './SearchBar'; -import { BasicTableSettings, CreatePersistedStoreReturn } from './types'; +import { BasicTableSettings, createPersistedStore } from './types'; export type TableState = TSettings & { setSearch: (search: string) => void; @@ -11,7 +11,10 @@ export type TableState = TSettings & { export function useTableState< TSettings extends BasicTableSettings = BasicTableSettings ->(store: CreatePersistedStoreReturn, storageKey: string) { +>( + store: ReturnType>, + storageKey: string +) { const settings = useStore(store); const [search, setSearch] = useSearchBarState(storageKey); @@ -23,21 +26,24 @@ export function useTableState< } export function useTableStateWithoutStorage( - defaultSortKey: string + defaultSortKey?: string ): BasicTableSettings & { setSearch: (search: string) => void; search: string; } { const [search, setSearch] = useState(''); const [pageSize, setPageSize] = useState(10); - const [sortBy, setSortBy] = useState({ id: defaultSortKey, desc: false }); + const [sortBy, setSortBy] = useState( + defaultSortKey ? { id: defaultSortKey, desc: false } : undefined + ); return { search, setSearch, pageSize, setPageSize, - setSortBy: (id: string, desc: boolean) => setSortBy({ id, desc }), + setSortBy: (id: string | undefined, desc: boolean) => + setSortBy(id ? { id, desc } : undefined), sortBy, }; } diff --git a/app/react/docker/components/FilesTable/FilesTable.tsx b/app/react/docker/components/FilesTable/FilesTable.tsx index 7b42a7788..01c951815 100644 --- a/app/react/docker/components/FilesTable/FilesTable.tsx +++ b/app/react/docker/components/FilesTable/FilesTable.tsx @@ -7,8 +7,9 @@ import { Datatable } from '@@/datatables'; import { BasicTableSettings } from '@@/datatables/types'; import { Button } from '@@/buttons'; 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'; interface Props { @@ -72,14 +73,19 @@ export function FilesTable({ } return ( - + title={title} titleIcon={FileIcon} dataset={isRoot ? dataset : [goToParent(onGoToParent), ...dataset]} settingsManager={tableState} columns={columns} getRowId={(row) => row.Name} - meta={{ + initialTableState={{ + columnVisibility: { + Dir: false, + }, + }} + extendTableOptions={withMeta({ table: 'files', isEdit, setIsEdit, @@ -87,12 +93,7 @@ export function FilesTable({ onBrowse, onDownload, onDelete, - }} - initialTableState={{ - columnVisibility: { - Dir: false, - }, - }} + })} disableSelect renderTableActions={() => { if (!isUploadAllowed) { diff --git a/app/react/docker/configs/ListView/ConfigsDatatable/columns.tsx b/app/react/docker/configs/ListView/ConfigsDatatable/columns.tsx index ac08b2478..5a3445332 100644 --- a/app/react/docker/configs/ListView/ConfigsDatatable/columns.tsx +++ b/app/react/docker/configs/ListView/ConfigsDatatable/columns.tsx @@ -3,14 +3,14 @@ import { createColumnHelper } from '@tanstack/react-table'; import { isoDate } from '@/portainer/filters/filters'; import { createOwnershipColumn } from '@/react/docker/components/datatable-helpers/createOwnershipColumn'; -import { buildNameColumn } from '@@/datatables/NameCell'; +import { buildNameColumn } from '@@/datatables/buildNameColumn'; import { DockerConfig } from '../../types'; const columnHelper = createColumnHelper(); export const columns = [ - buildNameColumn('Name', 'Id', 'docker.configs.config'), + buildNameColumn('Name', 'docker.configs.config'), columnHelper.accessor('CreatedAt', { header: 'Creation Date', cell: ({ getValue }) => { diff --git a/app/react/edge/components/EdgeGroupAssociationTable.tsx b/app/react/edge/components/EdgeGroupAssociationTable.tsx index 749374414..f1f315223 100644 --- a/app/react/edge/components/EdgeGroupAssociationTable.tsx +++ b/app/react/edge/components/EdgeGroupAssociationTable.tsx @@ -56,8 +56,8 @@ export function EdgeGroupAssociationTable({ pageLimit: tableState.pageSize, page: page + 1, search: tableState.search, - sort: tableState.sortBy.id as 'Group' | 'Name', - order: tableState.sortBy.desc ? 'desc' : 'asc', + sort: tableState.sortBy?.id as 'Group' | 'Name', + order: tableState.sortBy?.desc ? 'desc' : 'asc', ...query, }); const groupsQuery = useGroups({ diff --git a/app/react/edge/edge-devices/WaitingRoomView/Datatable/Datatable.tsx b/app/react/edge/edge-devices/WaitingRoomView/Datatable/Datatable.tsx index 0a1ddc132..4c5c408a7 100644 --- a/app/react/edge/edge-devices/WaitingRoomView/Datatable/Datatable.tsx +++ b/app/react/edge/edge-devices/WaitingRoomView/Datatable/Datatable.tsx @@ -9,7 +9,7 @@ import { useEnvironments } from './useEnvironments'; const storageKey = 'edge-devices-waiting-room'; -const settingsStore = createPersistedStore(storageKey); +const settingsStore = createPersistedStore(storageKey, 'name'); export function Datatable() { const tableState = useTableState(settingsStore, storageKey); diff --git a/app/react/edge/edge-stacks/ItemView/EnvironmentsDatatable/EnvironmentsDatatable.tsx b/app/react/edge/edge-stacks/ItemView/EnvironmentsDatatable/EnvironmentsDatatable.tsx index ad2029c76..6dd261178 100644 --- a/app/react/edge/edge-stacks/ItemView/EnvironmentsDatatable/EnvironmentsDatatable.tsx +++ b/app/react/edge/edge-stacks/ItemView/EnvironmentsDatatable/EnvironmentsDatatable.tsx @@ -44,8 +44,8 @@ export function EnvironmentsDatatable() { pageLimit: tableState.pageSize, page: page + 1, search: tableState.search, - sort: tableState.sortBy.id as 'Group' | 'Name', - order: tableState.sortBy.desc ? 'desc' : 'asc', + sort: tableState.sortBy?.id as 'Group' | 'Name', + order: tableState.sortBy?.desc ? 'desc' : 'asc', edgeStackId: stackId, edgeStackStatus: statusFilter, }); diff --git a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/columns.tsx b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/columns.tsx index 354b9669a..84bdf418d 100644 --- a/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/columns.tsx +++ b/app/react/edge/edge-stacks/ListView/EdgeStacksDatatable/columns.tsx @@ -4,7 +4,7 @@ import _ from 'lodash'; import { isoDateFromTimestamp } from '@/portainer/filters/filters'; 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 { StatusType } from '../../types'; @@ -16,12 +16,7 @@ import { DeploymentCounter } from './DeploymentCounter'; const columnHelper = createColumnHelper(); export const columns = _.compact([ - buildNameColumn( - 'Name', - 'Id', - 'edge.stacks.edit', - 'stackId' - ), + buildNameColumn('Name', 'edge.stacks.edit', 'stackId'), columnHelper.accessor( (item) => item.aggregatedStatus[StatusType.Acknowledged] || 0, { diff --git a/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/IngressClassDatatable.tsx b/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/IngressClassDatatable.tsx index cc5b462da..d9eaef5a6 100644 --- a/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/IngressClassDatatable.tsx +++ b/app/react/kubernetes/cluster/ingressClass/IngressClassDatatable/IngressClassDatatable.tsx @@ -15,7 +15,7 @@ import { IngressControllerClassMap } from '../types'; import { columns } from './columns'; const storageKey = 'ingressClasses'; -const settingsStore = createPersistedStore(storageKey); +const settingsStore = createPersistedStore(storageKey, 'name'); interface Props { onChangeControllers: ( diff --git a/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx b/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx index dba304a65..1b17a818c 100644 --- a/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx +++ b/app/react/portainer/environments/ListView/EnvironmentsDatatable.tsx @@ -37,8 +37,10 @@ export function EnvironmentsDatatable({ excludeSnapshots: true, page: page + 1, pageLimit: tableState.pageSize, - sort: isSortType(tableState.sortBy.id) ? tableState.sortBy.id : undefined, - order: tableState.sortBy.desc ? 'desc' : 'asc', + sort: isSortType(tableState.sortBy?.id) + ? tableState.sortBy?.id + : undefined, + order: tableState.sortBy?.desc ? 'desc' : 'asc', }, { enabled: groupsQuery.isSuccess, refetchInterval: 30 * 1000 } ); diff --git a/app/react/portainer/environments/environment-groups/components/GroupAssociationTable.tsx b/app/react/portainer/environments/environment-groups/components/GroupAssociationTable.tsx index 33f172fdf..061e15286 100644 --- a/app/react/portainer/environments/environment-groups/components/GroupAssociationTable.tsx +++ b/app/react/portainer/environments/environment-groups/components/GroupAssociationTable.tsx @@ -38,8 +38,8 @@ export function GroupAssociationTable({ pageLimit: tableState.pageSize, page: page + 1, search: tableState.search, - sort: tableState.sortBy.id as 'Name', - order: tableState.sortBy.desc ? 'desc' : 'asc', + sort: tableState.sortBy?.id as 'Name', + order: tableState.sortBy?.desc ? 'desc' : 'asc', ...query, }); diff --git a/app/react/portainer/environments/queries/useEnvironmentList.ts b/app/react/portainer/environments/queries/useEnvironmentList.ts index ba05e9e23..64432d53d 100644 --- a/app/react/portainer/environments/queries/useEnvironmentList.ts +++ b/app/react/portainer/environments/queries/useEnvironmentList.ts @@ -14,7 +14,7 @@ export const ENVIRONMENTS_POLLING_INTERVAL = 30000; // in ms export const SortOptions = ['Name', 'Group', 'Status'] as const; 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); } diff --git a/app/react/portainer/environments/update-schedules/ListView/columns/index.ts b/app/react/portainer/environments/update-schedules/ListView/columns/index.ts index ceffc063a..5b99dd04c 100644 --- a/app/react/portainer/environments/update-schedules/ListView/columns/index.ts +++ b/app/react/portainer/environments/update-schedules/ListView/columns/index.ts @@ -1,4 +1,4 @@ -import { buildNameColumn } from '@@/datatables/NameCell'; +import { buildNameColumn } from '@@/datatables/buildNameColumn'; import { EdgeUpdateListItemResponse } from '../../queries/list'; @@ -9,7 +9,7 @@ import { scheduledTime } from './scheduled-time'; import { scheduleType } from './type'; export const columns = [ - buildNameColumn('name', 'id', '.item'), + buildNameColumn('name', '.item'), scheduledTime, groups, scheduleType, diff --git a/app/react/portainer/settings/EdgeComputeView/FDOProfilesDatatable/columns/index.tsx b/app/react/portainer/settings/EdgeComputeView/FDOProfilesDatatable/columns/index.tsx index 6983ca15c..01ae237ee 100644 --- a/app/react/portainer/settings/EdgeComputeView/FDOProfilesDatatable/columns/index.tsx +++ b/app/react/portainer/settings/EdgeComputeView/FDOProfilesDatatable/columns/index.tsx @@ -1,10 +1,10 @@ import { Profile } from '@/portainer/hostmanagement/fdo/model'; -import { buildNameColumn } from '@@/datatables/NameCell'; +import { buildNameColumn } from '@@/datatables/buildNameColumn'; import { created } from './created'; export const columns = [ - buildNameColumn('name', 'id', 'portainer.endpoints.profile.edit'), + buildNameColumn('name', 'portainer.endpoints.profile.edit'), created, ]; diff --git a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamMembersList/TeamMembersList.tsx b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamMembersList/TeamMembersList.tsx index f0a6a1d1f..e7f25dc75 100644 --- a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamMembersList/TeamMembersList.tsx +++ b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/TeamMembersList/TeamMembersList.tsx @@ -33,7 +33,9 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) { const [search, setSearch] = useState(''); 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(); @@ -79,8 +81,8 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) { ); - function handleSetSort(colId: string, desc: boolean) { - setSortBy({ id: colId, desc }); + function handleSetSort(colId: string | undefined, desc: boolean) { + setSortBy(colId ? { id: colId, desc } : undefined); } function handleRemoveMembers(userIds: UserId[]) { diff --git a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/UsersList/UsersList.tsx b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/UsersList/UsersList.tsx index fd790d2b3..15c5f8e55 100644 --- a/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/UsersList/UsersList.tsx +++ b/app/react/portainer/users/teams/ItemView/TeamAssociationSelector/UsersList/UsersList.tsx @@ -25,7 +25,9 @@ export function UsersList({ users, disabled, teamId }: Props) { const [search, setSearch] = useState(''); const [pageSize, setPageSize] = useState(10); 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(); @@ -62,8 +64,8 @@ export function UsersList({ users, disabled, teamId }: Props) { ); - function handleSetSort(colId: string, desc: boolean) { - setSortBy({ id: colId, desc }); + function handleSetSort(colId: string | undefined, desc: boolean) { + setSortBy(colId ? { id: colId, desc } : undefined); } function handleAddAllMembers(userIds: UserId[]) { diff --git a/app/react/portainer/users/teams/ListView/TeamsDatatable/TeamsDatatable.tsx b/app/react/portainer/users/teams/ListView/TeamsDatatable/TeamsDatatable.tsx index 9312eeb6f..a8f277385 100644 --- a/app/react/portainer/users/teams/ListView/TeamsDatatable/TeamsDatatable.tsx +++ b/app/react/portainer/users/teams/ListView/TeamsDatatable/TeamsDatatable.tsx @@ -10,14 +10,14 @@ import { deleteTeam } from '@/react/portainer/users/teams/teams.service'; import { confirmDelete } from '@@/modals/confirm'; import { Datatable } from '@@/datatables'; import { Button } from '@@/buttons'; -import { buildNameColumn } from '@@/datatables/NameCell'; +import { buildNameColumn } from '@@/datatables/buildNameColumn'; import { createPersistedStore } from '@@/datatables/types'; import { useTableState } from '@@/datatables/useTableState'; const storageKey = 'teams'; const columns: ColumnDef[] = [ - buildNameColumn('Name', 'Id', 'portainer.teams.team'), + buildNameColumn('Name', 'portainer.teams.team'), ]; interface Props { @@ -25,7 +25,7 @@ interface Props { isAdmin: boolean; } -const settingsStore = createPersistedStore(storageKey); +const settingsStore = createPersistedStore(storageKey, 'name'); export function TeamsDatatable({ teams, isAdmin }: Props) { const { handleRemove } = useRemoveMutation();