chore(deps): upgrade react-table to v8 [EE-4837] (#8245)

pull/8878/head
Chaim Lev-Ari 2023-05-02 13:42:16 +07:00 committed by GitHub
parent f20d3e72b9
commit 757461d58b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
140 changed files with 1805 additions and 2872 deletions

View File

@ -1,149 +1,9 @@
import {
UseColumnOrderInstanceProps,
UseColumnOrderState,
UseExpandedHooks,
UseExpandedInstanceProps,
UseExpandedOptions,
UseExpandedRowProps,
UseExpandedState,
UseFiltersColumnOptions,
UseFiltersColumnProps,
UseFiltersInstanceProps,
UseFiltersOptions,
UseFiltersState,
UseGlobalFiltersColumnOptions,
UseGlobalFiltersInstanceProps,
UseGlobalFiltersOptions,
UseGlobalFiltersState,
UseGroupByCellProps,
UseGroupByColumnOptions,
UseGroupByColumnProps,
UseGroupByHooks,
UseGroupByInstanceProps,
UseGroupByOptions,
UseGroupByRowProps,
UseGroupByState,
UsePaginationInstanceProps,
UsePaginationOptions,
UsePaginationState,
UseResizeColumnsColumnOptions,
UseResizeColumnsColumnProps,
UseResizeColumnsOptions,
UseResizeColumnsState,
UseRowSelectHooks,
UseRowSelectInstanceProps,
UseRowSelectOptions,
UseRowSelectRowProps,
UseRowSelectState,
UseRowStateCellProps,
UseRowStateInstanceProps,
UseRowStateOptions,
UseRowStateRowProps,
UseRowStateState,
UseSortByColumnOptions,
UseSortByColumnProps,
UseSortByHooks,
UseSortByInstanceProps,
UseSortByOptions,
UseSortByState,
} from 'react-table';
import { UseSelectColumnTableOptions } from '@lineup-lite/hooks';
import '@tanstack/react-table';
declare module 'react-table' {
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
export interface TableOptions<D extends Record<string, unknown>>
extends UseExpandedOptions<D>,
UseFiltersOptions<D>,
UseGlobalFiltersOptions<D>,
UseGroupByOptions<D>,
UsePaginationOptions<D>,
UseResizeColumnsOptions<D>,
UseRowSelectOptions<D>,
UseRowStateOptions<D>,
UseSortByOptions<D>,
UseSelectColumnTableOptions<D>,
// note that having Record here allows you to add anything to the options, this matches the spirit of the
// underlying js library, but might be cleaner if it's replaced by a more specific type that matches your
// feature set, this is a safe default.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Record<string, any> {}
export interface Hooks<
D extends Record<string, unknown> = Record<string, unknown>
> extends UseExpandedHooks<D>,
UseGroupByHooks<D>,
UseRowSelectHooks<D>,
UseSortByHooks<D> {}
export interface TableInstance<
D extends Record<string, unknown> = Record<string, unknown>
> extends UseColumnOrderInstanceProps<D>,
UseExpandedInstanceProps<D>,
UseFiltersInstanceProps<D>,
UseGlobalFiltersInstanceProps<D>,
UseGroupByInstanceProps<D>,
UsePaginationInstanceProps<D>,
UseRowSelectInstanceProps<D>,
UseRowStateInstanceProps<D>,
UseSortByInstanceProps<D> {}
export interface TableState<
D extends Record<string, unknown> = Record<string, unknown>
> extends UseColumnOrderState<D>,
UseExpandedState<D>,
UseFiltersState<D>,
UseGlobalFiltersState<D>,
UseGroupByState<D>,
UsePaginationState<D>,
UseResizeColumnsState<D>,
UseRowSelectState<D>,
UseRowStateState<D>,
UseSortByState<D> {}
export interface ColumnInterface<
D extends Record<string, unknown> = Record<string, unknown>
> extends UseFiltersColumnOptions<D>,
UseGlobalFiltersColumnOptions<D>,
UseGroupByColumnOptions<D>,
UseResizeColumnsColumnOptions<D>,
UseSortByColumnOptions<D> {
declare module '@tanstack/table-core' {
interface ColumnMeta<TData extends RowData, TValue> {
className?: string;
canHide?: boolean;
}
export interface ColumnInstance<
D extends Record<string, unknown> = Record<string, unknown>
> extends UseFiltersColumnProps<D>,
UseGroupByColumnProps<D>,
UseResizeColumnsColumnProps<D>,
UseSortByColumnProps<D> {
className?: string;
}
export interface Cell<
D extends Record<string, unknown> = Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
V = any
> extends UseTableCellProps<D, V>,
UseGroupByCellProps<D>,
UseRowStateCellProps<D> {
className?: string;
}
export interface Row<
D extends Record<string, unknown> = Record<string, unknown>
> extends UseExpandedRowProps<D>,
UseGroupByRowProps<D>,
UseRowSelectRowProps<D>,
UseRowStateRowProps<D> {}
export function makePropGetter(
hooks: Array<PropGetter>,
...meta: Record<string, unknown>[]
): PropGetter;
export interface TableToggleRowsSelectedProps {
disabled: boolean;
filter?: Filter<TData, TValue>;
width?: number | 'auto' | string;
}
}

View File

@ -1,5 +1,4 @@
import { Box, Plus, Trash2 } from 'lucide-react';
import { useStore } from 'zustand';
import { ContainerGroup } from '@/react/azure/types';
import { Authorized } from '@/react/hooks/useUser';
@ -9,7 +8,7 @@ import { Datatable } from '@@/datatables';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
import { createPersistedStore } from '@@/datatables/types';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { useTableState } from '@@/datatables/useTableState';
import { columns } from './columns';
@ -22,19 +21,13 @@ export interface Props {
}
export function ContainersDatatable({ dataset, onRemoveClick }: Props) {
const settings = useStore(settingsStore);
const [search, setSearch] = useSearchBarState(tableKey);
const tableState = useTableState(settingsStore, tableKey);
return (
<Datatable
dataset={dataset}
columns={columns}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
settingsManager={tableState}
title="Containers"
titleIcon={Box}
getRowId={(container) => container.id}

View File

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { ContainerGroup } from '@/react/azure/types';
export const columnHelper = createColumnHelper<ContainerGroup>();

View File

@ -1,13 +1,5 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import { ContainerGroup } from '@/react/azure/types';
export const location: Column<ContainerGroup> = {
Header: 'Location',
accessor: (container) => container.location,
id: 'location',
disableFilters: true,
Filter: () => null,
canHide: true,
sortType: 'string',
};
export const location = columnHelper.accessor('location', {
header: 'Location',
});

View File

@ -1,24 +1,22 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { ContainerGroup } from '@/react/azure/types';
import { Link } from '@@/Link';
export const name: Column<ContainerGroup> = {
Header: 'Name',
accessor: (container) => container.name,
id: 'name',
Cell: NameCell,
disableFilters: true,
Filter: () => null,
canHide: true,
sortType: 'string',
};
import { columnHelper } from './helper';
export const name = columnHelper.accessor('name', {
header: 'Name',
cell: NameCell,
});
export function NameCell({
value: name,
getValue,
row: { original: container },
}: CellProps<ContainerGroup, string>) {
}: CellContext<ContainerGroup, string>) {
const name = getValue();
return (
<Link
to="azure.containerinstances.container"

View File

@ -1,30 +1,30 @@
import { Column } from 'react-table';
import clsx from 'clsx';
import { CellContext } from '@tanstack/react-table';
import { ownershipIcon } from '@/portainer/filters/filters';
import { ResourceControlOwnership } from '@/react/portainer/access-control/types';
import { ContainerGroup } from '@/react/azure/types';
import { determineOwnership } from '@/react/portainer/access-control/models/ResourceControlViewModel';
export const ownership: Column<ContainerGroup> = {
Header: 'Ownership',
id: 'ownership',
accessor: (row) =>
import { columnHelper } from './helper';
export const ownership = columnHelper.accessor(
(row) =>
row.Portainer && row.Portainer.ResourceControl
? determineOwnership(row.Portainer.ResourceControl)
: ResourceControlOwnership.ADMINISTRATORS,
Cell: OwnershipCell,
disableFilters: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};
{
header: 'Ownership',
cell: OwnershipCell,
id: 'ownership',
}
);
interface Props {
value: 'public' | 'private' | 'restricted' | 'administrators';
}
function OwnershipCell({
getValue,
}: CellContext<ContainerGroup, ResourceControlOwnership>) {
const value = getValue();
function OwnershipCell({ value }: Props) {
return (
<>
<i

View File

@ -1,25 +1,25 @@
import { CellProps, Column } from 'react-table';
import { ExternalLink } from 'lucide-react';
import { CellContext } from '@tanstack/react-table';
import { ContainerGroup } from '@/react/azure/types';
import { getPorts } from '@/react/azure/utils';
import { Icon } from '@@/Icon';
export const ports: Column<ContainerGroup> = {
Header: 'Published Ports',
accessor: (container) => getPorts(container),
import { columnHelper } from './helper';
export const ports = columnHelper.accessor(getPorts, {
header: 'Published Ports',
cell: PortsCell,
id: 'ports',
disableFilters: true,
Filter: () => null,
canHide: true,
Cell: PortsCell,
};
});
function PortsCell({
value: ports,
getValue,
row: { original: container },
}: CellProps<ContainerGroup, ReturnType<typeof getPorts>>) {
}: CellContext<ContainerGroup, ReturnType<typeof getPorts>>) {
const ports = getValue();
const ip = container.properties.ipAddress
? container.properties.ipAddress.ip
: '';

View File

@ -19,7 +19,7 @@ export function DetailsRow({
<td className={clsx(colClassName, 'min-w-[150px] !break-normal')}>
{label}
</td>
{children && (
{!!children && (
<td className={colClassName} data-cy={`detailsTable-${label}Value`}>
{children}
</td>

View File

@ -21,7 +21,7 @@ export function DetailsTable({
</tr>
</thead>
)}
<tbody>{children}</tbody>
{children && <tbody>{children}</tbody>}
</table>
);
}

View File

@ -1,12 +1,12 @@
import clsx from 'clsx';
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
import { ColumnInstance } from 'react-table';
import { Columns } from 'lucide-react';
import { Column } from '@tanstack/react-table';
import { Checkbox } from '@@/form-components/Checkbox';
interface Props<D extends object> {
columns: ColumnInstance<D>[];
columns: Column<D>[];
onChange: (value: string[]) => void;
value: string[];
}
@ -40,8 +40,12 @@ export function ColumnVisibilityMenu<D extends object>({
{columns.map((column) => (
<div key={column.id}>
<Checkbox
checked={column.isVisible}
label={column.Header as string}
checked={column.getIsVisible()}
label={
typeof column.columnDef.header === 'string'
? column.columnDef.header
: ''
}
id={`visibility_${column.id}`}
onChange={(e) =>
handleChangeColumnVisibility(

View File

@ -1,36 +1,40 @@
import {
useTable,
useFilters,
useGlobalFilter,
useSortBy,
usePagination,
Column,
Row,
TableInstance,
Table as TableInstance,
TableState,
TableRowProps,
useExpanded,
} from 'react-table';
import { ReactNode } from 'react';
import { useRowSelectColumn } from '@lineup-lite/hooks';
useReactTable,
Row,
getCoreRowModel,
getPaginationRowModel,
getFilteredRowModel,
getSortedRowModel,
getFacetedRowModel,
getFacetedUniqueValues,
getFacetedMinMaxValues,
getExpandedRowModel,
TableOptions,
} from '@tanstack/react-table';
import { ReactNode, useMemo } from 'react';
import clsx from 'clsx';
import _ from 'lodash';
import { IconProps } from '@@/Icon';
import { Table } from './Table';
import { multiple } from './filter-types';
import { useRowSelect } from './useRowSelect';
import { BasicTableSettings } from './types';
import { DatatableHeader } from './DatatableHeader';
import { DatatableFooter } from './DatatableFooter';
import { DatatableContent } from './DatatableContent';
import { defaultGetRowId } from './defaultGetRowId';
import { emptyPlugin } from './emptyReactTablePlugin';
import { Table } from './Table';
import { useGoToHighlightedRow } from './useGoToHighlightedRow';
import { BasicTableSettings } from './types';
import { DatatableContent } from './DatatableContent';
import { createSelectColumn } from './select-column';
import { TableRow } from './TableRow';
export interface Props<D extends Record<string, unknown>> {
export interface Props<
D extends Record<string, unknown>,
TSettings extends BasicTableSettings = BasicTableSettings
> {
dataset: D[];
columns: readonly Column<D>[];
columns: TableOptions<D>['columns'];
renderTableSettings?(instance: TableInstance<D>): ReactNode;
renderTableActions?(selectedRows: D[]): ReactNode;
disableSelect?: boolean;
@ -39,29 +43,20 @@ export interface Props<D extends Record<string, unknown>> {
emptyContentLabel?: string;
title?: string;
titleIcon?: IconProps['icon'];
initialTableState?: Partial<TableState<D>>;
initialTableState?: Partial<TableState>;
isLoading?: boolean;
totalCount?: number;
description?: ReactNode;
pageCount?: number;
initialSortBy?: BasicTableSettings['sortBy'];
initialPageSize?: BasicTableSettings['pageSize'];
highlightedItemId?: string;
searchValue: string;
onSearchChange(search: string): void;
onSortByChange(colId: string, desc: boolean): void;
onPageSizeChange(pageSize: number): void;
// send state up
onPageChange?(page: number): void;
renderRow?(
row: Row<D>,
rowProps: TableRowProps,
highlightedItemId?: string
): ReactNode;
expandable?: boolean;
settingsManager: TSettings & {
search: string;
setSearch: (value: string) => void;
};
renderRow?(row: Row<D>, highlightedItemId?: string): ReactNode;
getRowCanExpand?(row: Row<D>): boolean;
noWidget?: boolean;
}
@ -81,78 +76,83 @@ export function Datatable<D extends Record<string, unknown>>({
totalCount = dataset.length,
description,
pageCount,
initialSortBy,
initialPageSize = 10,
onPageChange = () => {},
onPageSizeChange,
onSortByChange,
searchValue,
onSearchChange,
onPageChange = () => null,
settingsManager: settings,
renderRow = defaultRenderRow,
expandable = false,
highlightedItemId,
noWidget,
getRowCanExpand,
}: Props<D>) {
const isServerSidePagination = typeof pageCount !== 'undefined';
const tableInstance = useTable<D>(
{
defaultCanFilter: false,
columns,
data: dataset,
filterTypes: { multiple },
initialState: {
pageSize: initialPageSize,
sortBy: initialSortBy ? [initialSortBy] : [],
globalFilter: searchValue,
...initialTableState,
},
isRowSelectable,
autoResetExpanded: false,
autoResetSelectedRows: false,
getRowId,
...(isServerSidePagination ? { manualPagination: true, pageCount } : {}),
},
useFilters,
useGlobalFilter,
useSortBy,
expandable ? useExpanded : emptyPlugin,
usePagination,
useRowSelect,
!disableSelect ? useRowSelectColumn : emptyPlugin
const enableRowSelection = getIsSelectionEnabled(
disableSelect,
isRowSelectable
);
const allColumns = useMemo(
() => _.compact([!disableSelect && createSelectColumn<D>(), ...columns]),
[disableSelect, columns]
);
const tableInstance = useReactTable<D>({
columns: allColumns,
data: dataset,
initialState: {
pagination: {
pageSize: settings.pageSize,
},
sorting: settings.sortBy ? [settings.sortBy] : [],
globalFilter: settings.search,
...initialTableState,
},
defaultColumn: {
enableColumnFilter: false,
enableHiding: true,
},
enableRowSelection,
autoResetExpanded: false,
globalFilterFn,
getRowId,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
getFacetedRowModel: getFacetedRowModel(),
getFacetedUniqueValues: getFacetedUniqueValues(),
getFacetedMinMaxValues: getFacetedMinMaxValues(),
getExpandedRowModel: getExpandedRowModel(),
getRowCanExpand,
...(isServerSidePagination ? { manualPagination: true, pageCount } : {}),
});
const tableState = tableInstance.getState();
useGoToHighlightedRow(
isServerSidePagination,
tableInstance.state.pageSize,
tableInstance.rows,
tableState.pagination.pageSize,
tableInstance.getCoreRowModel().rows,
handlePageChange,
highlightedItemId
);
const selectedItems = tableInstance.selectedFlatRows.map(
(row) => row.original
);
const selectedRowModel = tableInstance.getSelectedRowModel();
const selectedItems = selectedRowModel.rows.map((row) => row.original);
return (
<Table.Container noWidget={noWidget}>
<DatatableHeader
onSearchChange={handleSearchBarChange}
searchValue={searchValue}
searchValue={settings.search}
title={title}
titleIcon={titleIcon}
description={description}
renderTableActions={() => renderTableActions(selectedItems)}
renderTableSettings={() => renderTableSettings(tableInstance)}
description={description}
/>
<DatatableContent<D>
tableInstance={tableInstance}
renderRow={(row, rowProps) =>
renderRow(row, rowProps, highlightedItemId)
}
renderRow={(row) => renderRow(row, highlightedItemId)}
emptyContentLabel={emptyContentLabel}
isLoading={isLoading}
onSortChange={handleSortChange}
@ -161,8 +161,8 @@ export function Datatable<D extends Record<string, unknown>>({
<DatatableFooter
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
page={tableInstance.state.pageIndex}
pageSize={tableInstance.state.pageSize}
page={tableState.pagination.pageIndex}
pageSize={tableState.pagination.pageSize}
totalCount={totalCount}
totalSelected={selectedItems.length}
/>
@ -171,38 +171,81 @@ export function Datatable<D extends Record<string, unknown>>({
function handleSearchBarChange(value: string) {
tableInstance.setGlobalFilter(value);
onSearchChange(value);
settings.setSearch(value);
}
function handlePageChange(page: number) {
tableInstance.gotoPage(page);
tableInstance.setPageIndex(page);
onPageChange(page);
}
function handleSortChange(colId: string, desc: boolean) {
onSortByChange(colId, desc);
settings.setSortBy(colId, desc);
}
function handlePageSizeChange(pageSize: number) {
tableInstance.setPageSize(pageSize);
onPageSizeChange(pageSize);
settings.setPageSize(pageSize);
}
}
function defaultRenderRow<D extends Record<string, unknown>>(
row: Row<D>,
rowProps: TableRowProps,
highlightedItemId?: string
) {
return (
<Table.Row<D>
key={rowProps.key}
cells={row.cells}
className={clsx(rowProps.className, {
<TableRow<D>
cells={row.getVisibleCells()}
className={clsx({
active: highlightedItemId === row.id,
})}
role={rowProps.role}
style={rowProps.style}
/>
);
}
function getIsSelectionEnabled<D extends Record<string, unknown>>(
disabledSelect?: boolean,
isRowSelectable?: Props<D>['isRowSelectable']
) {
if (disabledSelect) {
return false;
}
if (isRowSelectable) {
return isRowSelectable;
}
return true;
}
function globalFilterFn<D>(
row: Row<D>,
columnId: string,
filterValue: null | string
): boolean {
const value = row.getValue(columnId);
if (filterValue === null || filterValue === '') {
return true;
}
if (value == null) {
return false;
}
const filterValueLower = filterValue.toLowerCase();
if (
typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean'
) {
return value.toString().toLowerCase().includes(filterValueLower);
}
if (Array.isArray(value)) {
return value.some((item) => item.toLowerCase().includes(filterValueLower));
}
return false;
}

View File

@ -1,10 +1,10 @@
import { Row, TableInstance, TableRowProps } from 'react-table';
import { Row, Table as TableInstance } from '@tanstack/react-table';
import { Table } from './Table';
interface Props<D extends Record<string, unknown>> {
tableInstance: TableInstance<D>;
renderRow(row: Row<D>, rowProps: TableRowProps): React.ReactNode;
renderRow(row: Row<D>): React.ReactNode;
onSortChange?(colId: string, desc: boolean): void;
isLoading?: boolean;
emptyContentLabel?: string;
@ -17,42 +17,24 @@ export function DatatableContent<D extends Record<string, unknown>>({
isLoading,
emptyContentLabel,
}: Props<D>) {
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
tableInstance;
const headerGroups = tableInstance.getHeaderGroups();
const pageRowModel = tableInstance.getPaginationRowModel();
const tableProps = getTableProps();
const tbodyProps = getTableBodyProps();
return (
<Table
className={tableProps.className}
role={tableProps.role}
style={tableProps.style}
>
<Table>
<thead>
{headerGroups.map((headerGroup) => {
const { key, className, role, style } =
headerGroup.getHeaderGroupProps();
return (
<Table.HeaderRow<D>
key={key}
className={className}
role={role}
style={style}
headers={headerGroup.headers}
onSortChange={onSortChange}
/>
);
})}
{headerGroups.map((headerGroup) => (
<Table.HeaderRow<D>
key={headerGroup.id}
headers={headerGroup.headers}
onSortChange={onSortChange}
/>
))}
</thead>
<tbody
className={tbodyProps.className}
role={tbodyProps.role}
style={tbodyProps.style}
>
<tbody>
<Table.Content<D>
rows={page}
rows={pageRowModel.rows}
isLoading={isLoading}
prepareRow={prepareRow}
emptyContent={emptyContentLabel}
renderRow={renderRow}
/>

View File

@ -1,4 +1,4 @@
import { Row } from 'react-table';
import { Row } from '@tanstack/react-table';
import { ReactNode } from 'react';
import { ExpandableDatatableTableRow } from './ExpandableDatatableRow';
@ -7,25 +7,25 @@ import { Datatable, Props as DatatableProps } from './Datatable';
interface Props<D extends Record<string, unknown>>
extends Omit<DatatableProps<D>, 'renderRow' | 'expandable'> {
renderSubRow(row: Row<D>): ReactNode;
expandOnRowClick?: boolean;
}
export function ExpandableDatatable<D extends Record<string, unknown>>({
renderSubRow,
getRowCanExpand = () => true,
expandOnRowClick,
...props
}: Props<D>) {
return (
<Datatable<D>
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
expandable
renderRow={(row, { key, className, role, style }) => (
getRowCanExpand={getRowCanExpand}
renderRow={(row) => (
<ExpandableDatatableTableRow<D>
key={key}
row={row}
className={className}
role={role}
style={style}
renderSubRow={renderSubRow}
expandOnClick={expandOnRowClick}
/>
)}
/>

View File

@ -1,37 +1,33 @@
import { CSSProperties, ReactNode } from 'react';
import { Row } from 'react-table';
import { ReactNode } from 'react';
import { Row } from '@tanstack/react-table';
import { TableRow } from './TableRow';
interface Props<D extends Record<string, unknown>> {
row: Row<D>;
className?: string;
role?: string;
style?: CSSProperties;
disableSelect?: boolean;
renderSubRow(row: Row<D>): ReactNode;
expandOnClick?: boolean;
}
export function ExpandableDatatableTableRow<D extends Record<string, unknown>>({
row,
className,
role,
style,
disableSelect,
renderSubRow,
expandOnClick,
}: Props<D>) {
const cells = row.getVisibleCells();
return (
<>
<TableRow<D>
cells={row.cells}
className={className}
role={role}
style={style}
cells={cells}
onClick={expandOnClick ? () => row.toggleExpanded() : undefined}
/>
{row.isExpanded && (
{row.getIsExpanded() && (
<tr>
{!disableSelect && <td />}
<td colSpan={disableSelect ? row.cells.length : row.cells.length - 1}>
<td colSpan={disableSelect ? cells.length : cells.length - 1}>
{renderSubRow(row)}
</td>
</tr>

View File

@ -1,9 +0,0 @@
.expand-button {
background: none;
color: inherit;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
}

View File

@ -1,32 +0,0 @@
import { PropsWithChildren } from 'react';
import { Row } from 'react-table';
import { ChevronDown, ChevronRight } from 'lucide-react';
import { Icon } from '@@/Icon';
import styles from './ExpandingCell.module.css';
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
row: Row<D>;
showExpandArrow: boolean;
}
export function ExpandingCell<
D extends Record<string, unknown> = Record<string, unknown>
>({ row, showExpandArrow, children }: PropsWithChildren<Props<D>>) {
return (
<>
{showExpandArrow && (
<button type="button" className={styles.expandButton}>
<Icon
// eslint-disable-next-line react/jsx-props-no-spreading
{...row.getToggleRowExpandedProps()}
icon={row.isExpanded ? ChevronDown : ChevronRight}
className="mr-1"
/>
</button>
)}
{children}
</>
);
}

View File

@ -1,13 +1,11 @@
import clsx from 'clsx';
import { useMemo } from 'react';
import { Menu, MenuButton, MenuPopover } from '@reach/menu-button';
import { ColumnInstance } from 'react-table';
import { Column } from '@tanstack/react-table';
import { Check, Filter } from 'lucide-react';
import { Icon } from '@@/Icon';
export const DefaultFilter = filterHOC('Filter by state');
interface MultipleSelectionFilterProps {
options: string[];
value: string[];
@ -28,12 +26,12 @@ export function MultipleSelectionFilter({
<div>
<Menu>
<MenuButton
className={clsx('table-filter flex items-center', {
'filter-active': enabled,
})}
className={clsx('table-filter', { 'filter-active': enabled })}
>
Filter
<Icon icon={enabled ? Check : Filter} className="!ml-1" />
<div className="flex items-center gap-1">
Filter
<Icon icon={enabled ? Check : Filter} />
</div>
</MenuButton>
<MenuPopover className="dropdown-menu">
<div className="tableMenu">
@ -70,27 +68,54 @@ export function MultipleSelectionFilter({
}
}
export function filterHOC(menuTitle: string) {
export function filterHOC<TData extends Record<string, unknown>>(
menuTitle: string
) {
return function Filter({
column: { filterValue, setFilter, preFilteredRows, id },
column: { getFilterValue, setFilterValue, getFacetedRowModel, id },
}: {
column: ColumnInstance;
column: Column<TData>;
}) {
const { flatRows } = getFacetedRowModel();
const options = useMemo(() => {
const options = new Set<string>();
preFilteredRows.forEach((row) => {
options.add(row.values[id]);
flatRows.forEach(({ getValue }) => {
const value = getValue<string>(id);
options.add(value);
});
return Array.from(options);
}, [id, preFilteredRows]);
}, [flatRows, id]);
const value = getFilterValue();
const valueAsArray = getValueAsArrayOfStrings(value);
return (
<MultipleSelectionFilter
options={options}
filterKey={id}
value={filterValue}
onChange={setFilter}
value={valueAsArray}
onChange={setFilterValue}
menuTitle={menuTitle}
/>
);
};
}
function getValueAsArrayOfStrings(value: unknown): string[] {
if (Array.isArray(value)) {
return value;
}
if (!value || (typeof value !== 'string' && typeof value !== 'number')) {
return [];
}
if (typeof value === 'number') {
return [value.toString()];
}
return [value];
}

View File

@ -1,30 +1,36 @@
import { CellProps, Column } from 'react-table';
import { ColumnDef, CellContext } from '@tanstack/react-table';
import { Link } from '@@/Link';
export function buildNameColumn<T extends Record<string, unknown>>(
nameKey: string,
nameKey: keyof T,
idKey: string,
path: string
) {
const name: Column<T> = {
Header: 'Name',
accessor: (row) => row[nameKey],
): ColumnDef<T> {
const cell = createCell<T>();
return {
header: 'Name',
accessorKey: nameKey,
id: 'name',
Cell: NameCell,
disableFilters: true,
Filter: () => null,
canHide: false,
sortType: 'string',
cell,
enableSorting: true,
sortingFn: 'text',
};
return name;
function createCell<T extends Record<string, unknown>>() {
return function NameCell({ renderValue, row }: CellContext<T, unknown>) {
const name = renderValue() || '';
function NameCell({ value: name, row }: CellProps<T, string>) {
return (
<Link to={path} params={{ id: row.original[idKey] }} title={name}>
{name}
</Link>
);
if (typeof name !== 'string') {
return null;
}
return (
<Link to={path} params={{ id: row.original[idKey] }} title={name}>
{name}
</Link>
);
};
}
}

View File

@ -1,27 +1,28 @@
import {
useTable,
useFilters,
useSortBy,
Column,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
TableOptions,
TableState,
usePagination,
} from 'react-table';
useReactTable,
} from '@tanstack/react-table';
import { defaultGetRowId } from './defaultGetRowId';
import { Table } from './Table';
import { multiple } from './filter-types';
import { NestedTable } from './NestedTable';
import { DatatableContent } from './DatatableContent';
import { defaultGetRowId } from './defaultGetRowId';
import { BasicTableSettings } from './types';
interface Props<D extends Record<string, unknown>> {
dataset: D[];
columns: readonly Column<D>[];
columns: TableOptions<D>['columns'];
getRowId?(row: D): string;
emptyContentLabel?: string;
initialTableState?: Partial<TableState<D>>;
initialTableState?: Partial<TableState>;
isLoading?: boolean;
defaultSortBy?: string;
initialSortBy?: BasicTableSettings['sortBy'];
}
export function NestedDatatable<D extends Record<string, unknown>>({
@ -31,25 +32,26 @@ export function NestedDatatable<D extends Record<string, unknown>>({
emptyContentLabel,
initialTableState = {},
isLoading,
defaultSortBy,
initialSortBy,
}: Props<D>) {
const tableInstance = useTable<D>(
{
defaultCanFilter: false,
columns,
data: dataset,
filterTypes: { multiple },
initialState: {
sortBy: defaultSortBy ? [{ id: defaultSortBy, desc: true }] : [],
...initialTableState,
},
autoResetSelectedRows: false,
getRowId,
const tableInstance = useReactTable<D>({
columns,
data: dataset,
initialState: {
sorting: initialSortBy ? [initialSortBy] : [],
...initialTableState,
},
useFilters,
useSortBy,
usePagination
);
defaultColumn: {
enableColumnFilter: false,
enableHiding: false,
},
getRowId,
autoResetExpanded: false,
getCoreRowModel: getCoreRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getSortedRowModel: getSortedRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<NestedTable>
@ -58,15 +60,7 @@ export function NestedDatatable<D extends Record<string, unknown>>({
tableInstance={tableInstance}
isLoading={isLoading}
emptyContentLabel={emptyContentLabel}
renderRow={(row, { key, className, role, style }) => (
<Table.Row<D>
cells={row.cells}
key={key}
className={className}
role={role}
style={style}
/>
)}
renderRow={(row) => <Table.Row<D> cells={row.getVisibleCells()} />}
/>
</Table.Container>
</NestedTable>

View File

@ -1,3 +1,7 @@
.inner-datatable .widget {
border: 0 !important;
}
.inner-datatable {
@apply rounded-md border border-solid border-gray-5 th-dark:border-gray-9;
overflow: hidden;

View File

@ -1,24 +1,22 @@
import clsx from 'clsx';
import { PropsWithChildren } from 'react';
import { TableProps } from 'react-table';
import { TableContainer } from './TableContainer';
import { TableActions } from './TableActions';
import { TableFooter } from './TableFooter';
import { TableTitleActions } from './TableTitleActions';
import { TableContent } from './TableContent';
import { TableHeaderCell } from './TableHeaderCell';
import { TableSettingsMenu } from './TableSettingsMenu';
import { TableTitle } from './TableTitle';
import { TableContent } from './TableContent';
import { TableHeaderCell } from './TableHeaderCell';
import { TableHeaderRow } from './TableHeaderRow';
import { TableRow } from './TableRow';
import { TableFooter } from './TableFooter';
function MainComponent({
children,
className,
role,
style,
}: PropsWithChildren<TableProps>) {
interface Props {
className?: string;
}
function MainComponent({ children, className }: PropsWithChildren<Props>) {
return (
<div className="table-responsive">
<table
@ -26,8 +24,6 @@ function MainComponent({
'table-hover table-filters nowrap-cells table',
className
)}
role={role}
style={style}
>
{children}
</table>

View File

@ -1,12 +1,11 @@
import { PropsWithChildren } from 'react';
import { Row, TableRowProps } from 'react-table';
import { Fragment, PropsWithChildren } from 'react';
import { Row } from '@tanstack/react-table';
interface Props<T extends Record<string, unknown> = Record<string, unknown>> {
isLoading?: boolean;
rows: Row<T>[];
emptyContent?: string;
prepareRow(row: Row<T>): void;
renderRow(row: Row<T>, rowProps: TableRowProps): React.ReactNode;
renderRow(row: Row<T>): React.ReactNode;
}
export function TableContent<
@ -15,7 +14,6 @@ export function TableContent<
isLoading = false,
rows,
emptyContent = 'No items available',
prepareRow,
renderRow,
}: Props<T>) {
if (isLoading) {
@ -28,11 +26,9 @@ export function TableContent<
return (
<>
{rows.map((row) => {
prepareRow(row);
const { key, className, role, style } = row.getRowProps();
return renderRow(row, { key, className, role, style });
})}
{rows.map((row) => (
<Fragment key={row.id}>{renderRow(row)}</Fragment>
))}
</>
);
}

View File

@ -1,33 +1,33 @@
import clsx from 'clsx';
import { PropsWithChildren, ReactNode } from 'react';
import { TableHeaderProps } from 'react-table';
import { CSSProperties, PropsWithChildren, ReactNode } from 'react';
import { TableHeaderSortIcons } from './TableHeaderSortIcons';
import styles from './TableHeaderCell.module.css';
import { TableHeaderSortIcons } from './TableHeaderSortIcons';
interface Props {
canFilter: boolean;
canSort: boolean;
headerProps: TableHeaderProps;
isSorted: boolean;
isSortedDesc?: boolean;
onSortClick: (desc: boolean) => void;
render: () => ReactNode;
renderFilter: () => ReactNode;
renderFilter?: () => ReactNode;
className?: string;
style?: CSSProperties;
}
export function TableHeaderCell({
headerProps: { className, role, style },
canSort,
render,
onSortClick,
isSorted,
isSortedDesc = true,
canFilter,
renderFilter,
className,
style,
}: Props) {
return (
<th role={role} style={style} className={className}>
<th style={style} className={className}>
<div className="flex h-full flex-row flex-nowrap items-center gap-1">
<SortWrapper
canSort={canSort}
@ -37,7 +37,7 @@ export function TableHeaderCell({
>
{render()}
</SortWrapper>
{canFilter ? renderFilter() : null}
{renderFilter ? renderFilter() : null}
</div>
</th>
);
@ -76,7 +76,6 @@ function SortWrapper({
<TableHeaderSortIcons
sorted={isSorted}
descending={isSorted && !!isSortedDesc}
className="ml-1"
/>
</div>
</button>

View File

@ -1,48 +1,58 @@
import { HeaderGroup, TableHeaderProps } from 'react-table';
import { Header, flexRender } from '@tanstack/react-table';
import { filterHOC } from './Filter';
import { TableHeaderCell } from './TableHeaderCell';
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
headers: HeaderGroup<D>[];
headers: Header<D, unknown>[];
onSortChange?(colId: string, desc: boolean): void;
}
export function TableHeaderRow<
D extends Record<string, unknown> = Record<string, unknown>
>({
headers,
onSortChange,
className,
role,
style,
}: Props<D> & TableHeaderProps) {
>({ headers, onSortChange }: Props<D>) {
return (
<tr className={className} role={role} style={style}>
{headers.map((column) => (
<TableHeaderCell
headerProps={{
...column.getHeaderProps({
className: column.className,
style: {
width: column.disableResizing ? column.width : '',
},
}),
}}
key={column.id}
canSort={column.canSort}
onSortClick={(desc) => {
column.toggleSortBy(desc);
if (onSortChange) {
onSortChange(column.id, desc);
<tr>
{headers.map((header) => {
const sortDirection = header.column.getIsSorted();
const {
meta: { className, width } = { className: '', width: undefined },
} = header.column.columnDef;
return (
<TableHeaderCell
className={className}
style={{
width,
}}
key={header.id}
canSort={header.column.getCanSort()}
onSortClick={(desc) => {
header.column.toggleSorting(desc);
if (onSortChange) {
onSortChange(header.id, desc);
}
}}
isSorted={!!sortDirection}
isSortedDesc={sortDirection ? sortDirection === 'desc' : false}
render={() =>
flexRender(header.column.columnDef.header, header.getContext())
}
}}
isSorted={column.isSorted}
isSortedDesc={column.isSortedDesc}
render={() => column.render('Header')}
canFilter={!column.disableFilters}
renderFilter={() => column.render('Filter')}
/>
))}
renderFilter={
header.column.getCanFilter()
? () =>
flexRender(
header.column.columnDef.meta?.filter ||
filterHOC('Filter'),
{
column: header.column,
}
)
: undefined
}
/>
);
})}
</tr>
);
}

View File

@ -1,31 +1,25 @@
import { Cell, TableRowProps } from 'react-table';
import { Cell, flexRender } from '@tanstack/react-table';
import clsx from 'clsx';
interface Props<D extends Record<string, unknown> = Record<string, unknown>>
extends Omit<TableRowProps, 'key'> {
cells: Cell<D>[];
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
cells: Cell<D, unknown>[];
className?: string;
onClick?: () => void;
}
export function TableRow<
D extends Record<string, unknown> = Record<string, unknown>
>({ cells, className, role, style }: Props<D>) {
>({ cells, className, onClick }: Props<D>) {
return (
<tr className={className} role={role} style={style}>
{cells.map((cell) => {
const cellProps = cell.getCellProps({
className: cell.className,
});
return (
<td
className={cellProps.className}
role={cellProps.role}
style={cellProps.style}
key={cellProps.key}
>
{cell.render('Cell')}
</td>
);
})}
<tr
className={clsx(className, { 'cursor-pointer': !!onClick })}
onClick={onClick}
>
{cells.map((cell) => (
<td key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
);
}

View File

@ -1,3 +0,0 @@
export function emptyPlugin() {}
emptyPlugin.pluginName = 'emptyPlugin';

View File

@ -1,49 +1,45 @@
import { ChevronDown, ChevronUp } from 'lucide-react';
import { CellProps, Column, HeaderProps } from 'react-table';
import { ColumnDef } from '@tanstack/react-table';
import { Button } from '@@/buttons';
export function buildExpandColumn<T extends Record<string, unknown>>(
isExpandable: (item: T) => boolean
): Column<T> {
export function buildExpandColumn<
T extends Record<string, unknown>
>(): ColumnDef<T> {
return {
id: 'expand',
Header: ({
filteredFlatRows,
getToggleAllRowsExpandedProps,
isAllRowsExpanded,
}: HeaderProps<T>) => {
const hasExpandableItems = filteredFlatRows.some((item) =>
isExpandable(item.original)
);
header: ({ table }) => {
const hasExpandableItems = table.getExpandedRowModel().rows.length > 0;
return (
hasExpandableItems && (
<Button
// eslint-disable-next-line react/jsx-props-no-spreading
{...getToggleAllRowsExpandedProps()}
onClick={table.getToggleAllRowsExpandedHandler()}
color="none"
icon={isAllRowsExpanded ? ChevronDown : ChevronUp}
icon={table.getIsAllRowsExpanded() ? ChevronDown : ChevronUp}
/>
)
);
},
Cell: ({ row }: CellProps<T>) => (
<div className="vertical-center">
{isExpandable(row.original) && (
<Button
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...row.getToggleRowExpandedProps()}
color="none"
icon={row.isExpanded ? ChevronDown : ChevronUp}
/>
)}
</div>
),
disableFilters: true,
Filter: () => null,
canHide: false,
width: 30,
disableResizing: true,
cell: ({ row }) =>
row.getCanExpand() && (
<Button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
row.toggleExpanded();
}}
color="none"
icon={row.getIsExpanded() ? ChevronDown : ChevronUp}
/>
),
enableColumnFilter: false,
enableGlobalFilter: false,
enableHiding: false,
meta: {
width: 40,
},
};
}

View File

@ -1,14 +1,13 @@
import { Row } from 'react-table';
import { Row } from '@tanstack/react-table';
export function multiple<
D extends Record<string, unknown> = Record<string, unknown>
>(rows: Row<D>[], columnIds: string[], filterValue: string[] = []) {
if (filterValue.length === 0 || columnIds.length === 0) {
return rows;
>({ getValue }: Row<D>, columnId: string, filterValue: string[]): boolean {
if (filterValue.length === 0) {
return true;
}
return rows.filter((row) => {
const value = row.values[columnIds[0]];
return filterValue.includes(value);
});
const value = getValue(columnId) as string;
return filterValue.includes(value);
}

View File

@ -0,0 +1,67 @@
import { ColumnDef, Row } from '@tanstack/react-table';
import { Checkbox } from '@@/form-components/Checkbox';
export function createSelectColumn<T>(): ColumnDef<T> {
let lastSelectedId = '';
return {
id: 'select',
header: ({ table }) => (
<Checkbox
id="select-all"
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row, table }) => (
<Checkbox
id={`select-row-${row.id}`}
checked={row.getIsSelected()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
onClick={(e) => {
if (e.shiftKey) {
const { rows, rowsById } = table.getRowModel();
const rowsToToggle = getRowRange(rows, row.id, lastSelectedId);
const isLastSelected = rowsById[lastSelectedId].getIsSelected();
rowsToToggle.forEach((row) => row.toggleSelected(isLastSelected));
}
lastSelectedId = row.id;
}}
/>
),
meta: {
width: 50,
},
};
}
function getRowRange<T>(rows: Array<Row<T>>, idA: string, idB: string) {
const range: Array<Row<T>> = [];
let foundStart = false;
let foundEnd = false;
for (let index = 0; index < rows.length; index += 1) {
const row = rows[index];
if (row.id === idA || row.id === idB) {
if (foundStart) {
foundEnd = true;
}
if (!foundStart) {
foundStart = true;
}
}
if (foundStart) {
range.push(row);
}
if (foundEnd) {
break;
}
}
return range;
}

View File

@ -13,7 +13,7 @@ export function useGoToHighlightedRow<T extends { id: string }>(
handlePageChangeRef.current = goToPage;
});
const highlightedItemIdRef = useRef(highlightedItemId);
const highlightedItemIdRef = useRef<string>();
useEffect(() => {
if (

View File

@ -1,481 +0,0 @@
/* eslint no-param-reassign: ["error", { "props": false }] */
import { ChangeEvent, useCallback, useMemo } from 'react';
import {
actions,
makePropGetter,
ensurePluginOrder,
useGetLatest,
useMountedLayoutEffect,
Hooks,
TableInstance,
TableState,
ActionType,
ReducerTableState,
IdType,
Row,
PropGetter,
TableToggleRowsSelectedProps,
TableToggleAllRowsSelectedProps,
} from 'react-table';
type DefaultType = Record<string, unknown>;
interface UseRowSelectTableInstance<D extends DefaultType = DefaultType>
extends TableInstance<D> {
isAllRowSelected: boolean;
selectSubRows: boolean;
getSubRows(row: Row<D>): Row<D>[];
isRowSelectable?(row: Row<D>): boolean;
}
const pluginName = 'useRowSelect';
// Actions
actions.resetSelectedRows = 'resetSelectedRows';
actions.toggleAllRowsSelected = 'toggleAllRowsSelected';
actions.toggleRowSelected = 'toggleRowSelected';
actions.toggleAllPageRowsSelected = 'toggleAllPageRowsSelected';
export function useRowSelect<D extends DefaultType>(hooks: Hooks<D>) {
hooks.getToggleRowSelectedProps = [
defaultGetToggleRowSelectedProps as PropGetter<
D,
TableToggleRowsSelectedProps
>,
];
hooks.getToggleAllRowsSelectedProps = [
defaultGetToggleAllRowsSelectedProps as PropGetter<
D,
TableToggleAllRowsSelectedProps
>,
];
hooks.getToggleAllPageRowsSelectedProps = [
defaultGetToggleAllPageRowsSelectedProps as PropGetter<
D,
TableToggleAllRowsSelectedProps
>,
];
hooks.stateReducers.push(
reducer as (
newState: TableState<D>,
action: ActionType,
previousState?: TableState<D>,
instance?: TableInstance<D>
) => ReducerTableState<D> | undefined
);
hooks.useInstance.push(useInstance as (instance: TableInstance<D>) => void);
hooks.prepareRow.push(prepareRow);
}
useRowSelect.pluginName = pluginName;
function defaultGetToggleRowSelectedProps<D extends DefaultType>(
props: D,
{ instance, row }: { instance: UseRowSelectTableInstance<D>; row: Row<D> }
) {
const {
manualRowSelectedKey = 'isSelected',
isRowSelectable = defaultIsRowSelectable,
} = instance;
let checked = false;
if (row.original && row.original[manualRowSelectedKey]) {
checked = true;
} else {
checked = row.isSelected;
}
return [
props,
{
onChange: (e: ChangeEvent<HTMLInputElement>) => {
row.toggleRowSelected(e.target.checked);
},
style: {
cursor: 'pointer',
},
checked,
title: 'Toggle Row Selected',
indeterminate: row.isSomeSelected,
disabled: !isRowSelectable(row),
},
];
}
function defaultGetToggleAllRowsSelectedProps<D extends DefaultType>(
props: D,
{ instance }: { instance: UseRowSelectTableInstance<D> }
) {
return [
props,
{
onChange: (e: ChangeEvent<HTMLInputElement>) => {
instance.toggleAllRowsSelected(e.target.checked);
},
style: {
cursor: 'pointer',
},
checked: instance.isAllRowsSelected,
title: 'Toggle All Rows Selected',
indeterminate: Boolean(
!instance.isAllRowsSelected &&
Object.keys(instance.state.selectedRowIds).length
),
},
];
}
function defaultGetToggleAllPageRowsSelectedProps<D extends DefaultType>(
props: D,
{ instance }: { instance: UseRowSelectTableInstance<D> }
) {
return [
props,
{
onChange(e: ChangeEvent<HTMLInputElement>) {
instance.toggleAllPageRowsSelected(e.target.checked);
},
style: {
cursor: 'pointer',
},
checked: instance.isAllPageRowsSelected,
title: 'Toggle All Current Page Rows Selected',
indeterminate: Boolean(
!instance.isAllPageRowsSelected &&
instance.page.some(({ id }) => instance.state.selectedRowIds[id])
),
},
];
}
function reducer<D extends Record<string, unknown>>(
state: TableState<D>,
action: ActionType,
_previousState?: TableState<D>,
instance?: UseRowSelectTableInstance<D>
) {
if (action.type === actions.init) {
return {
...state,
selectedRowIds: <Record<IdType<D>, boolean>>{},
};
}
if (action.type === actions.resetSelectedRows) {
return {
...state,
selectedRowIds: instance?.initialState.selectedRowIds || {},
};
}
if (action.type === actions.toggleAllRowsSelected) {
const { value: setSelected } = action;
if (!instance) {
return state;
}
const {
isAllRowsSelected,
rowsById,
nonGroupedRowsById = rowsById,
isRowSelectable = defaultIsRowSelectable,
} = instance;
const selectAll =
typeof setSelected !== 'undefined' ? setSelected : !isAllRowsSelected;
// Only remove/add the rows that are visible on the screen
// Leave all the other rows that are selected alone.
const selectedRowIds = { ...state.selectedRowIds };
Object.keys(nonGroupedRowsById).forEach((rowId: IdType<D>) => {
if (selectAll) {
const row = rowsById[rowId];
if (isRowSelectable(row)) {
selectedRowIds[rowId] = true;
}
} else {
delete selectedRowIds[rowId];
}
});
return {
...state,
selectedRowIds,
};
}
if (action.type === actions.toggleRowSelected) {
if (!instance) {
return state;
}
const { id, value: setSelected } = action;
const {
rowsById,
selectSubRows = true,
getSubRows,
isRowSelectable = defaultIsRowSelectable,
} = instance;
const isSelected = state.selectedRowIds[id];
const shouldExist =
typeof setSelected !== 'undefined' ? setSelected : !isSelected;
if (isSelected === shouldExist) {
return state;
}
const newSelectedRowIds = { ...state.selectedRowIds };
// eslint-disable-next-line no-inner-declarations
function handleRowById(id: IdType<D>) {
const row = rowsById[id];
if (!isRowSelectable(row)) {
return;
}
if (!row.isGrouped) {
if (shouldExist) {
newSelectedRowIds[id] = true;
} else {
delete newSelectedRowIds[id];
}
}
if (selectSubRows && getSubRows(row)) {
getSubRows(row).forEach((row) => handleRowById(row.id));
}
}
handleRowById(id);
return {
...state,
selectedRowIds: newSelectedRowIds,
};
}
if (action.type === actions.toggleAllPageRowsSelected) {
if (!instance) {
return state;
}
const { value: setSelected } = action;
const {
page,
rowsById,
selectSubRows = true,
isAllPageRowsSelected,
getSubRows,
} = instance;
const selectAll =
typeof setSelected !== 'undefined' ? setSelected : !isAllPageRowsSelected;
const newSelectedRowIds = { ...state.selectedRowIds };
// eslint-disable-next-line no-inner-declarations
function handleRowById(id: IdType<D>) {
const row = rowsById[id];
if (!row.isGrouped) {
if (selectAll) {
newSelectedRowIds[id] = true;
} else {
delete newSelectedRowIds[id];
}
}
if (selectSubRows && getSubRows(row)) {
getSubRows(row).forEach((row) => handleRowById(row.id));
}
}
page.forEach((row) => handleRowById(row.id));
return {
...state,
selectedRowIds: newSelectedRowIds,
};
}
return state;
}
function useInstance<D extends Record<string, unknown>>(
instance: UseRowSelectTableInstance<D>
) {
const {
data,
rows,
getHooks,
plugins,
rowsById,
nonGroupedRowsById = rowsById,
autoResetSelectedRows = true,
state: { selectedRowIds },
selectSubRows = true,
dispatch,
page,
getSubRows,
isRowSelectable = defaultIsRowSelectable,
} = instance;
ensurePluginOrder(
plugins,
['useFilters', 'useGroupBy', 'useSortBy', 'useExpanded', 'usePagination'],
'useRowSelect'
);
const selectedFlatRows = useMemo(() => {
const selectedFlatRows = <Array<Row<D>>>[];
rows.forEach((row) => {
const isSelected = selectSubRows
? getRowIsSelected(row, selectedRowIds, getSubRows)
: !!selectedRowIds[row.id];
row.isSelected = !!isSelected;
row.isSomeSelected = isSelected === null;
if (isSelected) {
selectedFlatRows.push(row);
}
});
return selectedFlatRows;
}, [rows, selectSubRows, selectedRowIds, getSubRows]);
let isAllRowsSelected = Boolean(
Object.keys(nonGroupedRowsById).length && Object.keys(selectedRowIds).length
);
let isAllPageRowsSelected = isAllRowsSelected;
if (isAllRowsSelected) {
if (
Object.keys(nonGroupedRowsById).some((id) => {
const row = rowsById[id];
return !selectedRowIds[id] && isRowSelectable(row);
})
) {
isAllRowsSelected = false;
}
}
if (!isAllRowsSelected) {
if (
page &&
page.length &&
page.some(({ id }) => {
const row = rowsById[id];
return !selectedRowIds[id] && isRowSelectable(row);
})
) {
isAllPageRowsSelected = false;
}
}
const getAutoResetSelectedRows = useGetLatest(autoResetSelectedRows);
useMountedLayoutEffect(() => {
if (getAutoResetSelectedRows()) {
dispatch({ type: actions.resetSelectedRows });
}
}, [dispatch, data]);
const toggleAllRowsSelected = useCallback(
(value) => dispatch({ type: actions.toggleAllRowsSelected, value }),
[dispatch]
);
const toggleAllPageRowsSelected = useCallback(
(value) => dispatch({ type: actions.toggleAllPageRowsSelected, value }),
[dispatch]
);
const toggleRowSelected = useCallback(
(id, value) => dispatch({ type: actions.toggleRowSelected, id, value }),
[dispatch]
);
const getInstance = useGetLatest(instance);
const getToggleAllRowsSelectedProps = makePropGetter(
getHooks().getToggleAllRowsSelectedProps,
{ instance: getInstance() }
);
const getToggleAllPageRowsSelectedProps = makePropGetter(
getHooks().getToggleAllPageRowsSelectedProps,
{ instance: getInstance() }
);
Object.assign(instance, {
selectedFlatRows,
isAllRowsSelected,
isAllPageRowsSelected,
toggleRowSelected,
toggleAllRowsSelected,
getToggleAllRowsSelectedProps,
getToggleAllPageRowsSelectedProps,
toggleAllPageRowsSelected,
});
}
function prepareRow<D extends Record<string, unknown>>(
row: Row<D>,
{ instance }: { instance: TableInstance<D> }
) {
row.toggleRowSelected = (set) => instance.toggleRowSelected(row.id, set);
row.getToggleRowSelectedProps = makePropGetter(
instance.getHooks().getToggleRowSelectedProps,
{ instance, row }
);
}
function getRowIsSelected<D extends Record<string, unknown>>(
row: Row<D>,
selectedRowIds: Record<IdType<D>, boolean>,
getSubRows: (row: Row<D>) => Array<Row<D>>
) {
if (selectedRowIds[row.id]) {
return true;
}
const subRows = getSubRows(row);
if (subRows && subRows.length) {
let allChildrenSelected = true;
let someSelected = false;
subRows.forEach((subRow) => {
// Bail out early if we know both of these
if (someSelected && !allChildrenSelected) {
return;
}
if (getRowIsSelected(subRow, selectedRowIds, getSubRows)) {
someSelected = true;
} else {
allChildrenSelected = false;
}
});
if (allChildrenSelected) {
return true;
}
return someSelected ? null : false;
}
return false;
}
function defaultIsRowSelectable<D extends DefaultType>(row: Row<D>) {
return !row.original.disabled;
}

View File

@ -0,0 +1,29 @@
import { useMemo } from 'react';
import { useStore } from 'zustand';
import { useSearchBarState } from './SearchBar';
import { BasicTableSettings, createPersistedStore } from './types';
/** 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 function useTableState<
TSettings extends BasicTableSettings = BasicTableSettings
>(store: ReturnType<Wrapper<TSettings>['wrapped']>, storageKey: string) {
const settings = useStore(store);
const [search, setSearch] = useSearchBarState(storageKey);
return useMemo(
() => ({ ...settings, setSearch, search }),
[settings, search, setSearch]
);
}

View File

@ -0,0 +1,38 @@
import { CellContext, ColumnDef } from '@tanstack/react-table';
import { ownershipIcon } from '@/portainer/filters/filters';
import { ResourceControlOwnership } from '@/react/portainer/access-control/types';
import { Icon } from '@@/Icon';
export interface IResource {
ResourceControl?: {
Ownership: ResourceControlOwnership;
};
}
export function createOwnershipColumn<D extends IResource>(): ColumnDef<
D,
ResourceControlOwnership
> {
return {
accessorFn: (row) =>
row.ResourceControl?.Ownership || ResourceControlOwnership.ADMINISTRATORS,
header: 'Ownership',
id: 'ownership',
cell: OwnershipCell,
};
function OwnershipCell({
getValue,
}: CellContext<D, ResourceControlOwnership>) {
const value = getValue();
return (
<span className="flex items-center gap-2">
<Icon icon={ownershipIcon(value)} className="space-right" />
{value}
</span>
);
}
}

View File

@ -1,19 +1,17 @@
import _ from 'lodash';
import { useStore } from 'zustand';
import { Box } from 'lucide-react';
import { Environment } from '@/react/portainer/environments/types';
import type { DockerContainer } from '@/react/docker/containers/types';
import { useShowGPUsColumn } from '@/react/docker/containers/utils';
import { TableSettingsMenu, Datatable } from '@@/datatables';
import { Table, Datatable } from '@@/datatables';
import {
buildAction,
QuickActionsSettings,
} from '@@/datatables/QuickActionsSettings';
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
import { useTableState } from '@@/datatables/useTableState';
import { useContainers } from '../../queries/containers';
@ -43,20 +41,15 @@ export function ContainersDatatable({
isHostColumnVisible,
environment,
}: Props) {
const settings = useStore(settingsStore);
const isGPUsColumnVisible = useShowGPUsColumn(environment.Id);
const columns = useColumns(isHostColumnVisible, isGPUsColumnVisible);
const hidableColumns = _.compact(
columns.filter((col) => col.canHide).map((col) => col.id)
);
const [search, setSearch] = useSearchBarState(storageKey);
const tableState = useTableState(settingsStore, storageKey);
const containersQuery = useContainers(
environment.Id,
true,
undefined,
settings.autoRefreshRate * 1000
tableState.autoRefreshRate * 1000
);
return (
@ -65,12 +58,7 @@ export function ContainersDatatable({
<Datatable
titleIcon={Box}
title="Containers"
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
settingsManager={tableState}
columns={columns}
renderTableActions={(selectedRows) => (
<ContainersDatatableActions
@ -81,30 +69,38 @@ export function ContainersDatatable({
)}
isLoading={containersQuery.isLoading}
isRowSelectable={(row) => !row.original.IsPortainer}
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
initialTableState={{
columnVisibility: Object.fromEntries(
tableState.hiddenColumns.map((col) => [col, false])
),
}}
renderTableSettings={(tableInstance) => {
const columnsToHide = tableInstance.allColumns.filter(
(colInstance) => hidableColumns?.includes(colInstance.id)
);
const columnsToHide = tableInstance
.getAllColumns()
.filter((col) => col.getCanHide());
return (
<>
<ColumnVisibilityMenu<DockerContainer>
columns={columnsToHide}
onChange={(hiddenColumns) => {
settings.setHiddenColumns(hiddenColumns);
tableInstance.setHiddenColumns(hiddenColumns);
tableState.setHiddenColumns(hiddenColumns);
tableInstance.setColumnVisibility(
Object.fromEntries(
hiddenColumns.map((col) => [col, false])
)
);
}}
value={settings.hiddenColumns}
value={tableState.hiddenColumns}
/>
<TableSettingsMenu
<Table.SettingsMenu
quickActions={<QuickActionsSettings actions={actions} />}
>
<ContainersDatatableSettings
isRefreshVisible
settings={settings}
settings={tableState}
/>
</TableSettingsMenu>
</Table.SettingsMenu>
</>
);
}}

View File

@ -1,14 +1,9 @@
import { Column } from 'react-table';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import type { DockerContainer } from '@/react/docker/containers/types';
export const created: Column<DockerContainer> = {
Header: 'Created',
accessor: 'Created',
import { columnHelper } from './helper';
export const created = columnHelper.accessor('Created', {
header: 'Created',
id: 'created',
Cell: ({ value }) => isoDateFromTimestamp(value),
disableFilters: true,
canHide: true,
Filter: () => null,
};
cell: ({ getValue }) => isoDateFromTimestamp(getValue()),
});

View File

@ -1,21 +1,20 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import type { DockerContainer } from '@/react/docker/containers/types';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useContainerGpus } from '@/react/docker/containers/queries/gpus';
export const gpus: Column<DockerContainer> = {
Header: 'GPUs',
import { columnHelper } from './helper';
export const gpus = columnHelper.display({
header: 'GPUs',
id: 'gpus',
disableFilters: true,
canHide: true,
Filter: () => null,
Cell: GpusCell,
};
cell: GpusCell,
});
function GpusCell({
row: { original: container },
}: CellProps<DockerContainer>) {
}: CellContext<DockerContainer, unknown>) {
const containerId = container.Id;
const environmentId = useEnvironmentId();
const gpusQuery = useContainerGpus(environmentId, containerId);

View File

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { DockerContainer } from '../../../types';
export const columnHelper = createColumnHelper<DockerContainer>();

View File

@ -1,13 +1,6 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import type { DockerContainer } from '@/react/docker/containers/types';
export const host: Column<DockerContainer> = {
Header: 'Host',
accessor: (row) => row.NodeName || '-',
export const host = columnHelper.accessor((row) => row.NodeName || '-', {
header: 'Host',
id: 'host',
disableFilters: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};
});

View File

@ -1,24 +1,18 @@
import { Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { useSref } from '@uirouter/react';
import type { DockerContainer } from '@/react/docker/containers/types';
export const image: Column<DockerContainer> = {
Header: 'Image',
accessor: 'Image',
import { columnHelper } from './helper';
export const image = columnHelper.accessor('Image', {
header: 'Image',
id: 'image',
disableFilters: true,
Cell: ImageCell,
canHide: true,
sortType: 'string',
Filter: () => null,
};
cell: ImageCell,
});
interface Props {
value: string;
}
function ImageCell({ value: imageName }: Props) {
function ImageCell({ getValue }: CellContext<DockerContainer, string>) {
const imageName = getValue();
const linkProps = useSref('docker.images.image', { id: imageName });
const shortImageName = trimSHASum(imageName);

View File

@ -1,12 +1,14 @@
import _ from 'lodash';
import { useMemo } from 'react';
import { createOwnershipColumn } from '@/react/docker/components/datatable-helpers/createOwnershipColumn';
import { DockerContainer } from '@/react/docker/containers/types';
import { created } from './created';
import { host } from './host';
import { image } from './image';
import { ip } from './ip';
import { name } from './name';
import { ownership } from './ownership';
import { ports } from './ports';
import { quickActions } from './quick-actions';
import { stack } from './stack';
@ -15,7 +17,7 @@ import { gpus } from './gpus';
export function useColumns(
isHostColumnVisible: boolean,
isGPUsColumnVisible?: boolean
isGPUsColumnVisible: boolean | undefined
) {
return useMemo(
() =>
@ -30,7 +32,7 @@ export function useColumns(
isHostColumnVisible && host,
isGPUsColumnVisible && gpus,
ports,
ownership,
createOwnershipColumn<DockerContainer>(),
]),
[isHostColumnVisible, isGPUsColumnVisible]
);

View File

@ -1,12 +1,6 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import type { DockerContainer } from '@/react/docker/containers/types';
export const ip: Column<DockerContainer> = {
Header: 'IP Address',
accessor: (row) => row.IP || '-',
export const ip = columnHelper.accessor((row) => row.IP || '-', {
header: 'IP Address',
id: 'ip',
disableFilters: true,
canHide: true,
Filter: () => null,
};
});

View File

@ -1,4 +1,4 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import _ from 'lodash';
import { useSref } from '@uirouter/react';
@ -8,21 +8,20 @@ import { useTableSettings } from '@@/datatables/useTableSettings';
import { TableSettings } from '../types';
export const name: Column<DockerContainer> = {
Header: 'Name',
accessor: (row) => row.Names[0],
import { columnHelper } from './helper';
export const name = columnHelper.accessor((row) => row.Names[0], {
header: 'Name',
id: 'name',
Cell: NameCell,
disableFilters: true,
Filter: () => null,
canHide: true,
sortType: 'string',
};
cell: NameCell,
});
export function NameCell({
value: name,
getValue,
row: { original: container },
}: CellProps<DockerContainer>) {
}: CellContext<DockerContainer, string>) {
const name = getValue();
const linkProps = useSref('.container', {
id: container.Id,
nodeName: container.NodeName,

View File

@ -1,34 +0,0 @@
import { Column } from 'react-table';
import clsx from 'clsx';
import { ownershipIcon } from '@/portainer/filters/filters';
import type { DockerContainer } from '@/react/docker/containers/types';
import { ResourceControlOwnership } from '@/react/portainer/access-control/types';
export const ownership: Column<DockerContainer> = {
Header: 'Ownership',
id: 'ownership',
accessor: (row) =>
row.ResourceControl?.Ownership || ResourceControlOwnership.ADMINISTRATORS,
Cell: OwnershipCell,
disableFilters: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};
interface Props {
value: 'public' | 'private' | 'restricted' | 'administrators';
}
function OwnershipCell({ value }: Props) {
return (
<>
<i
className={clsx(ownershipIcon(value), 'space-right')}
aria-hidden="true"
/>
{value || ResourceControlOwnership.ADMINISTRATORS}
</>
);
}

View File

@ -1,6 +1,6 @@
import { Column } from 'react-table';
import _ from 'lodash';
import { ExternalLink } from 'lucide-react';
import { CellContext } from '@tanstack/react-table';
import type { DockerContainer, Port } from '@/react/docker/containers/types';
@ -8,22 +8,17 @@ import { Icon } from '@@/Icon';
import { useRowContext } from '../RowContext';
export const ports: Column<DockerContainer> = {
Header: 'Published Ports',
accessor: 'Ports',
import { columnHelper } from './helper';
export const ports = columnHelper.accessor('Ports', {
header: 'Published Ports',
id: 'ports',
Cell: PortsCell,
disableSortBy: true,
disableFilters: true,
canHide: true,
Filter: () => null,
};
cell: PortsCell,
});
interface Props {
value: Port[];
}
function PortsCell({ getValue }: CellContext<DockerContainer, Port[]>) {
const ports = getValue();
function PortsCell({ value: ports }: Props) {
const { environment } = useRowContext();
if (ports.length === 0) {

View File

@ -1,4 +1,4 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { useAuthorizations } from '@/react/hooks/useUser';
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
@ -8,20 +8,17 @@ import { useTableSettings } from '@@/datatables/useTableSettings';
import { TableSettings } from '../types';
export const quickActions: Column<DockerContainer> = {
Header: 'Quick Actions',
import { columnHelper } from './helper';
export const quickActions = columnHelper.display({
header: 'Quick Actions',
id: 'actions',
Cell: QuickActionsCell,
disableFilters: true,
disableSortBy: true,
canHide: true,
sortType: 'string',
Filter: () => null,
};
cell: QuickActionsCell,
});
function QuickActionsCell({
row: { original: container },
}: CellProps<DockerContainer>) {
}: CellContext<DockerContainer, unknown>) {
const settings = useTableSettings<TableSettings>();
const { hiddenQuickActions = [] } = settings;

View File

@ -1,13 +1,6 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import type { DockerContainer } from '@/react/docker/containers/types';
export const stack: Column<DockerContainer> = {
Header: 'Stack',
accessor: (row) => row.StackName || '-',
export const stack = columnHelper.accessor((row) => row.StackName || '-', {
header: 'Stack',
id: 'stack',
sortType: 'string',
disableFilters: true,
canHide: true,
Filter: () => null,
};
});

View File

@ -1,27 +1,32 @@
import { CellProps, Column } from 'react-table';
import clsx from 'clsx';
import { CellContext } from '@tanstack/react-table';
import {
type DockerContainer,
ContainerStatus,
} from '@/react/docker/containers/types';
import { DefaultFilter } from '@@/datatables/Filter';
import { filterHOC } from '@@/datatables/Filter';
import { multiple } from '@@/datatables/filter-types';
export const state: Column<DockerContainer> = {
Header: 'State',
accessor: 'Status',
import { columnHelper } from './helper';
export const state = columnHelper.accessor('Status', {
header: 'State',
id: 'state',
Cell: StatusCell,
sortType: 'string',
filter: 'multiple',
Filter: DefaultFilter,
canHide: true,
};
cell: StatusCell,
enableColumnFilter: true,
filterFn: multiple,
meta: {
filter: filterHOC('Filter by state'),
},
});
function StatusCell({
value: status,
}: CellProps<DockerContainer, ContainerStatus>) {
getValue,
}: CellContext<DockerContainer, ContainerStatus>) {
const status = getValue();
const hasHealthCheck = [
ContainerStatus.Starting,
ContainerStatus.Healthy,

View File

@ -9,7 +9,7 @@ import { QuickAction, TableSettings } from './types';
export const TRUNCATE_LENGTH = 32;
export function createStore(storageKey: string) {
return createPersistedStore<TableSettings>(storageKey, 'Name', (set) => ({
return createPersistedStore<TableSettings>(storageKey, 'name', (set) => ({
...hiddenColumnsSettings(set),
...refreshableSettings(set),
truncateContainerName: TRUNCATE_LENGTH,

View File

@ -7,7 +7,7 @@ import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
interface DockerImage {
Command: Array<string>;
Command: null | Array<string>;
Entrypoint: Array<string>;
ExposedPorts: Array<number>;
Volumes: Array<string>;
@ -24,7 +24,7 @@ export function DockerfileDetails({ image }: Props) {
<TableTitle label="Dockerfile details" icon={List} />
<DetailsTable>
<DetailsTable.Row label="CMD">
<code>{joinCommand(image.Command)}</code>
<code>{image.Command ? joinCommand(image.Command) : '-'}</code>
</DetailsTable.Row>
{image.Entrypoint && (

View File

@ -4,7 +4,7 @@ import { Authorized } from '@/react/hooks/useUser';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Icon } from '@/react/components/Icon';
import { Table, TableContainer, TableTitle } from '@@/datatables';
import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
@ -42,53 +42,51 @@ export function NetworkContainersTable({
return (
<TableContainer>
<TableTitle label="Containers in network" icon={Server} />
<Table className="nopadding">
<DetailsTable
headers={tableHeaders}
dataCy="networkDetails-networkContainers"
>
{networkContainers.map((container) => (
<tr key={container.Id}>
<td>
<Link
to="docker.containers.container"
params={{
id: container.Id,
nodeName,
<DetailsTable
headers={tableHeaders}
dataCy="networkDetails-networkContainers"
>
{networkContainers.map((container) => (
<tr key={container.Id}>
<td>
<Link
to="docker.containers.container"
params={{
id: container.Id,
nodeName,
}}
title={container.Name}
>
{container.Name}
</Link>
</td>
<td>{container.IPv4Address || '-'}</td>
<td>{container.IPv6Address || '-'}</td>
<td>{container.MacAddress || '-'}</td>
<td>
<Authorized authorizations="DockerNetworkDisconnect">
<Button
data-cy={`networkDetails-disconnect${container.Name}`}
size="xsmall"
color="dangerlight"
onClick={() => {
if (container.Id) {
disconnectContainer.mutate({
containerId: container.Id,
environmentId,
networkId,
});
}
}}
title={container.Name}
>
{container.Name}
</Link>
</td>
<td>{container.IPv4Address || '-'}</td>
<td>{container.IPv6Address || '-'}</td>
<td>{container.MacAddress || '-'}</td>
<td>
<Authorized authorizations="DockerNetworkDisconnect">
<Button
data-cy={`networkDetails-disconnect${container.Name}`}
size="xsmall"
color="dangerlight"
onClick={() => {
if (container.Id) {
disconnectContainer.mutate({
containerId: container.Id,
environmentId,
networkId,
});
}
}}
>
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
Leave Network
</Button>
</Authorized>
</td>
</tr>
))}
</DetailsTable>
</Table>
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
Leave Network
</Button>
</Authorized>
</td>
</tr>
))}
</DetailsTable>
</TableContainer>
);
}

View File

@ -4,7 +4,7 @@ import { Share2, Trash2 } from 'lucide-react';
import DockerNetworkHelper from '@/docker/helpers/networkHelper';
import { Authorized } from '@/react/hooks/useUser';
import { Table, TableContainer, TableTitle } from '@@/datatables';
import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
import { Button } from '@@/buttons';
import { Icon } from '@@/Icon';
@ -32,76 +32,74 @@ export function NetworkDetailsTable({
return (
<TableContainer>
<TableTitle label="Network details" icon={Share2} />
<Table className="nopadding">
<DetailsTable dataCy="networkDetails-detailsTable">
{/* networkRowContent */}
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
<DetailsTable.Row label="Id">
{network.Id}
{allowRemoveNetwork && (
<Authorized authorizations="DockerNetworkDelete">
<Button
data-cy="networkDetails-deleteNetwork"
size="xsmall"
color="danger"
onClick={() => onRemoveNetworkClicked()}
>
<Icon
icon={Trash2}
className="space-right"
aria-hidden="true"
/>
Delete this network
</Button>
</Authorized>
)}
</DetailsTable.Row>
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
<DetailsTable.Row label="Attachable">
{String(network.Attachable)}
</DetailsTable.Row>
<DetailsTable.Row label="Internal">
{String(network.Internal)}
</DetailsTable.Row>
<DetailsTable dataCy="networkDetails-detailsTable">
{/* networkRowContent */}
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
<DetailsTable.Row label="Id">
{network.Id}
{allowRemoveNetwork && (
<Authorized authorizations="DockerNetworkDelete">
<Button
data-cy="networkDetails-deleteNetwork"
size="xsmall"
color="danger"
onClick={() => onRemoveNetworkClicked()}
>
<Icon
icon={Trash2}
className="space-right"
aria-hidden="true"
/>
Delete this network
</Button>
</Authorized>
)}
</DetailsTable.Row>
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
<DetailsTable.Row label="Attachable">
{String(network.Attachable)}
</DetailsTable.Row>
<DetailsTable.Row label="Internal">
{String(network.Internal)}
</DetailsTable.Row>
{/* IPV4 ConfigRowContent */}
{ipv4Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
>
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row>
<DetailsTable.Row
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
>
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses
)}`}
</DetailsTable.Row>
</Fragment>
))}
{/* IPV4 ConfigRowContent */}
{ipv4Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
>
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row>
<DetailsTable.Row
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
>
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses
)}`}
</DetailsTable.Row>
</Fragment>
))}
{/* IPV6 ConfigRowContent */}
{ipv6Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
>
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row>
<DetailsTable.Row
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
>
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses
)}`}
</DetailsTable.Row>
</Fragment>
))}
</DetailsTable>
</Table>
{/* IPV6 ConfigRowContent */}
{ipv6Configs.map((config) => (
<Fragment key={config.Subnet}>
<DetailsTable.Row
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
>
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
</DetailsTable.Row>
<DetailsTable.Row
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
>
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
config.AuxiliaryAddresses
)}`}
</DetailsTable.Row>
</Fragment>
))}
</DetailsTable>
</TableContainer>
);

View File

@ -1,6 +1,6 @@
import { Share2 } from 'lucide-react';
import { Table, TableContainer, TableTitle } from '@@/datatables';
import { TableContainer, TableTitle } from '@@/datatables';
import { DetailsTable } from '@@/DetailsTable';
import { NetworkOptions } from '../types';
@ -19,15 +19,13 @@ export function NetworkOptionsTable({ options }: Props) {
return (
<TableContainer>
<TableTitle label="Network options" icon={Share2} />
<Table className="nopadding">
<DetailsTable dataCy="networkDetails-networkOptionsTable">
{networkEntries.map(([key, value]) => (
<DetailsTable.Row key={key} label={key}>
{value}
</DetailsTable.Row>
))}
</DetailsTable>
</Table>
<DetailsTable dataCy="networkDetails-networkOptionsTable">
{networkEntries.map(([key, value]) => (
<DetailsTable.Row key={key} label={key}>
{value}
</DetailsTable.Row>
))}
</DetailsTable>
</TableContainer>
);
}

View File

@ -1,5 +1,3 @@
import _ from 'lodash';
import { useStore } from 'zustand';
import { Box } from 'lucide-react';
import { DockerContainer } from '@/react/docker/containers/types';
@ -10,17 +8,16 @@ import { ContainersDatatableActions } from '@/react/docker/containers/ListView/C
import { ContainersDatatableSettings } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableSettings';
import { useShowGPUsColumn } from '@/react/docker/containers/utils';
import { Datatable, TableSettingsMenu } from '@@/datatables';
import { Datatable, Table } from '@@/datatables';
import {
buildAction,
QuickActionsSettings,
} from '@@/datatables/QuickActionsSettings';
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
import { useTableState } from '@@/datatables/useTableState';
import { useContainers } from '../../containers/queries/containers';
import { RowProvider } from '../../containers/ListView/ContainersDatatable/RowContext';
const storageKey = 'stack-containers';
const settingsStore = createStore(storageKey);
@ -39,74 +36,68 @@ export interface Props {
}
export function StackContainersDatatable({ environment, stackName }: Props) {
const settings = useStore(settingsStore);
const [search, setSearch] = useSearchBarState(storageKey);
const tableState = useTableState(settingsStore, storageKey);
const isGPUsColumnVisible = useShowGPUsColumn(environment.Id);
const columns = useColumns(false, isGPUsColumnVisible);
const hidableColumns = _.compact(
columns.filter((col) => col.canHide).map((col) => col.id)
);
const containersQuery = useContainers(
environment.Id,
true,
{
label: [`com.docker.compose.project=${stackName}`],
},
settings.autoRefreshRate * 1000
tableState.autoRefreshRate * 1000
);
return (
<RowProvider context={{ environment }}>
<TableSettingsProvider settings={settingsStore}>
<Datatable
title="Containers"
titleIcon={Box}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
columns={columns}
renderTableActions={(selectedRows) => (
<ContainersDatatableActions
selectedItems={selectedRows}
isAddActionVisible={false}
endpointId={environment.Id}
/>
)}
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
renderTableSettings={(tableInstance) => {
const columnsToHide = tableInstance.allColumns.filter(
(colInstance) => hidableColumns?.includes(colInstance.id)
);
<TableSettingsProvider settings={settingsStore}>
<Datatable
title="Containers"
titleIcon={Box}
settingsManager={tableState}
columns={columns}
renderTableActions={(selectedRows) => (
<ContainersDatatableActions
selectedItems={selectedRows}
isAddActionVisible={false}
endpointId={environment.Id}
/>
)}
initialTableState={{
columnVisibility: Object.fromEntries(
tableState.hiddenColumns.map((col) => [col, false])
),
}}
renderTableSettings={(tableInstance) => {
const columnsToHide = tableInstance
.getAllColumns()
.filter((col) => col.getCanHide());
return (
<>
<ColumnVisibilityMenu<DockerContainer>
columns={columnsToHide}
onChange={(hiddenColumns) => {
settings.setHiddenColumns(hiddenColumns);
tableInstance.setHiddenColumns(hiddenColumns);
}}
value={settings.hiddenColumns}
/>
<TableSettingsMenu
quickActions={<QuickActionsSettings actions={actions} />}
>
<ContainersDatatableSettings settings={settings} />
</TableSettingsMenu>
</>
);
}}
dataset={containersQuery.data || []}
isLoading={containersQuery.isLoading}
emptyContentLabel="No containers found"
/>
</TableSettingsProvider>
</RowProvider>
return (
<>
<ColumnVisibilityMenu<DockerContainer>
columns={columnsToHide}
onChange={(hiddenColumns) => {
tableState.setHiddenColumns(hiddenColumns);
tableInstance.setColumnVisibility(
Object.fromEntries(hiddenColumns.map((col) => [col, false]))
);
}}
value={tableState.hiddenColumns}
/>
<Table.SettingsMenu
quickActions={<QuickActionsSettings actions={actions} />}
>
<ContainersDatatableSettings settings={tableState} />
</Table.SettingsMenu>
</>
);
}}
dataset={containersQuery.data || []}
isLoading={containersQuery.isLoading}
emptyContentLabel="No containers found"
/>
</TableSettingsProvider>
);
}

View File

@ -1,4 +1,3 @@
import { useStore } from 'zustand';
import { Trash2 } from 'lucide-react';
import { Environment } from '@/react/portainer/environments/types';
@ -9,7 +8,7 @@ import { Datatable as GenericDatatable } from '@@/datatables';
import { Button } from '@@/buttons';
import { TextTip } from '@@/Tip/TextTip';
import { createPersistedStore } from '@@/datatables/types';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { useTableState } from '@@/datatables/useTableState';
import { confirm } from '@@/modals/confirm';
import { buildConfirmButton } from '@@/modals/utils';
import { ModalType } from '@@/modals';
@ -28,20 +27,14 @@ export function Datatable() {
const associateMutation = useAssociateDeviceMutation();
const removeMutation = useDeleteEnvironmentsMutation();
const licenseOverused = useLicenseOverused();
const settings = useStore(settingsStore);
const [search, setSearch] = useSearchBarState(storageKey);
const tableState = useTableState(settingsStore, storageKey);
const { data: environments, totalCount, isLoading } = useEnvironments();
return (
<GenericDatatable
settingsManager={tableState}
columns={columns}
dataset={environments}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
title="Edge Devices Waiting Room"
emptyContentLabel="No Edge Devices found"
renderTableActions={(selectedRows) => (
@ -57,7 +50,7 @@ export function Datatable() {
<Button
onClick={() => handleAssociateDevice(selectedRows)}
disabled={selectedRows.length === 0}
disabled={selectedRows.length === 0 || licenseOverused}
>
Associate Device
</Button>

View File

@ -1,76 +1,37 @@
import moment from 'moment';
import { CellProps, Column } from 'react-table';
import { createColumnHelper } from '@tanstack/react-table';
import { WaitingRoomEnvironment } from '../types';
export const columns: readonly Column<WaitingRoomEnvironment>[] = [
{
Header: 'Name',
accessor: (row) => row.Name,
const columnHelper = createColumnHelper<WaitingRoomEnvironment>();
export const columns = [
columnHelper.accessor('Name', {
header: 'Name',
id: 'name',
disableFilters: true,
Filter: () => null,
canHide: false,
sortType: 'string',
},
{
Header: 'Edge ID',
accessor: (row) => row.EdgeID,
}),
columnHelper.accessor('EdgeID', {
header: 'Edge ID',
id: 'edge-id',
disableFilters: true,
Filter: () => null,
canHide: false,
sortType: 'string',
},
{
Header: 'Edge Groups',
accessor: (row) => row.EdgeGroups || [],
Cell: ({ value }: CellProps<WaitingRoomEnvironment, string[]>) =>
value.join(', ') || '-',
}),
columnHelper.accessor((row) => row.EdgeGroups.join(', ') || '-', {
header: 'Edge Groups',
id: 'edge-groups',
disableFilters: true,
Filter: () => null,
canHide: false,
sortType: 'string',
},
{
Header: 'Group',
accessor: (row) => row.Group || '-',
}),
columnHelper.accessor((row) => row.Group || '-', {
header: 'Group',
id: 'group',
disableFilters: true,
Filter: () => null,
canHide: false,
sortType: 'string',
},
{
Header: 'Tags',
accessor: (row) => row.Tags || [],
Cell: ({ value }: CellProps<WaitingRoomEnvironment, string[]>) =>
value.join(', ') || '-',
}),
columnHelper.accessor((row) => row.Tags.join(', ') || '-', {
header: 'Tags',
id: 'tags',
disableFilters: true,
Filter: () => null,
canHide: false,
sortType: 'string',
},
{
Header: 'Last Check-in',
accessor: 'LastCheckInDate',
Cell: LastCheckinDateCell,
}),
columnHelper.accessor((row) => row.LastCheckInDate, {
header: 'Last Check-in',
id: 'last-check-in',
disableFilters: true,
Filter: () => null,
canHide: false,
sortType: 'string',
},
] as const;
function LastCheckinDateCell({
value,
}: CellProps<WaitingRoomEnvironment, number>) {
if (!value) {
return '-';
}
return moment(value * 1000).fromNow();
}
cell: ({ getValue }) => {
const value = getValue();
return value ? moment(value * 1000).fromNow() : '-';
},
}),
];

View File

@ -1,8 +1,7 @@
import { Row, TableRowProps } from 'react-table';
import { Shuffle, Trash2 } from 'lucide-react';
import { useStore } from 'zustand';
import { useRouter } from '@uirouter/react';
import clsx from 'clsx';
import { Row } from '@tanstack/react-table';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import {
@ -15,15 +14,15 @@ import { notifyError, notifySuccess } from '@/portainer/services/notifications';
import { Datatable, Table, TableSettingsMenu } from '@@/datatables';
import { confirmDelete } from '@@/modals/confirm';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
import { useTableState } from '@@/datatables/useTableState';
import { useMutationDeleteServices, useServices } from '../service';
import { Service } from '../types';
import { DefaultDatatableSettings } from '../../datatables/DefaultDatatableSettings';
import { useColumns } from './columns';
import { columns } from './columns';
import { createStore } from './datatable-store';
import { ServicesDatatableDescription } from './ServicesDatatableDescription';
@ -31,19 +30,16 @@ const storageKey = 'k8sServicesDatatable';
const settingsStore = createStore(storageKey);
export function ServicesDatatable() {
const tableState = useTableState(settingsStore, storageKey);
const environmentId = useEnvironmentId();
const servicesQuery = useServices(environmentId);
const settings = useStore(settingsStore);
const [search, setSearch] = useSearchBarState(storageKey);
const columns = useColumns();
const readOnly = !useAuthorizations(['K8sServiceW']);
const { isAdmin } = useCurrentUser();
const filteredServices = servicesQuery.data?.filter(
(service) =>
(isAdmin && settings.showSystemResources) ||
(isAdmin && tableState.showSystemResources) ||
!KubernetesNamespaceHelper.isSystemNamespace(service.Namespace)
);
@ -51,35 +47,30 @@ export function ServicesDatatable() {
<Datatable
dataset={filteredServices || []}
columns={columns}
settingsManager={tableState}
isLoading={servicesQuery.isLoading}
emptyContentLabel="No services found"
title="Services"
titleIcon={Shuffle}
getRowId={(row) => row.UID}
isRowSelectable={(row) =>
!KubernetesNamespaceHelper.isSystemNamespace(row.values.namespace)
!KubernetesNamespaceHelper.isSystemNamespace(row.original.Namespace)
}
disableSelect={readOnly}
renderTableActions={(selectedRows) => (
<TableActions selectedItems={selectedRows} />
)}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
renderTableSettings={() => (
<TableSettingsMenu>
<DefaultDatatableSettings
settings={settings}
settings={tableState}
hideShowSystemResources={!isAdmin}
/>
</TableSettingsMenu>
)}
description={
<ServicesDatatableDescription
showSystemResources={settings.showSystemResources || !isAdmin}
showSystemResources={tableState.showSystemResources || !isAdmin}
/>
}
renderRow={servicesRenderRow}
@ -89,20 +80,13 @@ export function ServicesDatatable() {
// needed to apply custom styling to the row cells and not globally.
// required in the AC's for this ticket.
function servicesRenderRow<D extends Record<string, unknown>>(
row: Row<D>,
rowProps: TableRowProps,
highlightedItemId?: string
) {
function servicesRenderRow(row: Row<Service>, highlightedItemId?: string) {
return (
<Table.Row<D>
key={rowProps.key}
cells={row.cells}
className={clsx('[&>td]:!py-4 [&>td]:!align-top', rowProps.className, {
<Table.Row<Service>
cells={row.getVisibleCells()}
className={clsx('[&>td]:!py-4 [&>td]:!align-top', {
active: highlightedItemId === row.id,
})}
role={rowProps.role}
style={rowProps.style}
/>
);
}

View File

@ -1,4 +1,4 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
@ -6,29 +6,34 @@ import { Link } from '@@/Link';
import { Service } from '../../types';
export const application: Column<Service> = {
Header: 'Application',
accessor: (row) => (row.Applications ? row.Applications[0].Name : ''),
id: 'application',
import { columnHelper } from './helper';
Cell: ({ row, value: appname }: CellProps<Service, string>) => {
const environmentId = useEnvironmentId();
return appname ? (
<Link
to="kubernetes.applications.application"
params={{
endpointId: environmentId,
namespace: row.original.Namespace,
name: appname,
}}
title={appname}
>
{appname}
</Link>
) : (
'-'
);
},
canHide: true,
disableFilters: true,
};
export const application = columnHelper.accessor(
(row) => (row.Applications ? row.Applications[0].Name : ''),
{
header: 'Application',
id: 'application',
cell: Cell,
}
);
function Cell({ row, getValue }: CellContext<Service, string>) {
const appName = getValue();
const environmentId = useEnvironmentId();
return appName ? (
<Link
to="kubernetes.applications.application"
params={{
endpointId: environmentId,
namespace: row.original.Namespace,
name: appName,
}}
title={appName}
>
{appName}
</Link>
) : (
'-'
);
}

View File

@ -1,20 +1,17 @@
import { CellProps, Column } from 'react-table';
import { columnHelper } from './helper';
import { Service } from '../../types';
export const clusterIP: Column<Service> = {
Header: 'Cluster IP',
accessor: 'ClusterIPs',
export const clusterIP = columnHelper.accessor('ClusterIPs', {
header: 'Cluster IP',
id: 'clusterIP',
Cell: ({ value: clusterIPs }: CellProps<Service, Service['ClusterIPs']>) => {
cell: ({ getValue }) => {
const clusterIPs = getValue();
if (!clusterIPs?.length) {
return '-';
}
return clusterIPs.map((ip) => <div key={ip}>{ip}</div>);
},
disableFilters: true,
canHide: true,
sortType: (rowA, rowB) => {
sortingFn: (rowA, rowB) => {
const a = rowA.original.ClusterIPs;
const b = rowB.original.ClusterIPs;
@ -38,4 +35,4 @@ export const clusterIP: Column<Service> = {
}
);
},
};
});

View File

@ -1,23 +1,20 @@
import { CellProps, Column } from 'react-table';
import { formatDate } from '@/portainer/filters/filters';
import { Service } from '../../types';
import { columnHelper } from './helper';
export const created: Column<Service> = {
Header: 'Created',
export const created = columnHelper.accessor('CreationTimestamp', {
header: 'Created',
id: 'created',
accessor: (row) => row.CreationTimestamp,
Cell: ({ row }: CellProps<Service>) => {
cell: ({ row, getValue }) => {
const date = formatDate(getValue());
const owner =
row.original.Labels?.['io.portainer.kubernetes.application.owner'];
if (owner) {
return `${formatDate(row.original.CreationTimestamp)} by ${owner}`;
return `${date} by ${owner}`;
}
return formatDate(row.original.CreationTimestamp);
return date;
},
disableFilters: true,
canHide: true,
};
});

View File

@ -1,8 +1,108 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { Service } from '../../types';
import { ExternalIPLink } from './externalIPLink';
import { ExternalIPLink } from './ExternalIPLink';
import { columnHelper } from './helper';
export const externalIP = columnHelper.accessor(
(row) => {
if (row.Type === 'ExternalName') {
return row.ExternalName;
}
if (row.ExternalIPs?.length) {
return row.ExternalIPs?.slice(0);
}
return row.IngressStatus?.slice(0);
},
{
header: 'External IP',
id: 'externalIP',
cell: Cell,
sortingFn: (rowA, rowB) => {
const a = rowA.original.IngressStatus;
const b = rowB.original.IngressStatus;
const aExternal = rowA.original.ExternalIPs;
const bExternal = rowB.original.ExternalIPs;
const ipA = a?.[0].IP || aExternal?.[0] || rowA.original.ExternalName;
const ipB = b?.[0].IP || bExternal?.[0] || rowA.original.ExternalName;
if (!ipA) return 1;
if (!ipB) return -1;
// use a nat sort order for ip addresses
return ipA.localeCompare(
ipB,
navigator.languages[0] || navigator.language,
{
numeric: true,
ignorePunctuation: true,
}
);
},
}
);
function Cell({ row }: CellContext<Service, string>) {
if (row.original.Type === 'ExternalName') {
if (row.original.ExternalName) {
const linkTo = `http://${row.original.ExternalName}`;
return <ExternalIPLink to={linkTo} text={row.original.ExternalName} />;
}
return '-';
}
const [scheme, port] = getSchemeAndPort(row.original);
if (row.original.ExternalIPs?.length) {
return row.original.ExternalIPs?.map((ip, index) => {
// some ips come through blank
if (ip.length === 0) {
return '-';
}
if (scheme) {
let linkTo = `${scheme}://${ip}`;
if (port !== 80 && port !== 443) {
linkTo = `${linkTo}:${port}`;
}
return (
<div key={index}>
<ExternalIPLink to={linkTo} text={ip} />
</div>
);
}
return <div key={index}>{ip}</div>;
});
}
const status = row.original.IngressStatus;
if (status) {
return status?.map((status, index) => {
// some ips come through blank
if (status.IP.length === 0) {
return '-';
}
if (scheme) {
let linkTo = `${scheme}://${status.IP}`;
if (port !== 80 && port !== 443) {
linkTo = `${linkTo}:${port}`;
}
return (
<div key={index}>
<ExternalIPLink to={linkTo} text={status.IP} />
</div>
);
}
return <div key={index}>{status.IP}</div>;
});
}
return '-';
}
// calculate the scheme based on the ports of the service
// favour https over http.
@ -37,100 +137,3 @@ function getSchemeAndPort(svc: Service): [string, number] {
return [scheme, servicePort];
}
export const externalIP: Column<Service> = {
Header: 'External IP',
id: 'externalIP',
accessor: (row) => {
if (row.Type === 'ExternalName') {
return row.ExternalName;
}
if (row.ExternalIPs?.length) {
return row.ExternalIPs?.slice(0);
}
return row.IngressStatus?.slice(0);
},
Cell: ({ row }: CellProps<Service>) => {
if (row.original.Type === 'ExternalName') {
if (row.original.ExternalName) {
const linkto = `http://${row.original.ExternalName}`;
return <ExternalIPLink to={linkto} text={row.original.ExternalName} />;
}
return '-';
}
const [scheme, port] = getSchemeAndPort(row.original);
if (row.original.ExternalIPs?.length) {
return row.original.ExternalIPs?.map((ip, index) => {
// some ips come through blank
if (ip.length === 0) {
return '-';
}
if (scheme) {
let linkto = `${scheme}://${ip}`;
if (port !== 80 && port !== 443) {
linkto = `${linkto}:${port}`;
}
return (
<div key={index}>
<ExternalIPLink to={linkto} text={ip} />
</div>
);
}
return <div key={index}>{ip}</div>;
});
}
const status = row.original.IngressStatus;
if (status) {
return status?.map((status, index) => {
// some ips come through blank
if (status.IP.length === 0) {
return '-';
}
if (scheme) {
let linkto = `${scheme}://${status.IP}`;
if (port !== 80 && port !== 443) {
linkto = `${linkto}:${port}`;
}
return (
<div key={index}>
<ExternalIPLink to={linkto} text={status.IP} />
</div>
);
}
return <div key={index}>{status.IP}</div>;
});
}
return '-';
},
disableFilters: true,
canHide: true,
sortType: (rowA, rowB) => {
const a = rowA.original.IngressStatus;
const b = rowB.original.IngressStatus;
const aExternal = rowA.original.ExternalIPs;
const bExternal = rowB.original.ExternalIPs;
const ipA = a?.[0].IP || aExternal?.[0] || rowA.original.ExternalName;
const ipB = b?.[0].IP || bExternal?.[0] || rowA.original.ExternalName;
if (!ipA) return 1;
if (!ipB) return -1;
// use a nat sort order for ip addresses
return ipA.localeCompare(
ipB,
navigator.languages[0] || navigator.language,
{
numeric: true,
ignorePunctuation: true,
}
);
},
};

View File

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { Service } from '../../types';
export const columnHelper = createColumnHelper<Service>();

View File

@ -8,16 +8,14 @@ import { targetPorts } from './targetPorts';
import { application } from './application';
import { created } from './created';
export function useColumns() {
return [
name,
application,
namespace,
type,
ports,
targetPorts,
clusterIP,
externalIP,
created,
];
}
export const columns = [
name,
application,
namespace,
type,
ports,
targetPorts,
clusterIP,
externalIP,
created,
];

View File

@ -1,15 +1,13 @@
import { CellProps, Column } from 'react-table';
import { Authorized } from '@/react/hooks/useUser';
import KubernetesNamespaceHelper from '@/kubernetes/helpers/namespaceHelper';
import { Service } from '../../types';
import { columnHelper } from './helper';
export const name: Column<Service> = {
Header: 'Name',
id: 'Name',
accessor: (row) => row.Name,
Cell: ({ row }: CellProps<Service>) => {
export const name = columnHelper.accessor('Name', {
header: 'Name',
id: 'name',
cell: ({ row, getValue }) => {
const name = getValue();
const isSystem = KubernetesNamespaceHelper.isSystemNamespace(
row.original.Namespace
);
@ -19,11 +17,8 @@ export const name: Column<Service> = {
!row.original.Labels['io.portainer.kubernetes.application.owner'];
return (
<Authorized
authorizations="K8sServiceW"
childrenUnauthorized={row.original.Name}
>
{row.original.Name}
<Authorized authorizations="K8sServiceW" childrenUnauthorized={name}>
{name}
{isSystem && (
<span className="label label-info image-tag label-margins">
@ -39,7 +34,4 @@ export const name: Column<Service> = {
</Authorized>
);
},
disableFilters: true,
canHide: true,
};
});

View File

@ -1,4 +1,4 @@
import { CellProps, Column, Row } from 'react-table';
import { Row } from '@tanstack/react-table';
import { filterHOC } from '@/react/components/datatables/Filter';
@ -6,28 +6,30 @@ import { Link } from '@@/Link';
import { Service } from '../../types';
export const namespace: Column<Service> = {
Header: 'Namespace',
import { columnHelper } from './helper';
export const namespace = columnHelper.accessor('Namespace', {
header: 'Namespace',
id: 'namespace',
accessor: 'Namespace',
Cell: ({ row }: CellProps<Service>) => (
<Link
to="kubernetes.resourcePools.resourcePool"
params={{
id: row.original.Namespace,
}}
title={row.original.Namespace}
>
{row.original.Namespace}
</Link>
),
canHide: true,
disableFilters: false,
Filter: filterHOC('Filter by namespace'),
filter: (rows: Row<Service>[], _filterValue, filters) => {
if (filters.length === 0) {
return rows;
}
return rows.filter((r) => filters.includes(r.original.Namespace));
cell: ({ getValue }) => {
const namespace = getValue();
return (
<Link
to="kubernetes.resourcePools.resourcePool"
params={{
id: namespace,
}}
title={namespace}
>
{namespace}
</Link>
);
},
};
meta: {
filter: filterHOC('Filter by namespace'),
},
enableColumnFilter: true,
filterFn: (row: Row<Service>, columnId: string, filterValue: string[]) =>
filterValue.length === 0 || filterValue.includes(row.original.Namespace),
});

View File

@ -1,76 +1,68 @@
import { CellProps, Column } from 'react-table';
import { Tooltip } from '@@/Tip/Tooltip';
import { Service } from '../../types';
import { columnHelper } from './helper';
export const ports: Column<Service> = {
Header: () => (
<>
Ports
<Tooltip message="The format of Ports is port[:nodePort]/protocol. Protocol is either TCP, UDP or SCTP." />
</>
),
id: 'ports',
accessor: (row) => {
const ports = row.Ports;
return ports.map(
(port) => `${port.Port}:${port.NodePort}/${port.Protocol}`
);
},
Cell: ({ row }: CellProps<Service>) => {
if (!row.original.Ports.length) {
return '-';
}
return (
export const ports = columnHelper.accessor(
(row) =>
row.Ports.map((port) => `${port.Port}:${port.NodePort}/${port.Protocol}`),
{
header: () => (
<>
{row.original.Ports.map((port, index) => {
if (port.NodePort !== 0) {
Ports
<Tooltip message="The format of Ports is port[:nodePort]/protocol. Protocol is either TCP, UDP or SCTP." />
</>
),
id: 'ports',
cell: ({ row }) => {
if (!row.original.Ports.length) {
return '-';
}
return (
<>
{row.original.Ports.map((port, index) => {
if (port.NodePort !== 0) {
return (
<div key={index}>
{port.Port}:{port.NodePort}/{port.Protocol}
</div>
);
}
return (
<div key={index}>
{port.Port}:{port.NodePort}/{port.Protocol}
{port.Port}/{port.Protocol}
</div>
);
}
})}
</>
);
},
sortingFn: (rowA, rowB) => {
const a = rowA.original.Ports;
const b = rowB.original.Ports;
return (
<div key={index}>
{port.Port}/{port.Protocol}
</div>
);
})}
</>
);
},
disableFilters: true,
canHide: true,
if (!a.length && !b.length) return 0;
sortType: (rowA, rowB) => {
const a = rowA.original.Ports;
const b = rowB.original.Ports;
if (!a.length) return 1;
if (!b.length) return -1;
if (!a.length && !b.length) return 0;
// sort order based on first port
const portA = a[0].Port;
const portB = b[0].Port;
if (!a.length) return 1;
if (!b.length) return -1;
if (portA === portB) {
// longer list of ports is considered "greater"
if (a.length < b.length) return -1;
if (a.length > b.length) return 1;
return 0;
}
// sort order based on first port
const portA = a[0].Port;
const portB = b[0].Port;
// now do a regular number sort
if (portA < portB) return -1;
if (portA > portB) return 1;
if (portA === portB) {
// longer list of ports is considered "greater"
if (a.length < b.length) return -1;
if (a.length > b.length) return 1;
return 0;
}
// now do a regular number sort
if (portA < portB) return -1;
if (portA > portB) return 1;
return 0;
},
};
},
}
);

View File

@ -1,53 +1,46 @@
import { CellProps, Column } from 'react-table';
import { columnHelper } from './helper';
import { Service } from '../../types';
export const targetPorts = columnHelper.accessor(
(row) => row.Ports.map((port) => `${port.TargetPort}`),
{
header: 'Target Ports',
id: 'targetPorts',
cell: ({ getValue }) => {
const ports = getValue();
export const targetPorts: Column<Service> = {
Header: 'Target Ports',
id: 'targetPorts',
accessor: (row) => {
const ports = row.Ports;
if (!ports.length) {
return '-';
}
return ports.map((port) => `${port.TargetPort}`);
},
Cell: ({ row }: CellProps<Service>) => {
const ports = row.original.Ports;
if (!ports.length) {
return '-';
}
return ports.map((port, index) => <div key={index}>{port.TargetPort}</div>);
},
disableFilters: true,
canHide: true,
sortType: (rowA, rowB) => {
const a = rowA.original.Ports;
const b = rowB.original.Ports;
if (!a.length && !b.length) return 0;
if (!a.length) return 1;
if (!b.length) return -1;
const portA = a[0].TargetPort;
const portB = b[0].TargetPort;
if (portA === portB) {
if (a.length < b.length) return -1;
if (a.length > b.length) return 1;
return 0;
}
// natural sort of the port
return portA.localeCompare(
portB,
navigator.languages[0] || navigator.language,
{
numeric: true,
ignorePunctuation: true,
if (!ports.length) {
return '-';
}
);
},
};
return ports.map((port, index) => <div key={index}>{port}</div>);
},
sortingFn: (rowA, rowB) => {
const a = rowA.original.Ports;
const b = rowB.original.Ports;
if (!a.length && !b.length) return 0;
if (!a.length) return 1;
if (!b.length) return -1;
const portA = a[0].TargetPort;
const portB = b[0].TargetPort;
if (portA === portB) {
if (a.length < b.length) return -1;
if (a.length > b.length) return 1;
return 0;
}
// natural sort of the port
return portA.localeCompare(
portB,
navigator.languages[0] || navigator.language,
{
numeric: true,
ignorePunctuation: true,
}
);
},
}
);

View File

@ -1,22 +1,18 @@
import { CellProps, Column, Row } from 'react-table';
import { Row } from '@tanstack/react-table';
import { filterHOC } from '@@/datatables/Filter';
import { Service } from '../../types';
export const type: Column<Service> = {
Header: 'Type',
id: 'type',
accessor: (row) => row.Type,
Cell: ({ row }: CellProps<Service>) => <div>{row.original.Type}</div>,
canHide: true,
import { columnHelper } from './helper';
disableFilters: false,
Filter: filterHOC('Filter by type'),
filter: (rows: Row<Service>[], _filterValue, filters) => {
if (filters.length === 0) {
return rows;
}
return rows.filter((r) => filters.includes(r.original.Type));
export const type = columnHelper.accessor('Type', {
header: 'Type',
id: 'type',
meta: {
filter: filterHOC('Filter by type'),
},
};
enableColumnFilter: true,
filterFn: (row: Row<Service>, columnId: string, filterValue: string[]) =>
filterValue.length === 0 || filterValue.includes(row.original.Type),
});

View File

@ -6,7 +6,7 @@ import {
} from '../../datatables/DefaultDatatableSettings';
export function createStore(storageKey: string) {
return createPersistedStore<TableSettings>(storageKey, 'Name', (set) => ({
return createPersistedStore<TableSettings>(storageKey, 'name', (set) => ({
...refreshableSettings(set),
...systemResourcesSettings(set),
}));

View File

@ -7,6 +7,8 @@ import { EnvironmentId } from '@/react/portainer/environments/types';
import { getNamespaces } from '../namespaces/service';
import { Service } from './types';
export const queryKeys = {
list: (environmentId: EnvironmentId) =>
['environments', environmentId, 'kubernetes', 'services'] as const,
@ -18,7 +20,7 @@ async function getServices(
lookupApps: boolean
) {
try {
const { data: services } = await axios.get(
const { data: services } = await axios.get<Array<Service>>(
`kubernetes/${environmentId}/namespaces/${namespace}/services`,
{
params: {

View File

@ -1,19 +1,18 @@
import { useEffect, useState } from 'react';
import { AlertTriangle, Database } from 'lucide-react';
import { useStore } from 'zustand';
import { Database, AlertTriangle } from 'lucide-react';
import { confirm } from '@@/modals/confirm';
import { ModalType } from '@@/modals';
import { Datatable } from '@@/datatables';
import { Button, ButtonGroup } from '@@/buttons';
import { Icon } from '@@/Icon';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { createPersistedStore } from '@@/datatables/types';
import { buildConfirmButton } from '@@/modals/utils';
import { useTableState } from '@@/datatables/useTableState';
import { IngressControllerClassMap } from '../types';
import { useColumns } from './columns';
import { columns } from './columns';
const storageKey = 'ingressClasses';
const settingsStore = createPersistedStore(storageKey);
@ -39,12 +38,11 @@ export function IngressClassDatatable({
noIngressControllerLabel,
view,
}: Props) {
const settings = useStore(settingsStore);
const [search, setSearch] = useSearchBarState(storageKey);
const tableState = useTableState(settingsStore, storageKey);
const [ingControllerFormValues, setIngControllerFormValues] = useState(
ingressControllers || []
);
const columns = useColumns();
useEffect(() => {
if (allowNoneIngressClass === undefined) {
@ -81,6 +79,7 @@ export function IngressClassDatatable({
return (
<div className="-mx-[15px]">
<Datatable
settingsManager={tableState}
dataset={ingControllerFormValues || []}
columns={columns}
isLoading={isLoading}
@ -90,12 +89,6 @@ export function IngressClassDatatable({
getRowId={(row) => `${row.Name}-${row.ClassName}-${row.Type}`}
renderTableActions={(selectedRows) => renderTableActions(selectedRows)}
description={renderIngressClassDescription()}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
/>
</div>
);

View File

@ -1,4 +1,4 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { Check, X } from 'lucide-react';
import { Badge } from '@@/Badge';
@ -6,23 +6,23 @@ import { Icon } from '@@/Icon';
import type { IngressControllerClassMap } from '../../types';
export const availability: Column<IngressControllerClassMap> = {
Header: 'Availability',
accessor: 'Availability',
Cell: AvailailityCell,
id: 'availability',
disableFilters: true,
canHide: true,
sortInverted: true,
sortType: 'basic',
Filter: () => null,
};
import { columnHelper } from './helper';
export const availability = columnHelper.accessor('Availability', {
header: 'Availability',
cell: Cell,
id: 'availability',
invertSorting: true,
sortingFn: 'basic',
});
function Cell({ getValue }: CellContext<IngressControllerClassMap, boolean>) {
const availability = getValue();
function AvailailityCell({ value }: CellProps<IngressControllerClassMap>) {
return (
<Badge type={value ? 'success' : 'danger'}>
<Icon icon={value ? Check : X} className="!mr-1" />
{value ? 'Allowed' : 'Disallowed'}
<Badge type={availability ? 'success' : 'danger'}>
<Icon icon={availability ? Check : X} className="!mr-1" />
{availability ? 'Allowed' : 'Disallowed'}
</Badge>
);
}

View File

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { IngressControllerClassMap } from '../../types';
export const columnHelper = createColumnHelper<IngressControllerClassMap>();

View File

@ -1,9 +1,5 @@
import { useMemo } from 'react';
import { availability } from './availability';
import { type } from './type';
import { name } from './name';
export function useColumns() {
return useMemo(() => [name, type, availability], []);
}
export const columns = [name, type, availability];

View File

@ -1,24 +1,26 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { Badge } from '@@/Badge';
import type { IngressControllerClassMap } from '../../types';
export const name: Column<IngressControllerClassMap> = {
Header: 'Ingress class',
accessor: 'ClassName',
Cell: NameCell,
id: 'name',
disableFilters: true,
canHide: true,
Filter: () => null,
sortType: 'string',
};
import { columnHelper } from './helper';
export const name = columnHelper.accessor('ClassName', {
header: 'Ingress class',
cell: NameCell,
id: 'name',
});
function NameCell({
row,
getValue,
}: CellContext<IngressControllerClassMap, string>) {
const className = getValue();
function NameCell({ row }: CellProps<IngressControllerClassMap>) {
return (
<span className="flex flex-nowrap">
{row.original.ClassName}
{className}
{row.original.New && <Badge className="ml-1">Newly detected</Badge>}
</span>
);

View File

@ -1,14 +1,10 @@
import { CellProps, Column } from 'react-table';
import { columnHelper } from './helper';
import type { IngressControllerClassMap } from '../../types';
export const type: Column<IngressControllerClassMap> = {
Header: 'Ingress controller type',
accessor: 'Type',
Cell: ({ row }: CellProps<IngressControllerClassMap>) =>
row.original.Type || '-',
export const type = columnHelper.accessor('Type', {
header: 'Ingress controller type',
cell: ({ getValue }) => {
const type = getValue();
return type || '-';
},
id: 'type',
disableFilters: true,
canHide: true,
Filter: () => null,
};
});

View File

@ -1,6 +1,5 @@
import { Plus, Trash2 } from 'lucide-react';
import { useRouter } from '@uirouter/react';
import { useStore } from 'zustand';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
@ -12,7 +11,7 @@ import { Datatable } from '@@/datatables';
import { Button } from '@@/buttons';
import { Link } from '@@/Link';
import { createPersistedStore } from '@@/datatables/types';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { useTableState } from '@@/datatables/useTableState';
import { DeleteIngressesRequest, Ingress } from '../types';
import { useDeleteIngresses, useIngresses } from '../queries';
@ -39,13 +38,13 @@ export function IngressDatatable() {
);
const deleteIngressesMutation = useDeleteIngresses();
const settings = useStore(settingsStore);
const [search, setSearch] = useSearchBarState(storageKey);
const tableState = useTableState(settingsStore, storageKey);
const router = useRouter();
return (
<Datatable
settingsManager={tableState}
dataset={ingressesQuery.data || []}
columns={columns}
isLoading={ingressesQuery.isLoading}
@ -55,12 +54,6 @@ export function IngressDatatable() {
getRowId={(row) => row.Name + row.Type + row.Namespace}
renderTableActions={tableActions}
disableSelect={useCheckboxes()}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
/>
);

View File

@ -1,11 +1,6 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import { Ingress } from '../../types';
export const className: Column<Ingress> = {
Header: 'Class Name',
accessor: 'ClassName',
export const className = columnHelper.accessor('ClassName', {
header: 'Class Name',
id: 'className',
disableFilters: true,
canHide: true,
};
});

View File

@ -1,23 +1,19 @@
import { CellProps, Column } from 'react-table';
import { formatDate } from '@/portainer/filters/filters';
import { Ingress } from '../../types';
import { columnHelper } from './helper';
export const created: Column<Ingress> = {
Header: 'Created',
id: 'created',
accessor: (row) => row.CreationDate,
Cell: ({ row }: CellProps<Ingress>) => {
export const created = columnHelper.accessor('CreationDate', {
header: 'Created',
cell: ({ row, getValue }) => {
const date = formatDate(getValue());
const owner =
row.original.Labels?.['io.portainer.kubernetes.ingress.owner'];
if (owner) {
return `${formatDate(row.original.CreationDate)} by ${owner}`;
return `${date} by ${owner}`;
}
return formatDate(row.original.CreationDate);
return date;
},
disableFilters: true,
canHide: true,
};
id: 'created',
});

View File

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { Ingress } from '../../types';
export const columnHelper = createColumnHelper<Ingress>();

View File

@ -1,4 +1,4 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { AlertTriangle, ArrowRight } from 'lucide-react';
import { Icon } from '@@/Icon';
@ -6,6 +6,41 @@ import { Badge } from '@@/Badge';
import { Ingress, TLS, Path } from '../../types';
import { columnHelper } from './helper';
export const ingressRules = columnHelper.accessor('Paths', {
header: 'Rules and Paths',
id: 'ingressRules',
cell: Cell,
});
function Cell({ row, getValue }: CellContext<Ingress, Path[] | undefined>) {
const paths = getValue();
if (!paths) {
return <div />;
}
return paths.map((path) => {
const isHttp = isHTTP(row.original.TLS || [], path.Host);
return (
<div key={`${path.Host}${path.Path}${path.ServiceName}:${path.Port}`}>
<span className="flex flex-nowrap items-center gap-1 px-2">
{link(path.Host, path.Path, isHttp)}
<Icon icon={ArrowRight} />
{`${path.ServiceName}:${path.Port}`}
{!path.HasService && (
<Badge type="warn" className="ml-1 gap-1">
<Icon icon={AlertTriangle} />
Service doesn&apos;t exist
</Badge>
)}
</span>
</div>
);
});
}
function isHTTP(TLSs: TLS[], host: string) {
return TLSs.filter((t) => t.Hosts.indexOf(host) !== -1).length === 0;
}
@ -24,33 +59,3 @@ function link(host: string, path: string, isHttp: boolean) {
</a>
);
}
export const ingressRules: Column<Ingress> = {
Header: 'Rules and Paths',
accessor: 'Paths',
Cell: ({ row }: CellProps<Ingress, Path[]>) => {
const results = row.original.Paths?.map((path: Path) => {
const isHttp = isHTTP(row.original.TLS || [], path.Host);
return (
<div key={`${path.Host}${path.Path}${path.ServiceName}:${path.Port}`}>
<span className="flex flex-nowrap items-center gap-1 px-2">
{link(path.Host, path.Path, isHttp)}
<Icon icon={ArrowRight} />
{`${path.ServiceName}:${path.Port}`}
{!path.HasService && (
<Badge type="warn" className="ml-1 gap-1">
<Icon icon={AlertTriangle} />
Service doesn&apos;t exist
</Badge>
)}
</span>
</div>
);
});
return results || <div />;
},
id: 'ingressRules',
disableFilters: true,
canHide: true,
disableSortBy: true,
};

View File

@ -1,4 +1,4 @@
import { CellProps, Column } from 'react-table';
import { CellContext } from '@tanstack/react-table';
import { Authorized } from '@/react/hooks/useUser';
@ -6,28 +6,30 @@ import { Link } from '@@/Link';
import { Ingress } from '../../types';
export const name: Column<Ingress> = {
Header: 'Name',
accessor: 'Name',
Cell: ({ row }: CellProps<Ingress>) => (
<Authorized
authorizations="K8sIngressesW"
childrenUnauthorized={row.original.Name}
>
import { columnHelper } from './helper';
export const name = columnHelper.accessor('Name', {
header: 'Name',
cell: Cell,
id: 'name',
});
function Cell({ row, getValue }: CellContext<Ingress, string>) {
const name = getValue();
return (
<Authorized authorizations="K8sIngressesW" childrenUnauthorized={name}>
<Link
to="kubernetes.ingresses.edit"
params={{
uid: row.original.UID,
namespace: row.original.Namespace,
name: row.original.Name,
name,
}}
title={row.original.Name}
title={name}
>
{row.original.Name}
{name}
</Link>
</Authorized>
),
id: 'name',
disableFilters: true,
canHide: true,
};
);
}

View File

@ -1,33 +1,40 @@
import { CellProps, Column, Row } from 'react-table';
import { filterHOC } from '@/react/components/datatables/Filter';
import { CellContext, Row } from '@tanstack/react-table';
import { filterHOC } from '@@/datatables/Filter';
import { Link } from '@@/Link';
import { Ingress } from '../../types';
export const namespace: Column<Ingress> = {
Header: 'Namespace',
accessor: 'Namespace',
Cell: ({ row }: CellProps<Ingress>) => (
import { columnHelper } from './helper';
export const namespace = columnHelper.accessor('Namespace', {
header: 'Namespace',
id: 'namespace',
cell: Cell,
filterFn: (row: Row<Ingress>, columnId: string, filterValue: string[]) => {
if (filterValue.length === 0) {
return true;
}
return filterValue.includes(row.original.Namespace);
},
meta: {
filter: filterHOC('Filter by namespace'),
},
enableColumnFilter: true,
});
function Cell({ getValue }: CellContext<Ingress, string>) {
const namespace = getValue();
return (
<Link
to="kubernetes.resourcePools.resourcePool"
params={{
id: row.original.Namespace,
id: namespace,
}}
title={row.original.Namespace}
title={namespace}
>
{row.original.Namespace}
{namespace}
</Link>
),
id: 'namespace',
disableFilters: false,
canHide: true,
Filter: filterHOC('Filter by namespace'),
filter: (rows: Row<Ingress>[], filterValue, filters) => {
if (filters.length === 0) {
return rows;
}
return rows.filter((r) => filters.includes(r.original.Namespace));
},
};
);
}

View File

@ -1,11 +1,6 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import { Ingress } from '../../types';
export const type: Column<Ingress> = {
Header: 'Type',
accessor: 'Type',
export const type = columnHelper.accessor('Type', {
header: 'Type',
id: 'type',
disableFilters: true,
canHide: true,
};
});

View File

@ -1,39 +1,31 @@
import { History } from 'lucide-react';
import { useStore } from 'zustand';
import { NomadEvent } from '@/react/nomad/types';
import { Datatable } from '@@/datatables';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { createPersistedStore } from '@@/datatables/types';
import { useTableState } from '@@/datatables/useTableState';
import { useColumns } from './columns';
import { columns } from './columns';
export interface EventsDatatableProps {
data: NomadEvent[];
isLoading: boolean;
}
const storageKey = 'events';
const storageKey = 'nomad_events';
const settingsStore = createPersistedStore(storageKey, 'Date');
const settingsStore = createPersistedStore(storageKey, 'date');
export function EventsDatatable({ data, isLoading }: EventsDatatableProps) {
const columns = useColumns();
const settings = useStore(settingsStore);
const [search, setSearch] = useSearchBarState(storageKey);
const tableState = useTableState(settingsStore, storageKey);
return (
<Datatable
isLoading={isLoading}
settingsManager={tableState}
columns={columns}
dataset={data}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
titleIcon={History}
title="Events"
totalCount={data.length}

View File

@ -1,12 +1,12 @@
import { Column } from 'react-table';
import { NomadEvent } from '@/react/nomad/types';
import { isoDate } from '@/portainer/filters/filters';
export const date: Column<NomadEvent> = {
Header: 'Date',
accessor: (row) => (row.Date ? isoDate(row.Date) : '-'),
import { columnHelper } from './helper';
export const date = columnHelper.accessor('Date', {
header: 'Date',
id: 'date',
disableFilters: true,
canHide: true,
};
cell: ({ getValue }) => {
const date = getValue();
return date ? isoDate(date) : '-';
},
});

View File

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { NomadEvent } from '@/react/nomad/types';
export const columnHelper = createColumnHelper<NomadEvent>();

View File

@ -1,9 +1,5 @@
import { useMemo } from 'react';
import { date } from './date';
import { type } from './type';
import { message } from './message';
export function useColumns() {
return useMemo(() => [date, type, message], []);
}
export const columns = [date, type, message];

View File

@ -1,11 +1,6 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import { NomadEvent } from '@/react/nomad/types';
export const message: Column<NomadEvent> = {
Header: 'Message',
accessor: 'Message',
export const message = columnHelper.accessor('Message', {
header: 'Message',
id: 'message',
disableFilters: true,
canHide: true,
};
});

View File

@ -1,11 +1,6 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import { NomadEvent } from '@/react/nomad/types';
export const type: Column<NomadEvent> = {
Header: 'Type',
accessor: 'Type',
export const type = columnHelper.accessor('Type', {
header: 'Type',
id: 'type',
disableFilters: true,
canHide: true,
};
});

View File

@ -1,12 +1,11 @@
import { useStore } from 'zustand';
import { Clock } from 'lucide-react';
import { Job } from '@/react/nomad/types';
import { useRepeater } from '@@/datatables/useRepeater';
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
import { TableSettingsMenu } from '@@/datatables';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { TableSettingsMenu } from '@@/datatables/TableSettingsMenu';
import { useTableState } from '@@/datatables/useTableState';
import { TasksDatatable } from './TasksDatatable';
import { columns } from './columns';
@ -19,7 +18,7 @@ export interface JobsDatatableProps {
isLoading?: boolean;
}
const storageKey = 'jobs';
const storageKey = 'nomad_jobs';
const settingsStore = createStore(storageKey);
export function JobsDatatable({
@ -27,20 +26,14 @@ export function JobsDatatable({
refreshData,
isLoading,
}: JobsDatatableProps) {
const [search, setSearch] = useSearchBarState(storageKey);
const settings = useStore(settingsStore);
useRepeater(settings.autoRefreshRate, refreshData);
const tableState = useTableState(settingsStore, storageKey);
useRepeater(tableState.autoRefreshRate, refreshData);
return (
<ExpandableDatatable<Job>
<ExpandableDatatable
dataset={jobs}
columns={columns}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
settingsManager={tableState}
title="Nomad Jobs"
titleIcon={Clock}
disableSelect
@ -49,9 +42,10 @@ export function JobsDatatable({
isLoading={isLoading}
renderTableSettings={() => (
<TableSettingsMenu>
<JobsDatatableSettings settings={settings} />
<JobsDatatableSettings settings={tableState} />
</TableSettingsMenu>
)}
expandOnRowClick
/>
);
}

View File

@ -2,20 +2,18 @@ import { Task } from '@/react/nomad/types';
import { NestedDatatable } from '@@/datatables/NestedDatatable';
import { useColumns } from './columns';
import { columns } from './columns';
export interface Props {
data: Task[];
}
export function TasksDatatable({ data }: Props) {
const columns = useColumns();
return (
<NestedDatatable
columns={columns}
dataset={data}
defaultSortBy="taskName"
initialSortBy={{ id: 'taskName', desc: false }}
/>
);
}

View File

@ -1,24 +1,23 @@
import { CellProps, Column } from 'react-table';
import { Clock, FileText } from 'lucide-react';
import { CellContext } from '@tanstack/react-table';
import { Task } from '@/react/nomad/types';
import { Link } from '@@/Link';
import { Icon } from '@@/Icon';
export const actions: Column<Task> = {
Header: 'Task Actions',
id: 'actions',
disableFilters: true,
canHide: true,
disableResizing: true,
width: '5px',
sortType: 'string',
Filter: () => null,
Cell: ActionsCell,
};
import { columnHelper } from './helper';
export function ActionsCell({ row }: CellProps<Task>) {
export const actions = columnHelper.display({
header: 'Task Actions',
id: 'actions',
meta: {
width: '5px',
},
cell: ActionsCell,
});
export function ActionsCell({ row }: CellContext<Task, unknown>) {
const params = {
allocationID: row.original.AllocationID,
taskName: row.original.TaskName,

View File

@ -1,11 +1,10 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import { Task } from '@/react/nomad/types';
export const allocationID: Column<Task> = {
Header: 'Allocation ID',
accessor: (row) => row.AllocationID || '-',
export const allocationID = columnHelper.accessor('AllocationID', {
header: 'Allocation ID',
id: 'allocationID',
disableFilters: true,
canHide: true,
};
cell: ({ getValue }) => {
const value = getValue();
return value || '-';
},
});

View File

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { Task } from '@/react/nomad/types';
export const columnHelper = createColumnHelper<Task>();

View File

@ -1,5 +1,3 @@
import { useMemo } from 'react';
import { taskStatus } from './taskStatus';
import { taskName } from './taskName';
import { taskGroup } from './taskGroup';
@ -7,9 +5,11 @@ import { allocationID } from './allocationID';
import { started } from './started';
import { actions } from './actions';
export function useColumns() {
return useMemo(
() => [taskStatus, taskName, taskGroup, allocationID, actions, started],
[]
);
}
export const columns = [
taskStatus,
taskName,
taskGroup,
allocationID,
actions,
started,
];

View File

@ -1,19 +1,17 @@
import moment from 'moment';
import { Column } from 'react-table';
import { Task } from '@/react/nomad/types';
import { isoDate } from '@/portainer/filters/filters';
import { columnHelper } from './helper';
function accessor(row: Task) {
const momentDate = moment(row.StartedAt);
const isValid = momentDate.unix() > 0;
return isValid ? isoDate(momentDate) : '-';
}
export const started: Column<Task> = {
accessor,
Header: 'Started',
export const started = columnHelper.accessor(accessor, {
header: 'Started',
id: 'startedName',
disableFilters: true,
canHide: true,
};
});

View File

@ -1,11 +1,10 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import { Task } from '@/react/nomad/types';
export const taskGroup: Column<Task> = {
Header: 'Task Group',
accessor: (row) => row.TaskGroup || '-',
export const taskGroup = columnHelper.accessor('TaskGroup', {
header: 'Task Group',
id: 'taskGroup',
disableFilters: true,
canHide: true,
};
cell: ({ getValue }) => {
const value = getValue();
return value || '-';
},
});

View File

@ -1,11 +1,6 @@
import { Column } from 'react-table';
import { columnHelper } from './helper';
import { Task } from '@/react/nomad/types';
export const taskName: Column<Task> = {
Header: 'Task Name',
accessor: (row) => row.TaskName || '-',
export const taskName = columnHelper.accessor('TaskName', {
header: 'Task Name',
id: 'taskName',
disableFilters: true,
canHide: true,
};
});

Some files were not shown because too many files have changed in this diff Show More