mirror of
https://github.com/portainer/portainer.git
synced 2025-07-19 13:29:41 +02:00
chore(deps): upgrade react-table to v8 [EE-4837] (#8245)
This commit is contained in:
parent
f20d3e72b9
commit
757461d58b
140 changed files with 1805 additions and 2872 deletions
150
app/react-table-config.d.ts
vendored
150
app/react-table-config.d.ts
vendored
|
@ -1,149 +1,9 @@
|
||||||
import {
|
import '@tanstack/react-table';
|
||||||
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';
|
|
||||||
|
|
||||||
declare module 'react-table' {
|
declare module '@tanstack/table-core' {
|
||||||
// take this file as-is, or comment out the sections that don't apply to your plugin configuration
|
interface ColumnMeta<TData extends RowData, TValue> {
|
||||||
|
|
||||||
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> {
|
|
||||||
className?: string;
|
className?: string;
|
||||||
canHide?: boolean;
|
filter?: Filter<TData, TValue>;
|
||||||
}
|
width?: number | 'auto' | string;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Box, Plus, Trash2 } from 'lucide-react';
|
import { Box, Plus, Trash2 } from 'lucide-react';
|
||||||
import { useStore } from 'zustand';
|
|
||||||
|
|
||||||
import { ContainerGroup } from '@/react/azure/types';
|
import { ContainerGroup } from '@/react/azure/types';
|
||||||
import { Authorized } from '@/react/hooks/useUser';
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
|
@ -9,7 +8,7 @@ import { Datatable } from '@@/datatables';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
import { createPersistedStore } from '@@/datatables/types';
|
import { createPersistedStore } from '@@/datatables/types';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
|
|
||||||
|
@ -22,19 +21,13 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ContainersDatatable({ dataset, onRemoveClick }: Props) {
|
export function ContainersDatatable({ dataset, onRemoveClick }: Props) {
|
||||||
const settings = useStore(settingsStore);
|
const tableState = useTableState(settingsStore, tableKey);
|
||||||
const [search, setSearch] = useSearchBarState(tableKey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datatable
|
<Datatable
|
||||||
dataset={dataset}
|
dataset={dataset}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
initialPageSize={settings.pageSize}
|
settingsManager={tableState}
|
||||||
onPageSizeChange={settings.setPageSize}
|
|
||||||
initialSortBy={settings.sortBy}
|
|
||||||
onSortByChange={settings.setSortBy}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
title="Containers"
|
title="Containers"
|
||||||
titleIcon={Box}
|
titleIcon={Box}
|
||||||
getRowId={(container) => container.id}
|
getRowId={(container) => container.id}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { ContainerGroup } from '@/react/azure/types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<ContainerGroup>();
|
|
@ -1,13 +1,5 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { ContainerGroup } from '@/react/azure/types';
|
export const location = columnHelper.accessor('location', {
|
||||||
|
header: 'Location',
|
||||||
export const location: Column<ContainerGroup> = {
|
});
|
||||||
Header: 'Location',
|
|
||||||
accessor: (container) => container.location,
|
|
||||||
id: 'location',
|
|
||||||
disableFilters: true,
|
|
||||||
Filter: () => null,
|
|
||||||
canHide: true,
|
|
||||||
sortType: 'string',
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { ContainerGroup } from '@/react/azure/types';
|
import { ContainerGroup } from '@/react/azure/types';
|
||||||
|
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
export const name: Column<ContainerGroup> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Name',
|
|
||||||
accessor: (container) => container.name,
|
export const name = columnHelper.accessor('name', {
|
||||||
id: 'name',
|
header: 'Name',
|
||||||
Cell: NameCell,
|
cell: NameCell,
|
||||||
disableFilters: true,
|
});
|
||||||
Filter: () => null,
|
|
||||||
canHide: true,
|
|
||||||
sortType: 'string',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function NameCell({
|
export function NameCell({
|
||||||
value: name,
|
getValue,
|
||||||
row: { original: container },
|
row: { original: container },
|
||||||
}: CellProps<ContainerGroup, string>) {
|
}: CellContext<ContainerGroup, string>) {
|
||||||
|
const name = getValue();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
to="azure.containerinstances.container"
|
to="azure.containerinstances.container"
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import { Column } from 'react-table';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { ownershipIcon } from '@/portainer/filters/filters';
|
import { ownershipIcon } from '@/portainer/filters/filters';
|
||||||
import { ResourceControlOwnership } from '@/react/portainer/access-control/types';
|
import { ResourceControlOwnership } from '@/react/portainer/access-control/types';
|
||||||
import { ContainerGroup } from '@/react/azure/types';
|
import { ContainerGroup } from '@/react/azure/types';
|
||||||
import { determineOwnership } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
import { determineOwnership } from '@/react/portainer/access-control/models/ResourceControlViewModel';
|
||||||
|
|
||||||
export const ownership: Column<ContainerGroup> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Ownership',
|
|
||||||
id: 'ownership',
|
export const ownership = columnHelper.accessor(
|
||||||
accessor: (row) =>
|
(row) =>
|
||||||
row.Portainer && row.Portainer.ResourceControl
|
row.Portainer && row.Portainer.ResourceControl
|
||||||
? determineOwnership(row.Portainer.ResourceControl)
|
? determineOwnership(row.Portainer.ResourceControl)
|
||||||
: ResourceControlOwnership.ADMINISTRATORS,
|
: ResourceControlOwnership.ADMINISTRATORS,
|
||||||
Cell: OwnershipCell,
|
{
|
||||||
disableFilters: true,
|
header: 'Ownership',
|
||||||
canHide: true,
|
cell: OwnershipCell,
|
||||||
sortType: 'string',
|
id: 'ownership',
|
||||||
Filter: () => null,
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
interface Props {
|
function OwnershipCell({
|
||||||
value: 'public' | 'private' | 'restricted' | 'administrators';
|
getValue,
|
||||||
}
|
}: CellContext<ContainerGroup, ResourceControlOwnership>) {
|
||||||
|
const value = getValue();
|
||||||
|
|
||||||
function OwnershipCell({ value }: Props) {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<i
|
<i
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
|
||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { ContainerGroup } from '@/react/azure/types';
|
import { ContainerGroup } from '@/react/azure/types';
|
||||||
import { getPorts } from '@/react/azure/utils';
|
import { getPorts } from '@/react/azure/utils';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
export const ports: Column<ContainerGroup> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Published Ports',
|
|
||||||
accessor: (container) => getPorts(container),
|
export const ports = columnHelper.accessor(getPorts, {
|
||||||
|
header: 'Published Ports',
|
||||||
|
cell: PortsCell,
|
||||||
id: 'ports',
|
id: 'ports',
|
||||||
disableFilters: true,
|
});
|
||||||
Filter: () => null,
|
|
||||||
canHide: true,
|
|
||||||
Cell: PortsCell,
|
|
||||||
};
|
|
||||||
|
|
||||||
function PortsCell({
|
function PortsCell({
|
||||||
value: ports,
|
getValue,
|
||||||
row: { original: container },
|
row: { original: container },
|
||||||
}: CellProps<ContainerGroup, ReturnType<typeof getPorts>>) {
|
}: CellContext<ContainerGroup, ReturnType<typeof getPorts>>) {
|
||||||
|
const ports = getValue();
|
||||||
|
|
||||||
const ip = container.properties.ipAddress
|
const ip = container.properties.ipAddress
|
||||||
? container.properties.ipAddress.ip
|
? container.properties.ipAddress.ip
|
||||||
: '';
|
: '';
|
||||||
|
|
|
@ -19,7 +19,7 @@ export function DetailsRow({
|
||||||
<td className={clsx(colClassName, 'min-w-[150px] !break-normal')}>
|
<td className={clsx(colClassName, 'min-w-[150px] !break-normal')}>
|
||||||
{label}
|
{label}
|
||||||
</td>
|
</td>
|
||||||
{children && (
|
{!!children && (
|
||||||
<td className={colClassName} data-cy={`detailsTable-${label}Value`}>
|
<td className={colClassName} data-cy={`detailsTable-${label}Value`}>
|
||||||
{children}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -21,7 +21,7 @@ export function DetailsTable({
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
)}
|
)}
|
||||||
<tbody>{children}</tbody>
|
{children && <tbody>{children}</tbody>}
|
||||||
</table>
|
</table>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
|
import { Menu, MenuButton, MenuList } from '@reach/menu-button';
|
||||||
import { ColumnInstance } from 'react-table';
|
|
||||||
import { Columns } from 'lucide-react';
|
import { Columns } from 'lucide-react';
|
||||||
|
import { Column } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Checkbox } from '@@/form-components/Checkbox';
|
import { Checkbox } from '@@/form-components/Checkbox';
|
||||||
|
|
||||||
interface Props<D extends object> {
|
interface Props<D extends object> {
|
||||||
columns: ColumnInstance<D>[];
|
columns: Column<D>[];
|
||||||
onChange: (value: string[]) => void;
|
onChange: (value: string[]) => void;
|
||||||
value: string[];
|
value: string[];
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,12 @@ export function ColumnVisibilityMenu<D extends object>({
|
||||||
{columns.map((column) => (
|
{columns.map((column) => (
|
||||||
<div key={column.id}>
|
<div key={column.id}>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={column.isVisible}
|
checked={column.getIsVisible()}
|
||||||
label={column.Header as string}
|
label={
|
||||||
|
typeof column.columnDef.header === 'string'
|
||||||
|
? column.columnDef.header
|
||||||
|
: ''
|
||||||
|
}
|
||||||
id={`visibility_${column.id}`}
|
id={`visibility_${column.id}`}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleChangeColumnVisibility(
|
handleChangeColumnVisibility(
|
||||||
|
|
|
@ -1,36 +1,40 @@
|
||||||
import {
|
import {
|
||||||
useTable,
|
Table as TableInstance,
|
||||||
useFilters,
|
|
||||||
useGlobalFilter,
|
|
||||||
useSortBy,
|
|
||||||
usePagination,
|
|
||||||
Column,
|
|
||||||
Row,
|
|
||||||
TableInstance,
|
|
||||||
TableState,
|
TableState,
|
||||||
TableRowProps,
|
useReactTable,
|
||||||
useExpanded,
|
Row,
|
||||||
} from 'react-table';
|
getCoreRowModel,
|
||||||
import { ReactNode } from 'react';
|
getPaginationRowModel,
|
||||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
getFilteredRowModel,
|
||||||
|
getSortedRowModel,
|
||||||
|
getFacetedRowModel,
|
||||||
|
getFacetedUniqueValues,
|
||||||
|
getFacetedMinMaxValues,
|
||||||
|
getExpandedRowModel,
|
||||||
|
TableOptions,
|
||||||
|
} from '@tanstack/react-table';
|
||||||
|
import { ReactNode, useMemo } from 'react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
import { IconProps } from '@@/Icon';
|
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 { DatatableHeader } from './DatatableHeader';
|
||||||
import { DatatableFooter } from './DatatableFooter';
|
import { DatatableFooter } from './DatatableFooter';
|
||||||
import { DatatableContent } from './DatatableContent';
|
|
||||||
import { defaultGetRowId } from './defaultGetRowId';
|
import { defaultGetRowId } from './defaultGetRowId';
|
||||||
import { emptyPlugin } from './emptyReactTablePlugin';
|
import { Table } from './Table';
|
||||||
import { useGoToHighlightedRow } from './useGoToHighlightedRow';
|
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[];
|
dataset: D[];
|
||||||
columns: readonly Column<D>[];
|
columns: TableOptions<D>['columns'];
|
||||||
renderTableSettings?(instance: TableInstance<D>): ReactNode;
|
renderTableSettings?(instance: TableInstance<D>): ReactNode;
|
||||||
renderTableActions?(selectedRows: D[]): ReactNode;
|
renderTableActions?(selectedRows: D[]): ReactNode;
|
||||||
disableSelect?: boolean;
|
disableSelect?: boolean;
|
||||||
|
@ -39,29 +43,20 @@ export interface Props<D extends Record<string, unknown>> {
|
||||||
emptyContentLabel?: string;
|
emptyContentLabel?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
titleIcon?: IconProps['icon'];
|
titleIcon?: IconProps['icon'];
|
||||||
initialTableState?: Partial<TableState<D>>;
|
initialTableState?: Partial<TableState>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
pageCount?: number;
|
pageCount?: number;
|
||||||
initialSortBy?: BasicTableSettings['sortBy'];
|
|
||||||
initialPageSize?: BasicTableSettings['pageSize'];
|
|
||||||
highlightedItemId?: string;
|
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;
|
onPageChange?(page: number): void;
|
||||||
|
|
||||||
renderRow?(
|
settingsManager: TSettings & {
|
||||||
row: Row<D>,
|
search: string;
|
||||||
rowProps: TableRowProps,
|
setSearch: (value: string) => void;
|
||||||
highlightedItemId?: string
|
};
|
||||||
): ReactNode;
|
renderRow?(row: Row<D>, highlightedItemId?: string): ReactNode;
|
||||||
expandable?: boolean;
|
getRowCanExpand?(row: Row<D>): boolean;
|
||||||
noWidget?: boolean;
|
noWidget?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,78 +76,83 @@ export function Datatable<D extends Record<string, unknown>>({
|
||||||
totalCount = dataset.length,
|
totalCount = dataset.length,
|
||||||
description,
|
description,
|
||||||
pageCount,
|
pageCount,
|
||||||
|
onPageChange = () => null,
|
||||||
initialSortBy,
|
settingsManager: settings,
|
||||||
initialPageSize = 10,
|
|
||||||
onPageChange = () => {},
|
|
||||||
|
|
||||||
onPageSizeChange,
|
|
||||||
onSortByChange,
|
|
||||||
searchValue,
|
|
||||||
onSearchChange,
|
|
||||||
|
|
||||||
renderRow = defaultRenderRow,
|
renderRow = defaultRenderRow,
|
||||||
expandable = false,
|
|
||||||
highlightedItemId,
|
highlightedItemId,
|
||||||
noWidget,
|
noWidget,
|
||||||
|
getRowCanExpand,
|
||||||
}: Props<D>) {
|
}: Props<D>) {
|
||||||
const isServerSidePagination = typeof pageCount !== 'undefined';
|
const isServerSidePagination = typeof pageCount !== 'undefined';
|
||||||
|
const enableRowSelection = getIsSelectionEnabled(
|
||||||
const tableInstance = useTable<D>(
|
disableSelect,
|
||||||
{
|
isRowSelectable
|
||||||
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 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(
|
useGoToHighlightedRow(
|
||||||
isServerSidePagination,
|
isServerSidePagination,
|
||||||
tableInstance.state.pageSize,
|
tableState.pagination.pageSize,
|
||||||
tableInstance.rows,
|
tableInstance.getCoreRowModel().rows,
|
||||||
handlePageChange,
|
handlePageChange,
|
||||||
highlightedItemId
|
highlightedItemId
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedItems = tableInstance.selectedFlatRows.map(
|
const selectedRowModel = tableInstance.getSelectedRowModel();
|
||||||
(row) => row.original
|
const selectedItems = selectedRowModel.rows.map((row) => row.original);
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table.Container noWidget={noWidget}>
|
<Table.Container noWidget={noWidget}>
|
||||||
<DatatableHeader
|
<DatatableHeader
|
||||||
onSearchChange={handleSearchBarChange}
|
onSearchChange={handleSearchBarChange}
|
||||||
searchValue={searchValue}
|
searchValue={settings.search}
|
||||||
title={title}
|
title={title}
|
||||||
titleIcon={titleIcon}
|
titleIcon={titleIcon}
|
||||||
|
description={description}
|
||||||
renderTableActions={() => renderTableActions(selectedItems)}
|
renderTableActions={() => renderTableActions(selectedItems)}
|
||||||
renderTableSettings={() => renderTableSettings(tableInstance)}
|
renderTableSettings={() => renderTableSettings(tableInstance)}
|
||||||
description={description}
|
|
||||||
/>
|
/>
|
||||||
<DatatableContent<D>
|
<DatatableContent<D>
|
||||||
tableInstance={tableInstance}
|
tableInstance={tableInstance}
|
||||||
renderRow={(row, rowProps) =>
|
renderRow={(row) => renderRow(row, highlightedItemId)}
|
||||||
renderRow(row, rowProps, highlightedItemId)
|
|
||||||
}
|
|
||||||
emptyContentLabel={emptyContentLabel}
|
emptyContentLabel={emptyContentLabel}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onSortChange={handleSortChange}
|
onSortChange={handleSortChange}
|
||||||
|
@ -161,8 +161,8 @@ export function Datatable<D extends Record<string, unknown>>({
|
||||||
<DatatableFooter
|
<DatatableFooter
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
page={tableInstance.state.pageIndex}
|
page={tableState.pagination.pageIndex}
|
||||||
pageSize={tableInstance.state.pageSize}
|
pageSize={tableState.pagination.pageSize}
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
totalSelected={selectedItems.length}
|
totalSelected={selectedItems.length}
|
||||||
/>
|
/>
|
||||||
|
@ -171,38 +171,81 @@ export function Datatable<D extends Record<string, unknown>>({
|
||||||
|
|
||||||
function handleSearchBarChange(value: string) {
|
function handleSearchBarChange(value: string) {
|
||||||
tableInstance.setGlobalFilter(value);
|
tableInstance.setGlobalFilter(value);
|
||||||
onSearchChange(value);
|
settings.setSearch(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageChange(page: number) {
|
function handlePageChange(page: number) {
|
||||||
tableInstance.gotoPage(page);
|
tableInstance.setPageIndex(page);
|
||||||
onPageChange(page);
|
onPageChange(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSortChange(colId: string, desc: boolean) {
|
function handleSortChange(colId: string, desc: boolean) {
|
||||||
onSortByChange(colId, desc);
|
settings.setSortBy(colId, desc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePageSizeChange(pageSize: number) {
|
function handlePageSizeChange(pageSize: number) {
|
||||||
tableInstance.setPageSize(pageSize);
|
tableInstance.setPageSize(pageSize);
|
||||||
onPageSizeChange(pageSize);
|
settings.setPageSize(pageSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function defaultRenderRow<D extends Record<string, unknown>>(
|
function defaultRenderRow<D extends Record<string, unknown>>(
|
||||||
row: Row<D>,
|
row: Row<D>,
|
||||||
rowProps: TableRowProps,
|
|
||||||
highlightedItemId?: string
|
highlightedItemId?: string
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<Table.Row<D>
|
<TableRow<D>
|
||||||
key={rowProps.key}
|
cells={row.getVisibleCells()}
|
||||||
cells={row.cells}
|
className={clsx({
|
||||||
className={clsx(rowProps.className, {
|
|
||||||
active: highlightedItemId === row.id,
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { Row, TableInstance, TableRowProps } from 'react-table';
|
import { Row, Table as TableInstance } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Table } from './Table';
|
import { Table } from './Table';
|
||||||
|
|
||||||
interface Props<D extends Record<string, unknown>> {
|
interface Props<D extends Record<string, unknown>> {
|
||||||
tableInstance: TableInstance<D>;
|
tableInstance: TableInstance<D>;
|
||||||
renderRow(row: Row<D>, rowProps: TableRowProps): React.ReactNode;
|
renderRow(row: Row<D>): React.ReactNode;
|
||||||
onSortChange?(colId: string, desc: boolean): void;
|
onSortChange?(colId: string, desc: boolean): void;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
emptyContentLabel?: string;
|
emptyContentLabel?: string;
|
||||||
|
@ -17,42 +17,24 @@ export function DatatableContent<D extends Record<string, unknown>>({
|
||||||
isLoading,
|
isLoading,
|
||||||
emptyContentLabel,
|
emptyContentLabel,
|
||||||
}: Props<D>) {
|
}: Props<D>) {
|
||||||
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
|
const headerGroups = tableInstance.getHeaderGroups();
|
||||||
tableInstance;
|
const pageRowModel = tableInstance.getPaginationRowModel();
|
||||||
|
|
||||||
const tableProps = getTableProps();
|
|
||||||
const tbodyProps = getTableBodyProps();
|
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table>
|
||||||
className={tableProps.className}
|
|
||||||
role={tableProps.role}
|
|
||||||
style={tableProps.style}
|
|
||||||
>
|
|
||||||
<thead>
|
<thead>
|
||||||
{headerGroups.map((headerGroup) => {
|
{headerGroups.map((headerGroup) => (
|
||||||
const { key, className, role, style } =
|
<Table.HeaderRow<D>
|
||||||
headerGroup.getHeaderGroupProps();
|
key={headerGroup.id}
|
||||||
return (
|
headers={headerGroup.headers}
|
||||||
<Table.HeaderRow<D>
|
onSortChange={onSortChange}
|
||||||
key={key}
|
/>
|
||||||
className={className}
|
))}
|
||||||
role={role}
|
|
||||||
style={style}
|
|
||||||
headers={headerGroup.headers}
|
|
||||||
onSortChange={onSortChange}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody
|
<tbody>
|
||||||
className={tbodyProps.className}
|
|
||||||
role={tbodyProps.role}
|
|
||||||
style={tbodyProps.style}
|
|
||||||
>
|
|
||||||
<Table.Content<D>
|
<Table.Content<D>
|
||||||
rows={page}
|
rows={pageRowModel.rows}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
prepareRow={prepareRow}
|
|
||||||
emptyContent={emptyContentLabel}
|
emptyContent={emptyContentLabel}
|
||||||
renderRow={renderRow}
|
renderRow={renderRow}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Row } from 'react-table';
|
import { Row } from '@tanstack/react-table';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
import { ExpandableDatatableTableRow } from './ExpandableDatatableRow';
|
import { ExpandableDatatableTableRow } from './ExpandableDatatableRow';
|
||||||
|
@ -7,25 +7,25 @@ import { Datatable, Props as DatatableProps } from './Datatable';
|
||||||
interface Props<D extends Record<string, unknown>>
|
interface Props<D extends Record<string, unknown>>
|
||||||
extends Omit<DatatableProps<D>, 'renderRow' | 'expandable'> {
|
extends Omit<DatatableProps<D>, 'renderRow' | 'expandable'> {
|
||||||
renderSubRow(row: Row<D>): ReactNode;
|
renderSubRow(row: Row<D>): ReactNode;
|
||||||
|
expandOnRowClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExpandableDatatable<D extends Record<string, unknown>>({
|
export function ExpandableDatatable<D extends Record<string, unknown>>({
|
||||||
renderSubRow,
|
renderSubRow,
|
||||||
|
getRowCanExpand = () => true,
|
||||||
|
expandOnRowClick,
|
||||||
...props
|
...props
|
||||||
}: Props<D>) {
|
}: Props<D>) {
|
||||||
return (
|
return (
|
||||||
<Datatable<D>
|
<Datatable<D>
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
expandable
|
getRowCanExpand={getRowCanExpand}
|
||||||
renderRow={(row, { key, className, role, style }) => (
|
renderRow={(row) => (
|
||||||
<ExpandableDatatableTableRow<D>
|
<ExpandableDatatableTableRow<D>
|
||||||
key={key}
|
|
||||||
row={row}
|
row={row}
|
||||||
className={className}
|
|
||||||
role={role}
|
|
||||||
style={style}
|
|
||||||
renderSubRow={renderSubRow}
|
renderSubRow={renderSubRow}
|
||||||
|
expandOnClick={expandOnRowClick}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,37 +1,33 @@
|
||||||
import { CSSProperties, ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Row } from 'react-table';
|
import { Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { TableRow } from './TableRow';
|
import { TableRow } from './TableRow';
|
||||||
|
|
||||||
interface Props<D extends Record<string, unknown>> {
|
interface Props<D extends Record<string, unknown>> {
|
||||||
row: Row<D>;
|
row: Row<D>;
|
||||||
className?: string;
|
|
||||||
role?: string;
|
|
||||||
style?: CSSProperties;
|
|
||||||
disableSelect?: boolean;
|
disableSelect?: boolean;
|
||||||
renderSubRow(row: Row<D>): ReactNode;
|
renderSubRow(row: Row<D>): ReactNode;
|
||||||
|
expandOnClick?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExpandableDatatableTableRow<D extends Record<string, unknown>>({
|
export function ExpandableDatatableTableRow<D extends Record<string, unknown>>({
|
||||||
row,
|
row,
|
||||||
className,
|
|
||||||
role,
|
|
||||||
style,
|
|
||||||
disableSelect,
|
disableSelect,
|
||||||
renderSubRow,
|
renderSubRow,
|
||||||
|
expandOnClick,
|
||||||
}: Props<D>) {
|
}: Props<D>) {
|
||||||
|
const cells = row.getVisibleCells();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TableRow<D>
|
<TableRow<D>
|
||||||
cells={row.cells}
|
cells={cells}
|
||||||
className={className}
|
onClick={expandOnClick ? () => row.toggleExpanded() : undefined}
|
||||||
role={role}
|
|
||||||
style={style}
|
|
||||||
/>
|
/>
|
||||||
{row.isExpanded && (
|
{row.getIsExpanded() && (
|
||||||
<tr>
|
<tr>
|
||||||
{!disableSelect && <td />}
|
{!disableSelect && <td />}
|
||||||
<td colSpan={disableSelect ? row.cells.length : row.cells.length - 1}>
|
<td colSpan={disableSelect ? cells.length : cells.length - 1}>
|
||||||
{renderSubRow(row)}
|
{renderSubRow(row)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
.expand-button {
|
|
||||||
background: none;
|
|
||||||
color: inherit;
|
|
||||||
border: none;
|
|
||||||
padding: 0;
|
|
||||||
font: inherit;
|
|
||||||
cursor: pointer;
|
|
||||||
outline: inherit;
|
|
||||||
}
|
|
|
@ -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}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,13 +1,11 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Menu, MenuButton, MenuPopover } from '@reach/menu-button';
|
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 { Check, Filter } from 'lucide-react';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
export const DefaultFilter = filterHOC('Filter by state');
|
|
||||||
|
|
||||||
interface MultipleSelectionFilterProps {
|
interface MultipleSelectionFilterProps {
|
||||||
options: string[];
|
options: string[];
|
||||||
value: string[];
|
value: string[];
|
||||||
|
@ -28,12 +26,12 @@ export function MultipleSelectionFilter({
|
||||||
<div>
|
<div>
|
||||||
<Menu>
|
<Menu>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
className={clsx('table-filter flex items-center', {
|
className={clsx('table-filter', { 'filter-active': enabled })}
|
||||||
'filter-active': enabled,
|
|
||||||
})}
|
|
||||||
>
|
>
|
||||||
Filter
|
<div className="flex items-center gap-1">
|
||||||
<Icon icon={enabled ? Check : Filter} className="!ml-1" />
|
Filter
|
||||||
|
<Icon icon={enabled ? Check : Filter} />
|
||||||
|
</div>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
<MenuPopover className="dropdown-menu">
|
<MenuPopover className="dropdown-menu">
|
||||||
<div className="tableMenu">
|
<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({
|
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 = useMemo(() => {
|
||||||
const options = new Set<string>();
|
const options = new Set<string>();
|
||||||
preFilteredRows.forEach((row) => {
|
flatRows.forEach(({ getValue }) => {
|
||||||
options.add(row.values[id]);
|
const value = getValue<string>(id);
|
||||||
|
|
||||||
|
options.add(value);
|
||||||
});
|
});
|
||||||
return Array.from(options);
|
return Array.from(options);
|
||||||
}, [id, preFilteredRows]);
|
}, [flatRows, id]);
|
||||||
|
|
||||||
|
const value = getFilterValue();
|
||||||
|
|
||||||
|
const valueAsArray = getValueAsArrayOfStrings(value);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MultipleSelectionFilter
|
<MultipleSelectionFilter
|
||||||
options={options}
|
options={options}
|
||||||
filterKey={id}
|
filterKey={id}
|
||||||
value={filterValue}
|
value={valueAsArray}
|
||||||
onChange={setFilter}
|
onChange={setFilterValue}
|
||||||
menuTitle={menuTitle}
|
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];
|
||||||
|
}
|
||||||
|
|
|
@ -1,30 +1,36 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { ColumnDef, CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
export function buildNameColumn<T extends Record<string, unknown>>(
|
export function buildNameColumn<T extends Record<string, unknown>>(
|
||||||
nameKey: string,
|
nameKey: keyof T,
|
||||||
idKey: string,
|
idKey: string,
|
||||||
path: string
|
path: string
|
||||||
) {
|
): ColumnDef<T> {
|
||||||
const name: Column<T> = {
|
const cell = createCell<T>();
|
||||||
Header: 'Name',
|
|
||||||
accessor: (row) => row[nameKey],
|
return {
|
||||||
|
header: 'Name',
|
||||||
|
accessorKey: nameKey,
|
||||||
id: 'name',
|
id: 'name',
|
||||||
Cell: NameCell,
|
cell,
|
||||||
disableFilters: true,
|
enableSorting: true,
|
||||||
Filter: () => null,
|
sortingFn: 'text',
|
||||||
canHide: false,
|
|
||||||
sortType: 'string',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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>) {
|
if (typeof name !== 'string') {
|
||||||
return (
|
return null;
|
||||||
<Link to={path} params={{ id: row.original[idKey] }} title={name}>
|
}
|
||||||
{name}
|
|
||||||
</Link>
|
return (
|
||||||
);
|
<Link to={path} params={{ id: row.original[idKey] }} title={name}>
|
||||||
|
{name}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,27 +1,28 @@
|
||||||
import {
|
import {
|
||||||
useTable,
|
getCoreRowModel,
|
||||||
useFilters,
|
getFilteredRowModel,
|
||||||
useSortBy,
|
getPaginationRowModel,
|
||||||
Column,
|
getSortedRowModel,
|
||||||
|
TableOptions,
|
||||||
TableState,
|
TableState,
|
||||||
usePagination,
|
useReactTable,
|
||||||
} from 'react-table';
|
} from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { defaultGetRowId } from './defaultGetRowId';
|
||||||
import { Table } from './Table';
|
import { Table } from './Table';
|
||||||
import { multiple } from './filter-types';
|
|
||||||
import { NestedTable } from './NestedTable';
|
import { NestedTable } from './NestedTable';
|
||||||
import { DatatableContent } from './DatatableContent';
|
import { DatatableContent } from './DatatableContent';
|
||||||
import { defaultGetRowId } from './defaultGetRowId';
|
import { BasicTableSettings } from './types';
|
||||||
|
|
||||||
interface Props<D extends Record<string, unknown>> {
|
interface Props<D extends Record<string, unknown>> {
|
||||||
dataset: D[];
|
dataset: D[];
|
||||||
columns: readonly Column<D>[];
|
columns: TableOptions<D>['columns'];
|
||||||
|
|
||||||
getRowId?(row: D): string;
|
getRowId?(row: D): string;
|
||||||
emptyContentLabel?: string;
|
emptyContentLabel?: string;
|
||||||
initialTableState?: Partial<TableState<D>>;
|
initialTableState?: Partial<TableState>;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
defaultSortBy?: string;
|
initialSortBy?: BasicTableSettings['sortBy'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function NestedDatatable<D extends Record<string, unknown>>({
|
export function NestedDatatable<D extends Record<string, unknown>>({
|
||||||
|
@ -31,25 +32,26 @@ export function NestedDatatable<D extends Record<string, unknown>>({
|
||||||
emptyContentLabel,
|
emptyContentLabel,
|
||||||
initialTableState = {},
|
initialTableState = {},
|
||||||
isLoading,
|
isLoading,
|
||||||
defaultSortBy,
|
initialSortBy,
|
||||||
}: Props<D>) {
|
}: Props<D>) {
|
||||||
const tableInstance = useTable<D>(
|
const tableInstance = useReactTable<D>({
|
||||||
{
|
columns,
|
||||||
defaultCanFilter: false,
|
data: dataset,
|
||||||
columns,
|
initialState: {
|
||||||
data: dataset,
|
sorting: initialSortBy ? [initialSortBy] : [],
|
||||||
filterTypes: { multiple },
|
...initialTableState,
|
||||||
initialState: {
|
|
||||||
sortBy: defaultSortBy ? [{ id: defaultSortBy, desc: true }] : [],
|
|
||||||
...initialTableState,
|
|
||||||
},
|
|
||||||
autoResetSelectedRows: false,
|
|
||||||
getRowId,
|
|
||||||
},
|
},
|
||||||
useFilters,
|
defaultColumn: {
|
||||||
useSortBy,
|
enableColumnFilter: false,
|
||||||
usePagination
|
enableHiding: false,
|
||||||
);
|
},
|
||||||
|
getRowId,
|
||||||
|
autoResetExpanded: false,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getFilteredRowModel: getFilteredRowModel(),
|
||||||
|
getSortedRowModel: getSortedRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NestedTable>
|
<NestedTable>
|
||||||
|
@ -58,15 +60,7 @@ export function NestedDatatable<D extends Record<string, unknown>>({
|
||||||
tableInstance={tableInstance}
|
tableInstance={tableInstance}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
emptyContentLabel={emptyContentLabel}
|
emptyContentLabel={emptyContentLabel}
|
||||||
renderRow={(row, { key, className, role, style }) => (
|
renderRow={(row) => <Table.Row<D> cells={row.getVisibleCells()} />}
|
||||||
<Table.Row<D>
|
|
||||||
cells={row.cells}
|
|
||||||
key={key}
|
|
||||||
className={className}
|
|
||||||
role={role}
|
|
||||||
style={style}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</Table.Container>
|
</Table.Container>
|
||||||
</NestedTable>
|
</NestedTable>
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
.inner-datatable .widget {
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
.inner-datatable {
|
.inner-datatable {
|
||||||
@apply rounded-md border border-solid border-gray-5 th-dark:border-gray-9;
|
@apply rounded-md border border-solid border-gray-5 th-dark:border-gray-9;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { PropsWithChildren } from 'react';
|
import { PropsWithChildren } from 'react';
|
||||||
import { TableProps } from 'react-table';
|
|
||||||
|
|
||||||
import { TableContainer } from './TableContainer';
|
import { TableContainer } from './TableContainer';
|
||||||
import { TableActions } from './TableActions';
|
import { TableActions } from './TableActions';
|
||||||
|
import { TableFooter } from './TableFooter';
|
||||||
import { TableTitleActions } from './TableTitleActions';
|
import { TableTitleActions } from './TableTitleActions';
|
||||||
import { TableContent } from './TableContent';
|
|
||||||
import { TableHeaderCell } from './TableHeaderCell';
|
|
||||||
import { TableSettingsMenu } from './TableSettingsMenu';
|
import { TableSettingsMenu } from './TableSettingsMenu';
|
||||||
import { TableTitle } from './TableTitle';
|
import { TableTitle } from './TableTitle';
|
||||||
|
import { TableContent } from './TableContent';
|
||||||
|
import { TableHeaderCell } from './TableHeaderCell';
|
||||||
import { TableHeaderRow } from './TableHeaderRow';
|
import { TableHeaderRow } from './TableHeaderRow';
|
||||||
import { TableRow } from './TableRow';
|
import { TableRow } from './TableRow';
|
||||||
import { TableFooter } from './TableFooter';
|
|
||||||
|
|
||||||
function MainComponent({
|
interface Props {
|
||||||
children,
|
className?: string;
|
||||||
className,
|
}
|
||||||
role,
|
|
||||||
style,
|
function MainComponent({ children, className }: PropsWithChildren<Props>) {
|
||||||
}: PropsWithChildren<TableProps>) {
|
|
||||||
return (
|
return (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table
|
<table
|
||||||
|
@ -26,8 +24,6 @@ function MainComponent({
|
||||||
'table-hover table-filters nowrap-cells table',
|
'table-hover table-filters nowrap-cells table',
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
role={role}
|
|
||||||
style={style}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { PropsWithChildren } from 'react';
|
import { Fragment, PropsWithChildren } from 'react';
|
||||||
import { Row, TableRowProps } from 'react-table';
|
import { Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
interface Props<T extends Record<string, unknown> = Record<string, unknown>> {
|
interface Props<T extends Record<string, unknown> = Record<string, unknown>> {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
rows: Row<T>[];
|
rows: Row<T>[];
|
||||||
emptyContent?: string;
|
emptyContent?: string;
|
||||||
prepareRow(row: Row<T>): void;
|
renderRow(row: Row<T>): React.ReactNode;
|
||||||
renderRow(row: Row<T>, rowProps: TableRowProps): React.ReactNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableContent<
|
export function TableContent<
|
||||||
|
@ -15,7 +14,6 @@ export function TableContent<
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
rows,
|
rows,
|
||||||
emptyContent = 'No items available',
|
emptyContent = 'No items available',
|
||||||
prepareRow,
|
|
||||||
renderRow,
|
renderRow,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
|
@ -28,11 +26,9 @@ export function TableContent<
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{rows.map((row) => {
|
{rows.map((row) => (
|
||||||
prepareRow(row);
|
<Fragment key={row.id}>{renderRow(row)}</Fragment>
|
||||||
const { key, className, role, style } = row.getRowProps();
|
))}
|
||||||
return renderRow(row, { key, className, role, style });
|
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,33 +1,33 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { PropsWithChildren, ReactNode } from 'react';
|
import { CSSProperties, PropsWithChildren, ReactNode } from 'react';
|
||||||
import { TableHeaderProps } from 'react-table';
|
|
||||||
|
|
||||||
import { TableHeaderSortIcons } from './TableHeaderSortIcons';
|
|
||||||
import styles from './TableHeaderCell.module.css';
|
import styles from './TableHeaderCell.module.css';
|
||||||
|
import { TableHeaderSortIcons } from './TableHeaderSortIcons';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
canFilter: boolean;
|
|
||||||
canSort: boolean;
|
canSort: boolean;
|
||||||
headerProps: TableHeaderProps;
|
|
||||||
isSorted: boolean;
|
isSorted: boolean;
|
||||||
isSortedDesc?: boolean;
|
isSortedDesc?: boolean;
|
||||||
onSortClick: (desc: boolean) => void;
|
onSortClick: (desc: boolean) => void;
|
||||||
render: () => ReactNode;
|
render: () => ReactNode;
|
||||||
renderFilter: () => ReactNode;
|
renderFilter?: () => ReactNode;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableHeaderCell({
|
export function TableHeaderCell({
|
||||||
headerProps: { className, role, style },
|
|
||||||
canSort,
|
canSort,
|
||||||
render,
|
render,
|
||||||
onSortClick,
|
onSortClick,
|
||||||
isSorted,
|
isSorted,
|
||||||
isSortedDesc = true,
|
isSortedDesc = true,
|
||||||
canFilter,
|
|
||||||
renderFilter,
|
renderFilter,
|
||||||
|
className,
|
||||||
|
style,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
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">
|
<div className="flex h-full flex-row flex-nowrap items-center gap-1">
|
||||||
<SortWrapper
|
<SortWrapper
|
||||||
canSort={canSort}
|
canSort={canSort}
|
||||||
|
@ -37,7 +37,7 @@ export function TableHeaderCell({
|
||||||
>
|
>
|
||||||
{render()}
|
{render()}
|
||||||
</SortWrapper>
|
</SortWrapper>
|
||||||
{canFilter ? renderFilter() : null}
|
{renderFilter ? renderFilter() : null}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
|
@ -76,7 +76,6 @@ function SortWrapper({
|
||||||
<TableHeaderSortIcons
|
<TableHeaderSortIcons
|
||||||
sorted={isSorted}
|
sorted={isSorted}
|
||||||
descending={isSorted && !!isSortedDesc}
|
descending={isSorted && !!isSortedDesc}
|
||||||
className="ml-1"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -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';
|
import { TableHeaderCell } from './TableHeaderCell';
|
||||||
|
|
||||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
|
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
|
||||||
headers: HeaderGroup<D>[];
|
headers: Header<D, unknown>[];
|
||||||
onSortChange?(colId: string, desc: boolean): void;
|
onSortChange?(colId: string, desc: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableHeaderRow<
|
export function TableHeaderRow<
|
||||||
D extends Record<string, unknown> = Record<string, unknown>
|
D extends Record<string, unknown> = Record<string, unknown>
|
||||||
>({
|
>({ headers, onSortChange }: Props<D>) {
|
||||||
headers,
|
|
||||||
onSortChange,
|
|
||||||
className,
|
|
||||||
role,
|
|
||||||
style,
|
|
||||||
}: Props<D> & TableHeaderProps) {
|
|
||||||
return (
|
return (
|
||||||
<tr className={className} role={role} style={style}>
|
<tr>
|
||||||
{headers.map((column) => (
|
{headers.map((header) => {
|
||||||
<TableHeaderCell
|
const sortDirection = header.column.getIsSorted();
|
||||||
headerProps={{
|
const {
|
||||||
...column.getHeaderProps({
|
meta: { className, width } = { className: '', width: undefined },
|
||||||
className: column.className,
|
} = header.column.columnDef;
|
||||||
style: {
|
|
||||||
width: column.disableResizing ? column.width : '',
|
return (
|
||||||
},
|
<TableHeaderCell
|
||||||
}),
|
className={className}
|
||||||
}}
|
style={{
|
||||||
key={column.id}
|
width,
|
||||||
canSort={column.canSort}
|
}}
|
||||||
onSortClick={(desc) => {
|
key={header.id}
|
||||||
column.toggleSortBy(desc);
|
canSort={header.column.getCanSort()}
|
||||||
if (onSortChange) {
|
onSortClick={(desc) => {
|
||||||
onSortChange(column.id, 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())
|
||||||
}
|
}
|
||||||
}}
|
renderFilter={
|
||||||
isSorted={column.isSorted}
|
header.column.getCanFilter()
|
||||||
isSortedDesc={column.isSortedDesc}
|
? () =>
|
||||||
render={() => column.render('Header')}
|
flexRender(
|
||||||
canFilter={!column.disableFilters}
|
header.column.columnDef.meta?.filter ||
|
||||||
renderFilter={() => column.render('Filter')}
|
filterHOC('Filter'),
|
||||||
/>
|
{
|
||||||
))}
|
column: header.column,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>>
|
interface Props<D extends Record<string, unknown> = Record<string, unknown>> {
|
||||||
extends Omit<TableRowProps, 'key'> {
|
cells: Cell<D, unknown>[];
|
||||||
cells: Cell<D>[];
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableRow<
|
export function TableRow<
|
||||||
D extends Record<string, unknown> = Record<string, unknown>
|
D extends Record<string, unknown> = Record<string, unknown>
|
||||||
>({ cells, className, role, style }: Props<D>) {
|
>({ cells, className, onClick }: Props<D>) {
|
||||||
return (
|
return (
|
||||||
<tr className={className} role={role} style={style}>
|
<tr
|
||||||
{cells.map((cell) => {
|
className={clsx(className, { 'cursor-pointer': !!onClick })}
|
||||||
const cellProps = cell.getCellProps({
|
onClick={onClick}
|
||||||
className: cell.className,
|
>
|
||||||
});
|
{cells.map((cell) => (
|
||||||
|
<td key={cell.id}>
|
||||||
return (
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
<td
|
</td>
|
||||||
className={cellProps.className}
|
))}
|
||||||
role={cellProps.role}
|
|
||||||
style={cellProps.style}
|
|
||||||
key={cellProps.key}
|
|
||||||
>
|
|
||||||
{cell.render('Cell')}
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
export function emptyPlugin() {}
|
|
||||||
|
|
||||||
emptyPlugin.pluginName = 'emptyPlugin';
|
|
|
@ -1,49 +1,45 @@
|
||||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { CellProps, Column, HeaderProps } from 'react-table';
|
import { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
|
|
||||||
export function buildExpandColumn<T extends Record<string, unknown>>(
|
export function buildExpandColumn<
|
||||||
isExpandable: (item: T) => boolean
|
T extends Record<string, unknown>
|
||||||
): Column<T> {
|
>(): ColumnDef<T> {
|
||||||
return {
|
return {
|
||||||
id: 'expand',
|
id: 'expand',
|
||||||
Header: ({
|
header: ({ table }) => {
|
||||||
filteredFlatRows,
|
const hasExpandableItems = table.getExpandedRowModel().rows.length > 0;
|
||||||
getToggleAllRowsExpandedProps,
|
|
||||||
isAllRowsExpanded,
|
|
||||||
}: HeaderProps<T>) => {
|
|
||||||
const hasExpandableItems = filteredFlatRows.some((item) =>
|
|
||||||
isExpandable(item.original)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
hasExpandableItems && (
|
hasExpandableItems && (
|
||||||
<Button
|
<Button
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
onClick={table.getToggleAllRowsExpandedHandler()}
|
||||||
{...getToggleAllRowsExpandedProps()}
|
|
||||||
color="none"
|
color="none"
|
||||||
icon={isAllRowsExpanded ? ChevronDown : ChevronUp}
|
icon={table.getIsAllRowsExpanded() ? ChevronDown : ChevronUp}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
Cell: ({ row }: CellProps<T>) => (
|
cell: ({ row }) =>
|
||||||
<div className="vertical-center">
|
row.getCanExpand() && (
|
||||||
{isExpandable(row.original) && (
|
<Button
|
||||||
<Button
|
onClick={(e) => {
|
||||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
e.preventDefault();
|
||||||
{...row.getToggleRowExpandedProps()}
|
e.stopPropagation();
|
||||||
color="none"
|
|
||||||
icon={row.isExpanded ? ChevronDown : ChevronUp}
|
row.toggleExpanded();
|
||||||
/>
|
}}
|
||||||
)}
|
color="none"
|
||||||
</div>
|
icon={row.getIsExpanded() ? ChevronDown : ChevronUp}
|
||||||
),
|
/>
|
||||||
disableFilters: true,
|
),
|
||||||
Filter: () => null,
|
enableColumnFilter: false,
|
||||||
canHide: false,
|
enableGlobalFilter: false,
|
||||||
width: 30,
|
enableHiding: false,
|
||||||
disableResizing: true,
|
|
||||||
|
meta: {
|
||||||
|
width: 40,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { Row } from 'react-table';
|
import { Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
export function multiple<
|
export function multiple<
|
||||||
D extends Record<string, unknown> = Record<string, unknown>
|
D extends Record<string, unknown> = Record<string, unknown>
|
||||||
>(rows: Row<D>[], columnIds: string[], filterValue: string[] = []) {
|
>({ getValue }: Row<D>, columnId: string, filterValue: string[]): boolean {
|
||||||
if (filterValue.length === 0 || columnIds.length === 0) {
|
if (filterValue.length === 0) {
|
||||||
return rows;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return rows.filter((row) => {
|
const value = getValue(columnId) as string;
|
||||||
const value = row.values[columnIds[0]];
|
|
||||||
return filterValue.includes(value);
|
return filterValue.includes(value);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
67
app/react/components/datatables/select-column.tsx
Normal file
67
app/react/components/datatables/select-column.tsx
Normal 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;
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ export function useGoToHighlightedRow<T extends { id: string }>(
|
||||||
handlePageChangeRef.current = goToPage;
|
handlePageChangeRef.current = goToPage;
|
||||||
});
|
});
|
||||||
|
|
||||||
const highlightedItemIdRef = useRef(highlightedItemId);
|
const highlightedItemIdRef = useRef<string>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
29
app/react/components/datatables/useTableState.ts
Normal file
29
app/react/components/datatables/useTableState.ts
Normal 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]
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,17 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import { Box } from 'lucide-react';
|
import { Box } from 'lucide-react';
|
||||||
|
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||||
import { useShowGPUsColumn } from '@/react/docker/containers/utils';
|
import { useShowGPUsColumn } from '@/react/docker/containers/utils';
|
||||||
|
|
||||||
import { TableSettingsMenu, Datatable } from '@@/datatables';
|
import { Table, Datatable } from '@@/datatables';
|
||||||
import {
|
import {
|
||||||
buildAction,
|
buildAction,
|
||||||
QuickActionsSettings,
|
QuickActionsSettings,
|
||||||
} from '@@/datatables/QuickActionsSettings';
|
} from '@@/datatables/QuickActionsSettings';
|
||||||
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
|
||||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||||
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { useContainers } from '../../queries/containers';
|
import { useContainers } from '../../queries/containers';
|
||||||
|
|
||||||
|
@ -43,20 +41,15 @@ export function ContainersDatatable({
|
||||||
isHostColumnVisible,
|
isHostColumnVisible,
|
||||||
environment,
|
environment,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const settings = useStore(settingsStore);
|
|
||||||
const isGPUsColumnVisible = useShowGPUsColumn(environment.Id);
|
const isGPUsColumnVisible = useShowGPUsColumn(environment.Id);
|
||||||
const columns = useColumns(isHostColumnVisible, isGPUsColumnVisible);
|
const columns = useColumns(isHostColumnVisible, isGPUsColumnVisible);
|
||||||
const hidableColumns = _.compact(
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
columns.filter((col) => col.canHide).map((col) => col.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
|
||||||
|
|
||||||
const containersQuery = useContainers(
|
const containersQuery = useContainers(
|
||||||
environment.Id,
|
environment.Id,
|
||||||
true,
|
true,
|
||||||
undefined,
|
undefined,
|
||||||
settings.autoRefreshRate * 1000
|
tableState.autoRefreshRate * 1000
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -65,12 +58,7 @@ export function ContainersDatatable({
|
||||||
<Datatable
|
<Datatable
|
||||||
titleIcon={Box}
|
titleIcon={Box}
|
||||||
title="Containers"
|
title="Containers"
|
||||||
initialPageSize={settings.pageSize}
|
settingsManager={tableState}
|
||||||
onPageSizeChange={settings.setPageSize}
|
|
||||||
initialSortBy={settings.sortBy}
|
|
||||||
onSortByChange={settings.setSortBy}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
columns={columns}
|
columns={columns}
|
||||||
renderTableActions={(selectedRows) => (
|
renderTableActions={(selectedRows) => (
|
||||||
<ContainersDatatableActions
|
<ContainersDatatableActions
|
||||||
|
@ -81,30 +69,38 @@ export function ContainersDatatable({
|
||||||
)}
|
)}
|
||||||
isLoading={containersQuery.isLoading}
|
isLoading={containersQuery.isLoading}
|
||||||
isRowSelectable={(row) => !row.original.IsPortainer}
|
isRowSelectable={(row) => !row.original.IsPortainer}
|
||||||
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
initialTableState={{
|
||||||
|
columnVisibility: Object.fromEntries(
|
||||||
|
tableState.hiddenColumns.map((col) => [col, false])
|
||||||
|
),
|
||||||
|
}}
|
||||||
renderTableSettings={(tableInstance) => {
|
renderTableSettings={(tableInstance) => {
|
||||||
const columnsToHide = tableInstance.allColumns.filter(
|
const columnsToHide = tableInstance
|
||||||
(colInstance) => hidableColumns?.includes(colInstance.id)
|
.getAllColumns()
|
||||||
);
|
.filter((col) => col.getCanHide());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ColumnVisibilityMenu<DockerContainer>
|
<ColumnVisibilityMenu<DockerContainer>
|
||||||
columns={columnsToHide}
|
columns={columnsToHide}
|
||||||
onChange={(hiddenColumns) => {
|
onChange={(hiddenColumns) => {
|
||||||
settings.setHiddenColumns(hiddenColumns);
|
tableState.setHiddenColumns(hiddenColumns);
|
||||||
tableInstance.setHiddenColumns(hiddenColumns);
|
tableInstance.setColumnVisibility(
|
||||||
|
Object.fromEntries(
|
||||||
|
hiddenColumns.map((col) => [col, false])
|
||||||
|
)
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
value={settings.hiddenColumns}
|
value={tableState.hiddenColumns}
|
||||||
/>
|
/>
|
||||||
<TableSettingsMenu
|
<Table.SettingsMenu
|
||||||
quickActions={<QuickActionsSettings actions={actions} />}
|
quickActions={<QuickActionsSettings actions={actions} />}
|
||||||
>
|
>
|
||||||
<ContainersDatatableSettings
|
<ContainersDatatableSettings
|
||||||
isRefreshVisible
|
isRefreshVisible
|
||||||
settings={settings}
|
settings={tableState}
|
||||||
/>
|
/>
|
||||||
</TableSettingsMenu>
|
</Table.SettingsMenu>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,14 +1,9 @@
|
||||||
import { Column } from 'react-table';
|
|
||||||
|
|
||||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
|
||||||
|
|
||||||
export const created: Column<DockerContainer> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Created',
|
|
||||||
accessor: 'Created',
|
export const created = columnHelper.accessor('Created', {
|
||||||
|
header: 'Created',
|
||||||
id: 'created',
|
id: 'created',
|
||||||
Cell: ({ value }) => isoDateFromTimestamp(value),
|
cell: ({ getValue }) => isoDateFromTimestamp(getValue()),
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
Filter: () => null,
|
|
||||||
};
|
|
||||||
|
|
|
@ -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 type { DockerContainer } from '@/react/docker/containers/types';
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useContainerGpus } from '@/react/docker/containers/queries/gpus';
|
import { useContainerGpus } from '@/react/docker/containers/queries/gpus';
|
||||||
|
|
||||||
export const gpus: Column<DockerContainer> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'GPUs',
|
|
||||||
|
export const gpus = columnHelper.display({
|
||||||
|
header: 'GPUs',
|
||||||
id: 'gpus',
|
id: 'gpus',
|
||||||
disableFilters: true,
|
cell: GpusCell,
|
||||||
canHide: true,
|
});
|
||||||
Filter: () => null,
|
|
||||||
Cell: GpusCell,
|
|
||||||
};
|
|
||||||
|
|
||||||
function GpusCell({
|
function GpusCell({
|
||||||
row: { original: container },
|
row: { original: container },
|
||||||
}: CellProps<DockerContainer>) {
|
}: CellContext<DockerContainer, unknown>) {
|
||||||
const containerId = container.Id;
|
const containerId = container.Id;
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const gpusQuery = useContainerGpus(environmentId, containerId);
|
const gpusQuery = useContainerGpus(environmentId, containerId);
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { DockerContainer } from '../../../types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<DockerContainer>();
|
|
@ -1,13 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
export const host = columnHelper.accessor((row) => row.NodeName || '-', {
|
||||||
|
header: 'Host',
|
||||||
export const host: Column<DockerContainer> = {
|
|
||||||
Header: 'Host',
|
|
||||||
accessor: (row) => row.NodeName || '-',
|
|
||||||
id: 'host',
|
id: 'host',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
sortType: 'string',
|
|
||||||
Filter: () => null,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,24 +1,18 @@
|
||||||
import { Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { useSref } from '@uirouter/react';
|
import { useSref } from '@uirouter/react';
|
||||||
|
|
||||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||||
|
|
||||||
export const image: Column<DockerContainer> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Image',
|
|
||||||
accessor: 'Image',
|
export const image = columnHelper.accessor('Image', {
|
||||||
|
header: 'Image',
|
||||||
id: 'image',
|
id: 'image',
|
||||||
disableFilters: true,
|
cell: ImageCell,
|
||||||
Cell: ImageCell,
|
});
|
||||||
canHide: true,
|
|
||||||
sortType: 'string',
|
|
||||||
Filter: () => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
function ImageCell({ getValue }: CellContext<DockerContainer, string>) {
|
||||||
value: string;
|
const imageName = getValue();
|
||||||
}
|
|
||||||
|
|
||||||
function ImageCell({ value: imageName }: Props) {
|
|
||||||
const linkProps = useSref('docker.images.image', { id: imageName });
|
const linkProps = useSref('docker.images.image', { id: imageName });
|
||||||
const shortImageName = trimSHASum(imageName);
|
const shortImageName = trimSHASum(imageName);
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useMemo } from 'react';
|
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 { created } from './created';
|
||||||
import { host } from './host';
|
import { host } from './host';
|
||||||
import { image } from './image';
|
import { image } from './image';
|
||||||
import { ip } from './ip';
|
import { ip } from './ip';
|
||||||
import { name } from './name';
|
import { name } from './name';
|
||||||
import { ownership } from './ownership';
|
|
||||||
import { ports } from './ports';
|
import { ports } from './ports';
|
||||||
import { quickActions } from './quick-actions';
|
import { quickActions } from './quick-actions';
|
||||||
import { stack } from './stack';
|
import { stack } from './stack';
|
||||||
|
@ -15,7 +17,7 @@ import { gpus } from './gpus';
|
||||||
|
|
||||||
export function useColumns(
|
export function useColumns(
|
||||||
isHostColumnVisible: boolean,
|
isHostColumnVisible: boolean,
|
||||||
isGPUsColumnVisible?: boolean
|
isGPUsColumnVisible: boolean | undefined
|
||||||
) {
|
) {
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
@ -30,7 +32,7 @@ export function useColumns(
|
||||||
isHostColumnVisible && host,
|
isHostColumnVisible && host,
|
||||||
isGPUsColumnVisible && gpus,
|
isGPUsColumnVisible && gpus,
|
||||||
ports,
|
ports,
|
||||||
ownership,
|
createOwnershipColumn<DockerContainer>(),
|
||||||
]),
|
]),
|
||||||
[isHostColumnVisible, isGPUsColumnVisible]
|
[isHostColumnVisible, isGPUsColumnVisible]
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,12 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
export const ip = columnHelper.accessor((row) => row.IP || '-', {
|
||||||
|
header: 'IP Address',
|
||||||
export const ip: Column<DockerContainer> = {
|
|
||||||
Header: 'IP Address',
|
|
||||||
accessor: (row) => row.IP || '-',
|
|
||||||
id: 'ip',
|
id: 'ip',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
Filter: () => null,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { useSref } from '@uirouter/react';
|
import { useSref } from '@uirouter/react';
|
||||||
|
|
||||||
|
@ -8,21 +8,20 @@ import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||||
|
|
||||||
import { TableSettings } from '../types';
|
import { TableSettings } from '../types';
|
||||||
|
|
||||||
export const name: Column<DockerContainer> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Name',
|
|
||||||
accessor: (row) => row.Names[0],
|
export const name = columnHelper.accessor((row) => row.Names[0], {
|
||||||
|
header: 'Name',
|
||||||
id: 'name',
|
id: 'name',
|
||||||
Cell: NameCell,
|
cell: NameCell,
|
||||||
disableFilters: true,
|
});
|
||||||
Filter: () => null,
|
|
||||||
canHide: true,
|
|
||||||
sortType: 'string',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function NameCell({
|
export function NameCell({
|
||||||
value: name,
|
getValue,
|
||||||
row: { original: container },
|
row: { original: container },
|
||||||
}: CellProps<DockerContainer>) {
|
}: CellContext<DockerContainer, string>) {
|
||||||
|
const name = getValue();
|
||||||
|
|
||||||
const linkProps = useSref('.container', {
|
const linkProps = useSref('.container', {
|
||||||
id: container.Id,
|
id: container.Id,
|
||||||
nodeName: container.NodeName,
|
nodeName: container.NodeName,
|
||||||
|
|
|
@ -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}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ExternalLink } from 'lucide-react';
|
import { ExternalLink } from 'lucide-react';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import type { DockerContainer, Port } from '@/react/docker/containers/types';
|
import type { DockerContainer, Port } from '@/react/docker/containers/types';
|
||||||
|
|
||||||
|
@ -8,22 +8,17 @@ import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
import { useRowContext } from '../RowContext';
|
import { useRowContext } from '../RowContext';
|
||||||
|
|
||||||
export const ports: Column<DockerContainer> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Published Ports',
|
|
||||||
accessor: 'Ports',
|
export const ports = columnHelper.accessor('Ports', {
|
||||||
|
header: 'Published Ports',
|
||||||
id: 'ports',
|
id: 'ports',
|
||||||
Cell: PortsCell,
|
cell: PortsCell,
|
||||||
disableSortBy: true,
|
});
|
||||||
disableFilters: true,
|
|
||||||
canHide: true,
|
|
||||||
Filter: () => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
function PortsCell({ getValue }: CellContext<DockerContainer, Port[]>) {
|
||||||
value: Port[];
|
const ports = getValue();
|
||||||
}
|
|
||||||
|
|
||||||
function PortsCell({ value: ports }: Props) {
|
|
||||||
const { environment } = useRowContext();
|
const { environment } = useRowContext();
|
||||||
|
|
||||||
if (ports.length === 0) {
|
if (ports.length === 0) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { useAuthorizations } from '@/react/hooks/useUser';
|
import { useAuthorizations } from '@/react/hooks/useUser';
|
||||||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||||
|
@ -8,20 +8,17 @@ import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||||
|
|
||||||
import { TableSettings } from '../types';
|
import { TableSettings } from '../types';
|
||||||
|
|
||||||
export const quickActions: Column<DockerContainer> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Quick Actions',
|
|
||||||
|
export const quickActions = columnHelper.display({
|
||||||
|
header: 'Quick Actions',
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
Cell: QuickActionsCell,
|
cell: QuickActionsCell,
|
||||||
disableFilters: true,
|
});
|
||||||
disableSortBy: true,
|
|
||||||
canHide: true,
|
|
||||||
sortType: 'string',
|
|
||||||
Filter: () => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
function QuickActionsCell({
|
function QuickActionsCell({
|
||||||
row: { original: container },
|
row: { original: container },
|
||||||
}: CellProps<DockerContainer>) {
|
}: CellContext<DockerContainer, unknown>) {
|
||||||
const settings = useTableSettings<TableSettings>();
|
const settings = useTableSettings<TableSettings>();
|
||||||
|
|
||||||
const { hiddenQuickActions = [] } = settings;
|
const { hiddenQuickActions = [] } = settings;
|
||||||
|
|
|
@ -1,13 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
export const stack = columnHelper.accessor((row) => row.StackName || '-', {
|
||||||
|
header: 'Stack',
|
||||||
export const stack: Column<DockerContainer> = {
|
|
||||||
Header: 'Stack',
|
|
||||||
accessor: (row) => row.StackName || '-',
|
|
||||||
id: 'stack',
|
id: 'stack',
|
||||||
sortType: 'string',
|
});
|
||||||
disableFilters: true,
|
|
||||||
canHide: true,
|
|
||||||
Filter: () => null,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,27 +1,32 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type DockerContainer,
|
type DockerContainer,
|
||||||
ContainerStatus,
|
ContainerStatus,
|
||||||
} from '@/react/docker/containers/types';
|
} 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> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'State',
|
|
||||||
accessor: 'Status',
|
export const state = columnHelper.accessor('Status', {
|
||||||
|
header: 'State',
|
||||||
id: 'state',
|
id: 'state',
|
||||||
Cell: StatusCell,
|
cell: StatusCell,
|
||||||
sortType: 'string',
|
enableColumnFilter: true,
|
||||||
filter: 'multiple',
|
filterFn: multiple,
|
||||||
Filter: DefaultFilter,
|
meta: {
|
||||||
canHide: true,
|
filter: filterHOC('Filter by state'),
|
||||||
};
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function StatusCell({
|
function StatusCell({
|
||||||
value: status,
|
getValue,
|
||||||
}: CellProps<DockerContainer, ContainerStatus>) {
|
}: CellContext<DockerContainer, ContainerStatus>) {
|
||||||
|
const status = getValue();
|
||||||
|
|
||||||
const hasHealthCheck = [
|
const hasHealthCheck = [
|
||||||
ContainerStatus.Starting,
|
ContainerStatus.Starting,
|
||||||
ContainerStatus.Healthy,
|
ContainerStatus.Healthy,
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { QuickAction, TableSettings } from './types';
|
||||||
export const TRUNCATE_LENGTH = 32;
|
export const TRUNCATE_LENGTH = 32;
|
||||||
|
|
||||||
export function createStore(storageKey: string) {
|
export function createStore(storageKey: string) {
|
||||||
return createPersistedStore<TableSettings>(storageKey, 'Name', (set) => ({
|
return createPersistedStore<TableSettings>(storageKey, 'name', (set) => ({
|
||||||
...hiddenColumnsSettings(set),
|
...hiddenColumnsSettings(set),
|
||||||
...refreshableSettings(set),
|
...refreshableSettings(set),
|
||||||
truncateContainerName: TRUNCATE_LENGTH,
|
truncateContainerName: TRUNCATE_LENGTH,
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { TableContainer, TableTitle } from '@@/datatables';
|
||||||
import { DetailsTable } from '@@/DetailsTable';
|
import { DetailsTable } from '@@/DetailsTable';
|
||||||
|
|
||||||
interface DockerImage {
|
interface DockerImage {
|
||||||
Command: Array<string>;
|
Command: null | Array<string>;
|
||||||
Entrypoint: Array<string>;
|
Entrypoint: Array<string>;
|
||||||
ExposedPorts: Array<number>;
|
ExposedPorts: Array<number>;
|
||||||
Volumes: Array<string>;
|
Volumes: Array<string>;
|
||||||
|
@ -24,7 +24,7 @@ export function DockerfileDetails({ image }: Props) {
|
||||||
<TableTitle label="Dockerfile details" icon={List} />
|
<TableTitle label="Dockerfile details" icon={List} />
|
||||||
<DetailsTable>
|
<DetailsTable>
|
||||||
<DetailsTable.Row label="CMD">
|
<DetailsTable.Row label="CMD">
|
||||||
<code>{joinCommand(image.Command)}</code>
|
<code>{image.Command ? joinCommand(image.Command) : '-'}</code>
|
||||||
</DetailsTable.Row>
|
</DetailsTable.Row>
|
||||||
|
|
||||||
{image.Entrypoint && (
|
{image.Entrypoint && (
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Authorized } from '@/react/hooks/useUser';
|
||||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
import { Icon } from '@/react/components/Icon';
|
import { Icon } from '@/react/components/Icon';
|
||||||
|
|
||||||
import { Table, TableContainer, TableTitle } from '@@/datatables';
|
import { TableContainer, TableTitle } from '@@/datatables';
|
||||||
import { DetailsTable } from '@@/DetailsTable';
|
import { DetailsTable } from '@@/DetailsTable';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
@ -42,53 +42,51 @@ export function NetworkContainersTable({
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<TableTitle label="Containers in network" icon={Server} />
|
<TableTitle label="Containers in network" icon={Server} />
|
||||||
<Table className="nopadding">
|
<DetailsTable
|
||||||
<DetailsTable
|
headers={tableHeaders}
|
||||||
headers={tableHeaders}
|
dataCy="networkDetails-networkContainers"
|
||||||
dataCy="networkDetails-networkContainers"
|
>
|
||||||
>
|
{networkContainers.map((container) => (
|
||||||
{networkContainers.map((container) => (
|
<tr key={container.Id}>
|
||||||
<tr key={container.Id}>
|
<td>
|
||||||
<td>
|
<Link
|
||||||
<Link
|
to="docker.containers.container"
|
||||||
to="docker.containers.container"
|
params={{
|
||||||
params={{
|
id: container.Id,
|
||||||
id: container.Id,
|
nodeName,
|
||||||
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}
|
<Icon icon={Trash2} class-name="icon-secondary icon-md" />
|
||||||
</Link>
|
Leave Network
|
||||||
</td>
|
</Button>
|
||||||
<td>{container.IPv4Address || '-'}</td>
|
</Authorized>
|
||||||
<td>{container.IPv6Address || '-'}</td>
|
</td>
|
||||||
<td>{container.MacAddress || '-'}</td>
|
</tr>
|
||||||
<td>
|
))}
|
||||||
<Authorized authorizations="DockerNetworkDisconnect">
|
</DetailsTable>
|
||||||
<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>
|
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { Share2, Trash2 } from 'lucide-react';
|
||||||
import DockerNetworkHelper from '@/docker/helpers/networkHelper';
|
import DockerNetworkHelper from '@/docker/helpers/networkHelper';
|
||||||
import { Authorized } from '@/react/hooks/useUser';
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
import { Table, TableContainer, TableTitle } from '@@/datatables';
|
import { TableContainer, TableTitle } from '@@/datatables';
|
||||||
import { DetailsTable } from '@@/DetailsTable';
|
import { DetailsTable } from '@@/DetailsTable';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
|
@ -32,76 +32,74 @@ export function NetworkDetailsTable({
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<TableTitle label="Network details" icon={Share2} />
|
<TableTitle label="Network details" icon={Share2} />
|
||||||
<Table className="nopadding">
|
<DetailsTable dataCy="networkDetails-detailsTable">
|
||||||
<DetailsTable dataCy="networkDetails-detailsTable">
|
{/* networkRowContent */}
|
||||||
{/* networkRowContent */}
|
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
||||||
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
<DetailsTable.Row label="Id">
|
||||||
<DetailsTable.Row label="Id">
|
{network.Id}
|
||||||
{network.Id}
|
{allowRemoveNetwork && (
|
||||||
{allowRemoveNetwork && (
|
<Authorized authorizations="DockerNetworkDelete">
|
||||||
<Authorized authorizations="DockerNetworkDelete">
|
<Button
|
||||||
<Button
|
data-cy="networkDetails-deleteNetwork"
|
||||||
data-cy="networkDetails-deleteNetwork"
|
size="xsmall"
|
||||||
size="xsmall"
|
color="danger"
|
||||||
color="danger"
|
onClick={() => onRemoveNetworkClicked()}
|
||||||
onClick={() => onRemoveNetworkClicked()}
|
>
|
||||||
>
|
<Icon
|
||||||
<Icon
|
icon={Trash2}
|
||||||
icon={Trash2}
|
className="space-right"
|
||||||
className="space-right"
|
aria-hidden="true"
|
||||||
aria-hidden="true"
|
/>
|
||||||
/>
|
Delete this network
|
||||||
Delete this network
|
</Button>
|
||||||
</Button>
|
</Authorized>
|
||||||
</Authorized>
|
)}
|
||||||
)}
|
</DetailsTable.Row>
|
||||||
</DetailsTable.Row>
|
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
|
||||||
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
|
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
||||||
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
<DetailsTable.Row label="Attachable">
|
||||||
<DetailsTable.Row label="Attachable">
|
{String(network.Attachable)}
|
||||||
{String(network.Attachable)}
|
</DetailsTable.Row>
|
||||||
</DetailsTable.Row>
|
<DetailsTable.Row label="Internal">
|
||||||
<DetailsTable.Row label="Internal">
|
{String(network.Internal)}
|
||||||
{String(network.Internal)}
|
</DetailsTable.Row>
|
||||||
</DetailsTable.Row>
|
|
||||||
|
|
||||||
{/* IPV4 ConfigRowContent */}
|
{/* IPV4 ConfigRowContent */}
|
||||||
{ipv4Configs.map((config) => (
|
{ipv4Configs.map((config) => (
|
||||||
<Fragment key={config.Subnet}>
|
<Fragment key={config.Subnet}>
|
||||||
<DetailsTable.Row
|
<DetailsTable.Row
|
||||||
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
||||||
>
|
>
|
||||||
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
||||||
</DetailsTable.Row>
|
</DetailsTable.Row>
|
||||||
<DetailsTable.Row
|
<DetailsTable.Row
|
||||||
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
||||||
>
|
>
|
||||||
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
||||||
config.AuxiliaryAddresses
|
config.AuxiliaryAddresses
|
||||||
)}`}
|
)}`}
|
||||||
</DetailsTable.Row>
|
</DetailsTable.Row>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* IPV6 ConfigRowContent */}
|
{/* IPV6 ConfigRowContent */}
|
||||||
{ipv6Configs.map((config) => (
|
{ipv6Configs.map((config) => (
|
||||||
<Fragment key={config.Subnet}>
|
<Fragment key={config.Subnet}>
|
||||||
<DetailsTable.Row
|
<DetailsTable.Row
|
||||||
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
||||||
>
|
>
|
||||||
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
||||||
</DetailsTable.Row>
|
</DetailsTable.Row>
|
||||||
<DetailsTable.Row
|
<DetailsTable.Row
|
||||||
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
||||||
>
|
>
|
||||||
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
||||||
config.AuxiliaryAddresses
|
config.AuxiliaryAddresses
|
||||||
)}`}
|
)}`}
|
||||||
</DetailsTable.Row>
|
</DetailsTable.Row>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</DetailsTable>
|
</DetailsTable>
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Share2 } from 'lucide-react';
|
import { Share2 } from 'lucide-react';
|
||||||
|
|
||||||
import { Table, TableContainer, TableTitle } from '@@/datatables';
|
import { TableContainer, TableTitle } from '@@/datatables';
|
||||||
import { DetailsTable } from '@@/DetailsTable';
|
import { DetailsTable } from '@@/DetailsTable';
|
||||||
|
|
||||||
import { NetworkOptions } from '../types';
|
import { NetworkOptions } from '../types';
|
||||||
|
@ -19,15 +19,13 @@ export function NetworkOptionsTable({ options }: Props) {
|
||||||
return (
|
return (
|
||||||
<TableContainer>
|
<TableContainer>
|
||||||
<TableTitle label="Network options" icon={Share2} />
|
<TableTitle label="Network options" icon={Share2} />
|
||||||
<Table className="nopadding">
|
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
{networkEntries.map(([key, value]) => (
|
||||||
{networkEntries.map(([key, value]) => (
|
<DetailsTable.Row key={key} label={key}>
|
||||||
<DetailsTable.Row key={key} label={key}>
|
{value}
|
||||||
{value}
|
</DetailsTable.Row>
|
||||||
</DetailsTable.Row>
|
))}
|
||||||
))}
|
</DetailsTable>
|
||||||
</DetailsTable>
|
|
||||||
</Table>
|
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import { Box } from 'lucide-react';
|
import { Box } from 'lucide-react';
|
||||||
|
|
||||||
import { DockerContainer } from '@/react/docker/containers/types';
|
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 { ContainersDatatableSettings } from '@/react/docker/containers/ListView/ContainersDatatable/ContainersDatatableSettings';
|
||||||
import { useShowGPUsColumn } from '@/react/docker/containers/utils';
|
import { useShowGPUsColumn } from '@/react/docker/containers/utils';
|
||||||
|
|
||||||
import { Datatable, TableSettingsMenu } from '@@/datatables';
|
import { Datatable, Table } from '@@/datatables';
|
||||||
import {
|
import {
|
||||||
buildAction,
|
buildAction,
|
||||||
QuickActionsSettings,
|
QuickActionsSettings,
|
||||||
} from '@@/datatables/QuickActionsSettings';
|
} from '@@/datatables/QuickActionsSettings';
|
||||||
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
|
||||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||||
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { useContainers } from '../../containers/queries/containers';
|
import { useContainers } from '../../containers/queries/containers';
|
||||||
import { RowProvider } from '../../containers/ListView/ContainersDatatable/RowContext';
|
|
||||||
|
|
||||||
const storageKey = 'stack-containers';
|
const storageKey = 'stack-containers';
|
||||||
const settingsStore = createStore(storageKey);
|
const settingsStore = createStore(storageKey);
|
||||||
|
@ -39,74 +36,68 @@ export interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StackContainersDatatable({ environment, stackName }: Props) {
|
export function StackContainersDatatable({ environment, stackName }: Props) {
|
||||||
const settings = useStore(settingsStore);
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
|
||||||
|
|
||||||
const isGPUsColumnVisible = useShowGPUsColumn(environment.Id);
|
const isGPUsColumnVisible = useShowGPUsColumn(environment.Id);
|
||||||
const columns = useColumns(false, isGPUsColumnVisible);
|
const columns = useColumns(false, isGPUsColumnVisible);
|
||||||
|
|
||||||
const hidableColumns = _.compact(
|
|
||||||
columns.filter((col) => col.canHide).map((col) => col.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
const containersQuery = useContainers(
|
const containersQuery = useContainers(
|
||||||
environment.Id,
|
environment.Id,
|
||||||
true,
|
true,
|
||||||
{
|
{
|
||||||
label: [`com.docker.compose.project=${stackName}`],
|
label: [`com.docker.compose.project=${stackName}`],
|
||||||
},
|
},
|
||||||
settings.autoRefreshRate * 1000
|
tableState.autoRefreshRate * 1000
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RowProvider context={{ environment }}>
|
<TableSettingsProvider settings={settingsStore}>
|
||||||
<TableSettingsProvider settings={settingsStore}>
|
<Datatable
|
||||||
<Datatable
|
title="Containers"
|
||||||
title="Containers"
|
titleIcon={Box}
|
||||||
titleIcon={Box}
|
settingsManager={tableState}
|
||||||
initialPageSize={settings.pageSize}
|
columns={columns}
|
||||||
onPageSizeChange={settings.setPageSize}
|
renderTableActions={(selectedRows) => (
|
||||||
initialSortBy={settings.sortBy}
|
<ContainersDatatableActions
|
||||||
onSortByChange={settings.setSortBy}
|
selectedItems={selectedRows}
|
||||||
searchValue={search}
|
isAddActionVisible={false}
|
||||||
onSearchChange={setSearch}
|
endpointId={environment.Id}
|
||||||
columns={columns}
|
/>
|
||||||
renderTableActions={(selectedRows) => (
|
)}
|
||||||
<ContainersDatatableActions
|
initialTableState={{
|
||||||
selectedItems={selectedRows}
|
columnVisibility: Object.fromEntries(
|
||||||
isAddActionVisible={false}
|
tableState.hiddenColumns.map((col) => [col, false])
|
||||||
endpointId={environment.Id}
|
),
|
||||||
/>
|
}}
|
||||||
)}
|
renderTableSettings={(tableInstance) => {
|
||||||
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
const columnsToHide = tableInstance
|
||||||
renderTableSettings={(tableInstance) => {
|
.getAllColumns()
|
||||||
const columnsToHide = tableInstance.allColumns.filter(
|
.filter((col) => col.getCanHide());
|
||||||
(colInstance) => hidableColumns?.includes(colInstance.id)
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ColumnVisibilityMenu<DockerContainer>
|
<ColumnVisibilityMenu<DockerContainer>
|
||||||
columns={columnsToHide}
|
columns={columnsToHide}
|
||||||
onChange={(hiddenColumns) => {
|
onChange={(hiddenColumns) => {
|
||||||
settings.setHiddenColumns(hiddenColumns);
|
tableState.setHiddenColumns(hiddenColumns);
|
||||||
tableInstance.setHiddenColumns(hiddenColumns);
|
tableInstance.setColumnVisibility(
|
||||||
}}
|
Object.fromEntries(hiddenColumns.map((col) => [col, false]))
|
||||||
value={settings.hiddenColumns}
|
);
|
||||||
/>
|
}}
|
||||||
<TableSettingsMenu
|
value={tableState.hiddenColumns}
|
||||||
quickActions={<QuickActionsSettings actions={actions} />}
|
/>
|
||||||
>
|
<Table.SettingsMenu
|
||||||
<ContainersDatatableSettings settings={settings} />
|
quickActions={<QuickActionsSettings actions={actions} />}
|
||||||
</TableSettingsMenu>
|
>
|
||||||
</>
|
<ContainersDatatableSettings settings={tableState} />
|
||||||
);
|
</Table.SettingsMenu>
|
||||||
}}
|
</>
|
||||||
dataset={containersQuery.data || []}
|
);
|
||||||
isLoading={containersQuery.isLoading}
|
}}
|
||||||
emptyContentLabel="No containers found"
|
dataset={containersQuery.data || []}
|
||||||
/>
|
isLoading={containersQuery.isLoading}
|
||||||
</TableSettingsProvider>
|
emptyContentLabel="No containers found"
|
||||||
</RowProvider>
|
/>
|
||||||
|
</TableSettingsProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import { Trash2 } from 'lucide-react';
|
import { Trash2 } from 'lucide-react';
|
||||||
|
|
||||||
import { Environment } from '@/react/portainer/environments/types';
|
import { Environment } from '@/react/portainer/environments/types';
|
||||||
|
@ -9,7 +8,7 @@ import { Datatable as GenericDatatable } from '@@/datatables';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { TextTip } from '@@/Tip/TextTip';
|
import { TextTip } from '@@/Tip/TextTip';
|
||||||
import { createPersistedStore } from '@@/datatables/types';
|
import { createPersistedStore } from '@@/datatables/types';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
import { confirm } from '@@/modals/confirm';
|
import { confirm } from '@@/modals/confirm';
|
||||||
import { buildConfirmButton } from '@@/modals/utils';
|
import { buildConfirmButton } from '@@/modals/utils';
|
||||||
import { ModalType } from '@@/modals';
|
import { ModalType } from '@@/modals';
|
||||||
|
@ -28,20 +27,14 @@ export function Datatable() {
|
||||||
const associateMutation = useAssociateDeviceMutation();
|
const associateMutation = useAssociateDeviceMutation();
|
||||||
const removeMutation = useDeleteEnvironmentsMutation();
|
const removeMutation = useDeleteEnvironmentsMutation();
|
||||||
const licenseOverused = useLicenseOverused();
|
const licenseOverused = useLicenseOverused();
|
||||||
const settings = useStore(settingsStore);
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
|
||||||
const { data: environments, totalCount, isLoading } = useEnvironments();
|
const { data: environments, totalCount, isLoading } = useEnvironments();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GenericDatatable
|
<GenericDatatable
|
||||||
|
settingsManager={tableState}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataset={environments}
|
dataset={environments}
|
||||||
initialPageSize={settings.pageSize}
|
|
||||||
onPageSizeChange={settings.setPageSize}
|
|
||||||
initialSortBy={settings.sortBy}
|
|
||||||
onSortByChange={settings.setSortBy}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
title="Edge Devices Waiting Room"
|
title="Edge Devices Waiting Room"
|
||||||
emptyContentLabel="No Edge Devices found"
|
emptyContentLabel="No Edge Devices found"
|
||||||
renderTableActions={(selectedRows) => (
|
renderTableActions={(selectedRows) => (
|
||||||
|
@ -57,7 +50,7 @@ export function Datatable() {
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleAssociateDevice(selectedRows)}
|
onClick={() => handleAssociateDevice(selectedRows)}
|
||||||
disabled={selectedRows.length === 0}
|
disabled={selectedRows.length === 0 || licenseOverused}
|
||||||
>
|
>
|
||||||
Associate Device
|
Associate Device
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,76 +1,37 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { CellProps, Column } from 'react-table';
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { WaitingRoomEnvironment } from '../types';
|
import { WaitingRoomEnvironment } from '../types';
|
||||||
|
|
||||||
export const columns: readonly Column<WaitingRoomEnvironment>[] = [
|
const columnHelper = createColumnHelper<WaitingRoomEnvironment>();
|
||||||
{
|
|
||||||
Header: 'Name',
|
export const columns = [
|
||||||
accessor: (row) => row.Name,
|
columnHelper.accessor('Name', {
|
||||||
|
header: 'Name',
|
||||||
id: 'name',
|
id: 'name',
|
||||||
disableFilters: true,
|
}),
|
||||||
Filter: () => null,
|
columnHelper.accessor('EdgeID', {
|
||||||
canHide: false,
|
header: 'Edge ID',
|
||||||
sortType: 'string',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Edge ID',
|
|
||||||
accessor: (row) => row.EdgeID,
|
|
||||||
id: 'edge-id',
|
id: 'edge-id',
|
||||||
disableFilters: true,
|
}),
|
||||||
Filter: () => null,
|
columnHelper.accessor((row) => row.EdgeGroups.join(', ') || '-', {
|
||||||
canHide: false,
|
header: 'Edge Groups',
|
||||||
sortType: 'string',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Edge Groups',
|
|
||||||
accessor: (row) => row.EdgeGroups || [],
|
|
||||||
Cell: ({ value }: CellProps<WaitingRoomEnvironment, string[]>) =>
|
|
||||||
value.join(', ') || '-',
|
|
||||||
id: 'edge-groups',
|
id: 'edge-groups',
|
||||||
disableFilters: true,
|
}),
|
||||||
Filter: () => null,
|
columnHelper.accessor((row) => row.Group || '-', {
|
||||||
canHide: false,
|
header: 'Group',
|
||||||
sortType: 'string',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Group',
|
|
||||||
accessor: (row) => row.Group || '-',
|
|
||||||
id: 'group',
|
id: 'group',
|
||||||
disableFilters: true,
|
}),
|
||||||
Filter: () => null,
|
columnHelper.accessor((row) => row.Tags.join(', ') || '-', {
|
||||||
canHide: false,
|
header: 'Tags',
|
||||||
sortType: 'string',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Tags',
|
|
||||||
accessor: (row) => row.Tags || [],
|
|
||||||
Cell: ({ value }: CellProps<WaitingRoomEnvironment, string[]>) =>
|
|
||||||
value.join(', ') || '-',
|
|
||||||
id: 'tags',
|
id: 'tags',
|
||||||
disableFilters: true,
|
}),
|
||||||
Filter: () => null,
|
columnHelper.accessor((row) => row.LastCheckInDate, {
|
||||||
canHide: false,
|
header: 'Last Check-in',
|
||||||
sortType: 'string',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Header: 'Last Check-in',
|
|
||||||
accessor: 'LastCheckInDate',
|
|
||||||
Cell: LastCheckinDateCell,
|
|
||||||
id: 'last-check-in',
|
id: 'last-check-in',
|
||||||
disableFilters: true,
|
cell: ({ getValue }) => {
|
||||||
Filter: () => null,
|
const value = getValue();
|
||||||
canHide: false,
|
return value ? moment(value * 1000).fromNow() : '-';
|
||||||
sortType: 'string',
|
},
|
||||||
},
|
}),
|
||||||
] as const;
|
];
|
||||||
|
|
||||||
function LastCheckinDateCell({
|
|
||||||
value,
|
|
||||||
}: CellProps<WaitingRoomEnvironment, number>) {
|
|
||||||
if (!value) {
|
|
||||||
return '-';
|
|
||||||
}
|
|
||||||
|
|
||||||
return moment(value * 1000).fromNow();
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
import { Row, TableRowProps } from 'react-table';
|
|
||||||
import { Shuffle, Trash2 } from 'lucide-react';
|
import { Shuffle, Trash2 } from 'lucide-react';
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import { useRouter } from '@uirouter/react';
|
import { useRouter } from '@uirouter/react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import { Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import {
|
import {
|
||||||
|
@ -15,15 +14,15 @@ import { notifyError, notifySuccess } from '@/portainer/services/notifications';
|
||||||
|
|
||||||
import { Datatable, Table, TableSettingsMenu } from '@@/datatables';
|
import { Datatable, Table, TableSettingsMenu } from '@@/datatables';
|
||||||
import { confirmDelete } from '@@/modals/confirm';
|
import { confirmDelete } from '@@/modals/confirm';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { useMutationDeleteServices, useServices } from '../service';
|
import { useMutationDeleteServices, useServices } from '../service';
|
||||||
import { Service } from '../types';
|
import { Service } from '../types';
|
||||||
import { DefaultDatatableSettings } from '../../datatables/DefaultDatatableSettings';
|
import { DefaultDatatableSettings } from '../../datatables/DefaultDatatableSettings';
|
||||||
|
|
||||||
import { useColumns } from './columns';
|
import { columns } from './columns';
|
||||||
import { createStore } from './datatable-store';
|
import { createStore } from './datatable-store';
|
||||||
import { ServicesDatatableDescription } from './ServicesDatatableDescription';
|
import { ServicesDatatableDescription } from './ServicesDatatableDescription';
|
||||||
|
|
||||||
|
@ -31,19 +30,16 @@ const storageKey = 'k8sServicesDatatable';
|
||||||
const settingsStore = createStore(storageKey);
|
const settingsStore = createStore(storageKey);
|
||||||
|
|
||||||
export function ServicesDatatable() {
|
export function ServicesDatatable() {
|
||||||
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
const environmentId = useEnvironmentId();
|
const environmentId = useEnvironmentId();
|
||||||
const servicesQuery = useServices(environmentId);
|
const servicesQuery = useServices(environmentId);
|
||||||
|
|
||||||
const settings = useStore(settingsStore);
|
|
||||||
|
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
|
||||||
const columns = useColumns();
|
|
||||||
const readOnly = !useAuthorizations(['K8sServiceW']);
|
const readOnly = !useAuthorizations(['K8sServiceW']);
|
||||||
const { isAdmin } = useCurrentUser();
|
const { isAdmin } = useCurrentUser();
|
||||||
|
|
||||||
const filteredServices = servicesQuery.data?.filter(
|
const filteredServices = servicesQuery.data?.filter(
|
||||||
(service) =>
|
(service) =>
|
||||||
(isAdmin && settings.showSystemResources) ||
|
(isAdmin && tableState.showSystemResources) ||
|
||||||
!KubernetesNamespaceHelper.isSystemNamespace(service.Namespace)
|
!KubernetesNamespaceHelper.isSystemNamespace(service.Namespace)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -51,35 +47,30 @@ export function ServicesDatatable() {
|
||||||
<Datatable
|
<Datatable
|
||||||
dataset={filteredServices || []}
|
dataset={filteredServices || []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
settingsManager={tableState}
|
||||||
isLoading={servicesQuery.isLoading}
|
isLoading={servicesQuery.isLoading}
|
||||||
emptyContentLabel="No services found"
|
emptyContentLabel="No services found"
|
||||||
title="Services"
|
title="Services"
|
||||||
titleIcon={Shuffle}
|
titleIcon={Shuffle}
|
||||||
getRowId={(row) => row.UID}
|
getRowId={(row) => row.UID}
|
||||||
isRowSelectable={(row) =>
|
isRowSelectable={(row) =>
|
||||||
!KubernetesNamespaceHelper.isSystemNamespace(row.values.namespace)
|
!KubernetesNamespaceHelper.isSystemNamespace(row.original.Namespace)
|
||||||
}
|
}
|
||||||
disableSelect={readOnly}
|
disableSelect={readOnly}
|
||||||
renderTableActions={(selectedRows) => (
|
renderTableActions={(selectedRows) => (
|
||||||
<TableActions selectedItems={selectedRows} />
|
<TableActions selectedItems={selectedRows} />
|
||||||
)}
|
)}
|
||||||
initialPageSize={settings.pageSize}
|
|
||||||
onPageSizeChange={settings.setPageSize}
|
|
||||||
initialSortBy={settings.sortBy}
|
|
||||||
onSortByChange={settings.setSortBy}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
renderTableSettings={() => (
|
renderTableSettings={() => (
|
||||||
<TableSettingsMenu>
|
<TableSettingsMenu>
|
||||||
<DefaultDatatableSettings
|
<DefaultDatatableSettings
|
||||||
settings={settings}
|
settings={tableState}
|
||||||
hideShowSystemResources={!isAdmin}
|
hideShowSystemResources={!isAdmin}
|
||||||
/>
|
/>
|
||||||
</TableSettingsMenu>
|
</TableSettingsMenu>
|
||||||
)}
|
)}
|
||||||
description={
|
description={
|
||||||
<ServicesDatatableDescription
|
<ServicesDatatableDescription
|
||||||
showSystemResources={settings.showSystemResources || !isAdmin}
|
showSystemResources={tableState.showSystemResources || !isAdmin}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
renderRow={servicesRenderRow}
|
renderRow={servicesRenderRow}
|
||||||
|
@ -89,20 +80,13 @@ export function ServicesDatatable() {
|
||||||
|
|
||||||
// needed to apply custom styling to the row cells and not globally.
|
// needed to apply custom styling to the row cells and not globally.
|
||||||
// required in the AC's for this ticket.
|
// required in the AC's for this ticket.
|
||||||
function servicesRenderRow<D extends Record<string, unknown>>(
|
function servicesRenderRow(row: Row<Service>, highlightedItemId?: string) {
|
||||||
row: Row<D>,
|
|
||||||
rowProps: TableRowProps,
|
|
||||||
highlightedItemId?: string
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<Table.Row<D>
|
<Table.Row<Service>
|
||||||
key={rowProps.key}
|
cells={row.getVisibleCells()}
|
||||||
cells={row.cells}
|
className={clsx('[&>td]:!py-4 [&>td]:!align-top', {
|
||||||
className={clsx('[&>td]:!py-4 [&>td]:!align-top', rowProps.className, {
|
|
||||||
active: highlightedItemId === row.id,
|
active: highlightedItemId === row.id,
|
||||||
})}
|
})}
|
||||||
role={rowProps.role}
|
|
||||||
style={rowProps.style}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
|
|
||||||
|
@ -6,29 +6,34 @@ import { Link } from '@@/Link';
|
||||||
|
|
||||||
import { Service } from '../../types';
|
import { Service } from '../../types';
|
||||||
|
|
||||||
export const application: Column<Service> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Application',
|
|
||||||
accessor: (row) => (row.Applications ? row.Applications[0].Name : ''),
|
|
||||||
id: 'application',
|
|
||||||
|
|
||||||
Cell: ({ row, value: appname }: CellProps<Service, string>) => {
|
export const application = columnHelper.accessor(
|
||||||
const environmentId = useEnvironmentId();
|
(row) => (row.Applications ? row.Applications[0].Name : ''),
|
||||||
return appname ? (
|
{
|
||||||
<Link
|
header: 'Application',
|
||||||
to="kubernetes.applications.application"
|
id: 'application',
|
||||||
params={{
|
cell: Cell,
|
||||||
endpointId: environmentId,
|
}
|
||||||
namespace: row.original.Namespace,
|
);
|
||||||
name: appname,
|
|
||||||
}}
|
function Cell({ row, getValue }: CellContext<Service, string>) {
|
||||||
title={appname}
|
const appName = getValue();
|
||||||
>
|
const environmentId = useEnvironmentId();
|
||||||
{appname}
|
|
||||||
</Link>
|
return appName ? (
|
||||||
) : (
|
<Link
|
||||||
'-'
|
to="kubernetes.applications.application"
|
||||||
);
|
params={{
|
||||||
},
|
endpointId: environmentId,
|
||||||
canHide: true,
|
namespace: row.original.Namespace,
|
||||||
disableFilters: true,
|
name: appName,
|
||||||
};
|
}}
|
||||||
|
title={appName}
|
||||||
|
>
|
||||||
|
{appName}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
'-'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,20 +1,17 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { Service } from '../../types';
|
export const clusterIP = columnHelper.accessor('ClusterIPs', {
|
||||||
|
header: 'Cluster IP',
|
||||||
export const clusterIP: Column<Service> = {
|
|
||||||
Header: 'Cluster IP',
|
|
||||||
accessor: 'ClusterIPs',
|
|
||||||
id: 'clusterIP',
|
id: 'clusterIP',
|
||||||
Cell: ({ value: clusterIPs }: CellProps<Service, Service['ClusterIPs']>) => {
|
cell: ({ getValue }) => {
|
||||||
|
const clusterIPs = getValue();
|
||||||
|
|
||||||
if (!clusterIPs?.length) {
|
if (!clusterIPs?.length) {
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
return clusterIPs.map((ip) => <div key={ip}>{ip}</div>);
|
return clusterIPs.map((ip) => <div key={ip}>{ip}</div>);
|
||||||
},
|
},
|
||||||
disableFilters: true,
|
sortingFn: (rowA, rowB) => {
|
||||||
canHide: true,
|
|
||||||
sortType: (rowA, rowB) => {
|
|
||||||
const a = rowA.original.ClusterIPs;
|
const a = rowA.original.ClusterIPs;
|
||||||
const b = rowB.original.ClusterIPs;
|
const b = rowB.original.ClusterIPs;
|
||||||
|
|
||||||
|
@ -38,4 +35,4 @@ export const clusterIP: Column<Service> = {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
|
@ -1,23 +1,20 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
|
||||||
|
|
||||||
import { formatDate } from '@/portainer/filters/filters';
|
import { formatDate } from '@/portainer/filters/filters';
|
||||||
|
|
||||||
import { Service } from '../../types';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
export const created: Column<Service> = {
|
export const created = columnHelper.accessor('CreationTimestamp', {
|
||||||
Header: 'Created',
|
header: 'Created',
|
||||||
id: 'created',
|
id: 'created',
|
||||||
accessor: (row) => row.CreationTimestamp,
|
cell: ({ row, getValue }) => {
|
||||||
Cell: ({ row }: CellProps<Service>) => {
|
const date = formatDate(getValue());
|
||||||
|
|
||||||
const owner =
|
const owner =
|
||||||
row.original.Labels?.['io.portainer.kubernetes.application.owner'];
|
row.original.Labels?.['io.portainer.kubernetes.application.owner'];
|
||||||
|
|
||||||
if (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,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,8 +1,108 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Service } from '../../types';
|
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
|
// calculate the scheme based on the ports of the service
|
||||||
// favour https over http.
|
// favour https over http.
|
||||||
|
@ -37,100 +137,3 @@ function getSchemeAndPort(svc: Service): [string, number] {
|
||||||
|
|
||||||
return [scheme, servicePort];
|
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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Service } from '../../types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<Service>();
|
|
@ -8,16 +8,14 @@ import { targetPorts } from './targetPorts';
|
||||||
import { application } from './application';
|
import { application } from './application';
|
||||||
import { created } from './created';
|
import { created } from './created';
|
||||||
|
|
||||||
export function useColumns() {
|
export const columns = [
|
||||||
return [
|
name,
|
||||||
name,
|
application,
|
||||||
application,
|
namespace,
|
||||||
namespace,
|
type,
|
||||||
type,
|
ports,
|
||||||
ports,
|
targetPorts,
|
||||||
targetPorts,
|
clusterIP,
|
||||||
clusterIP,
|
externalIP,
|
||||||
externalIP,
|
created,
|
||||||
created,
|
];
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
|
||||||
|
|
||||||
import { Authorized } from '@/react/hooks/useUser';
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
import KubernetesNamespaceHelper from '@/kubernetes/helpers/namespaceHelper';
|
import KubernetesNamespaceHelper from '@/kubernetes/helpers/namespaceHelper';
|
||||||
|
|
||||||
import { Service } from '../../types';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
export const name: Column<Service> = {
|
export const name = columnHelper.accessor('Name', {
|
||||||
Header: 'Name',
|
header: 'Name',
|
||||||
id: 'Name',
|
id: 'name',
|
||||||
accessor: (row) => row.Name,
|
cell: ({ row, getValue }) => {
|
||||||
Cell: ({ row }: CellProps<Service>) => {
|
const name = getValue();
|
||||||
const isSystem = KubernetesNamespaceHelper.isSystemNamespace(
|
const isSystem = KubernetesNamespaceHelper.isSystemNamespace(
|
||||||
row.original.Namespace
|
row.original.Namespace
|
||||||
);
|
);
|
||||||
|
@ -19,11 +17,8 @@ export const name: Column<Service> = {
|
||||||
!row.original.Labels['io.portainer.kubernetes.application.owner'];
|
!row.original.Labels['io.portainer.kubernetes.application.owner'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Authorized
|
<Authorized authorizations="K8sServiceW" childrenUnauthorized={name}>
|
||||||
authorizations="K8sServiceW"
|
{name}
|
||||||
childrenUnauthorized={row.original.Name}
|
|
||||||
>
|
|
||||||
{row.original.Name}
|
|
||||||
|
|
||||||
{isSystem && (
|
{isSystem && (
|
||||||
<span className="label label-info image-tag label-margins">
|
<span className="label label-info image-tag label-margins">
|
||||||
|
@ -39,7 +34,4 @@ export const name: Column<Service> = {
|
||||||
</Authorized>
|
</Authorized>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
});
|
||||||
disableFilters: true,
|
|
||||||
canHide: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CellProps, Column, Row } from 'react-table';
|
import { Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { filterHOC } from '@/react/components/datatables/Filter';
|
import { filterHOC } from '@/react/components/datatables/Filter';
|
||||||
|
|
||||||
|
@ -6,28 +6,30 @@ import { Link } from '@@/Link';
|
||||||
|
|
||||||
import { Service } from '../../types';
|
import { Service } from '../../types';
|
||||||
|
|
||||||
export const namespace: Column<Service> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Namespace',
|
|
||||||
|
export const namespace = columnHelper.accessor('Namespace', {
|
||||||
|
header: 'Namespace',
|
||||||
id: 'namespace',
|
id: 'namespace',
|
||||||
accessor: 'Namespace',
|
cell: ({ getValue }) => {
|
||||||
Cell: ({ row }: CellProps<Service>) => (
|
const namespace = getValue();
|
||||||
<Link
|
|
||||||
to="kubernetes.resourcePools.resourcePool"
|
return (
|
||||||
params={{
|
<Link
|
||||||
id: row.original.Namespace,
|
to="kubernetes.resourcePools.resourcePool"
|
||||||
}}
|
params={{
|
||||||
title={row.original.Namespace}
|
id: namespace,
|
||||||
>
|
}}
|
||||||
{row.original.Namespace}
|
title={namespace}
|
||||||
</Link>
|
>
|
||||||
),
|
{namespace}
|
||||||
canHide: true,
|
</Link>
|
||||||
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));
|
|
||||||
},
|
},
|
||||||
};
|
meta: {
|
||||||
|
filter: filterHOC('Filter by namespace'),
|
||||||
|
},
|
||||||
|
enableColumnFilter: true,
|
||||||
|
filterFn: (row: Row<Service>, columnId: string, filterValue: string[]) =>
|
||||||
|
filterValue.length === 0 || filterValue.includes(row.original.Namespace),
|
||||||
|
});
|
||||||
|
|
|
@ -1,76 +1,68 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
|
||||||
|
|
||||||
import { Tooltip } from '@@/Tip/Tooltip';
|
import { Tooltip } from '@@/Tip/Tooltip';
|
||||||
|
|
||||||
import { Service } from '../../types';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
export const ports: Column<Service> = {
|
export const ports = columnHelper.accessor(
|
||||||
Header: () => (
|
(row) =>
|
||||||
<>
|
row.Ports.map((port) => `${port.Port}:${port.NodePort}/${port.Protocol}`),
|
||||||
Ports
|
{
|
||||||
<Tooltip message="The format of Ports is port[:nodePort]/protocol. Protocol is either TCP, UDP or SCTP." />
|
header: () => (
|
||||||
</>
|
|
||||||
),
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<>
|
<>
|
||||||
{row.original.Ports.map((port, index) => {
|
Ports
|
||||||
if (port.NodePort !== 0) {
|
<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 (
|
return (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
{port.Port}:{port.NodePort}/{port.Protocol}
|
{port.Port}/{port.Protocol}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
sortingFn: (rowA, rowB) => {
|
||||||
|
const a = rowA.original.Ports;
|
||||||
|
const b = rowB.original.Ports;
|
||||||
|
|
||||||
return (
|
if (!a.length && !b.length) return 0;
|
||||||
<div key={index}>
|
|
||||||
{port.Port}/{port.Protocol}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
disableFilters: true,
|
|
||||||
canHide: true,
|
|
||||||
|
|
||||||
sortType: (rowA, rowB) => {
|
if (!a.length) return 1;
|
||||||
const a = rowA.original.Ports;
|
if (!b.length) return -1;
|
||||||
const b = rowB.original.Ports;
|
|
||||||
|
|
||||||
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 (portA === portB) {
|
||||||
if (!b.length) return -1;
|
// 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
|
// now do a regular number sort
|
||||||
const portA = a[0].Port;
|
if (portA < portB) return -1;
|
||||||
const portB = b[0].Port;
|
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;
|
return 0;
|
||||||
}
|
},
|
||||||
|
}
|
||||||
// now do a regular number sort
|
);
|
||||||
if (portA < portB) return -1;
|
|
||||||
if (portA > portB) return 1;
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -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> = {
|
if (!ports.length) {
|
||||||
Header: 'Target Ports',
|
return '-';
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
},
|
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,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
|
@ -1,22 +1,18 @@
|
||||||
import { CellProps, Column, Row } from 'react-table';
|
import { Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { filterHOC } from '@@/datatables/Filter';
|
import { filterHOC } from '@@/datatables/Filter';
|
||||||
|
|
||||||
import { Service } from '../../types';
|
import { Service } from '../../types';
|
||||||
|
|
||||||
export const type: Column<Service> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Type',
|
|
||||||
id: 'type',
|
|
||||||
accessor: (row) => row.Type,
|
|
||||||
Cell: ({ row }: CellProps<Service>) => <div>{row.original.Type}</div>,
|
|
||||||
canHide: true,
|
|
||||||
|
|
||||||
disableFilters: false,
|
export const type = columnHelper.accessor('Type', {
|
||||||
Filter: filterHOC('Filter by type'),
|
header: 'Type',
|
||||||
filter: (rows: Row<Service>[], _filterValue, filters) => {
|
id: 'type',
|
||||||
if (filters.length === 0) {
|
meta: {
|
||||||
return rows;
|
filter: filterHOC('Filter by type'),
|
||||||
}
|
|
||||||
return rows.filter((r) => filters.includes(r.original.Type));
|
|
||||||
},
|
},
|
||||||
};
|
enableColumnFilter: true,
|
||||||
|
filterFn: (row: Row<Service>, columnId: string, filterValue: string[]) =>
|
||||||
|
filterValue.length === 0 || filterValue.includes(row.original.Type),
|
||||||
|
});
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from '../../datatables/DefaultDatatableSettings';
|
} from '../../datatables/DefaultDatatableSettings';
|
||||||
|
|
||||||
export function createStore(storageKey: string) {
|
export function createStore(storageKey: string) {
|
||||||
return createPersistedStore<TableSettings>(storageKey, 'Name', (set) => ({
|
return createPersistedStore<TableSettings>(storageKey, 'name', (set) => ({
|
||||||
...refreshableSettings(set),
|
...refreshableSettings(set),
|
||||||
...systemResourcesSettings(set),
|
...systemResourcesSettings(set),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -7,6 +7,8 @@ import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||||
|
|
||||||
import { getNamespaces } from '../namespaces/service';
|
import { getNamespaces } from '../namespaces/service';
|
||||||
|
|
||||||
|
import { Service } from './types';
|
||||||
|
|
||||||
export const queryKeys = {
|
export const queryKeys = {
|
||||||
list: (environmentId: EnvironmentId) =>
|
list: (environmentId: EnvironmentId) =>
|
||||||
['environments', environmentId, 'kubernetes', 'services'] as const,
|
['environments', environmentId, 'kubernetes', 'services'] as const,
|
||||||
|
@ -18,7 +20,7 @@ async function getServices(
|
||||||
lookupApps: boolean
|
lookupApps: boolean
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { data: services } = await axios.get(
|
const { data: services } = await axios.get<Array<Service>>(
|
||||||
`kubernetes/${environmentId}/namespaces/${namespace}/services`,
|
`kubernetes/${environmentId}/namespaces/${namespace}/services`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { AlertTriangle, Database } from 'lucide-react';
|
import { Database, AlertTriangle } from 'lucide-react';
|
||||||
import { useStore } from 'zustand';
|
|
||||||
|
|
||||||
import { confirm } from '@@/modals/confirm';
|
import { confirm } from '@@/modals/confirm';
|
||||||
import { ModalType } from '@@/modals';
|
import { ModalType } from '@@/modals';
|
||||||
import { Datatable } from '@@/datatables';
|
import { Datatable } from '@@/datatables';
|
||||||
import { Button, ButtonGroup } from '@@/buttons';
|
import { Button, ButtonGroup } from '@@/buttons';
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
|
||||||
import { createPersistedStore } from '@@/datatables/types';
|
import { createPersistedStore } from '@@/datatables/types';
|
||||||
import { buildConfirmButton } from '@@/modals/utils';
|
import { buildConfirmButton } from '@@/modals/utils';
|
||||||
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { IngressControllerClassMap } from '../types';
|
import { IngressControllerClassMap } from '../types';
|
||||||
|
|
||||||
import { useColumns } from './columns';
|
import { columns } from './columns';
|
||||||
|
|
||||||
const storageKey = 'ingressClasses';
|
const storageKey = 'ingressClasses';
|
||||||
const settingsStore = createPersistedStore(storageKey);
|
const settingsStore = createPersistedStore(storageKey);
|
||||||
|
@ -39,12 +38,11 @@ export function IngressClassDatatable({
|
||||||
noIngressControllerLabel,
|
noIngressControllerLabel,
|
||||||
view,
|
view,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const settings = useStore(settingsStore);
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
|
||||||
const [ingControllerFormValues, setIngControllerFormValues] = useState(
|
const [ingControllerFormValues, setIngControllerFormValues] = useState(
|
||||||
ingressControllers || []
|
ingressControllers || []
|
||||||
);
|
);
|
||||||
const columns = useColumns();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (allowNoneIngressClass === undefined) {
|
if (allowNoneIngressClass === undefined) {
|
||||||
|
@ -81,6 +79,7 @@ export function IngressClassDatatable({
|
||||||
return (
|
return (
|
||||||
<div className="-mx-[15px]">
|
<div className="-mx-[15px]">
|
||||||
<Datatable
|
<Datatable
|
||||||
|
settingsManager={tableState}
|
||||||
dataset={ingControllerFormValues || []}
|
dataset={ingControllerFormValues || []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
@ -90,12 +89,6 @@ export function IngressClassDatatable({
|
||||||
getRowId={(row) => `${row.Name}-${row.ClassName}-${row.Type}`}
|
getRowId={(row) => `${row.Name}-${row.ClassName}-${row.Type}`}
|
||||||
renderTableActions={(selectedRows) => renderTableActions(selectedRows)}
|
renderTableActions={(selectedRows) => renderTableActions(selectedRows)}
|
||||||
description={renderIngressClassDescription()}
|
description={renderIngressClassDescription()}
|
||||||
initialPageSize={settings.pageSize}
|
|
||||||
onPageSizeChange={settings.setPageSize}
|
|
||||||
initialSortBy={settings.sortBy}
|
|
||||||
onSortByChange={settings.setSortBy}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { Check, X } from 'lucide-react';
|
import { Check, X } from 'lucide-react';
|
||||||
|
|
||||||
import { Badge } from '@@/Badge';
|
import { Badge } from '@@/Badge';
|
||||||
|
@ -6,23 +6,23 @@ import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
import type { IngressControllerClassMap } from '../../types';
|
import type { IngressControllerClassMap } from '../../types';
|
||||||
|
|
||||||
export const availability: Column<IngressControllerClassMap> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Availability',
|
|
||||||
accessor: 'Availability',
|
export const availability = columnHelper.accessor('Availability', {
|
||||||
Cell: AvailailityCell,
|
header: 'Availability',
|
||||||
id: 'availability',
|
cell: Cell,
|
||||||
disableFilters: true,
|
id: 'availability',
|
||||||
canHide: true,
|
invertSorting: true,
|
||||||
sortInverted: true,
|
sortingFn: 'basic',
|
||||||
sortType: 'basic',
|
});
|
||||||
Filter: () => null,
|
|
||||||
};
|
function Cell({ getValue }: CellContext<IngressControllerClassMap, boolean>) {
|
||||||
|
const availability = getValue();
|
||||||
|
|
||||||
function AvailailityCell({ value }: CellProps<IngressControllerClassMap>) {
|
|
||||||
return (
|
return (
|
||||||
<Badge type={value ? 'success' : 'danger'}>
|
<Badge type={availability ? 'success' : 'danger'}>
|
||||||
<Icon icon={value ? Check : X} className="!mr-1" />
|
<Icon icon={availability ? Check : X} className="!mr-1" />
|
||||||
{value ? 'Allowed' : 'Disallowed'}
|
{availability ? 'Allowed' : 'Disallowed'}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { IngressControllerClassMap } from '../../types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<IngressControllerClassMap>();
|
|
@ -1,9 +1,5 @@
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { availability } from './availability';
|
import { availability } from './availability';
|
||||||
import { type } from './type';
|
import { type } from './type';
|
||||||
import { name } from './name';
|
import { name } from './name';
|
||||||
|
|
||||||
export function useColumns() {
|
export const columns = [name, type, availability];
|
||||||
return useMemo(() => [name, type, availability], []);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,24 +1,26 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Badge } from '@@/Badge';
|
import { Badge } from '@@/Badge';
|
||||||
|
|
||||||
import type { IngressControllerClassMap } from '../../types';
|
import type { IngressControllerClassMap } from '../../types';
|
||||||
|
|
||||||
export const name: Column<IngressControllerClassMap> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Ingress class',
|
|
||||||
accessor: 'ClassName',
|
export const name = columnHelper.accessor('ClassName', {
|
||||||
Cell: NameCell,
|
header: 'Ingress class',
|
||||||
id: 'name',
|
cell: NameCell,
|
||||||
disableFilters: true,
|
id: 'name',
|
||||||
canHide: true,
|
});
|
||||||
Filter: () => null,
|
|
||||||
sortType: 'string',
|
function NameCell({
|
||||||
};
|
row,
|
||||||
|
getValue,
|
||||||
|
}: CellContext<IngressControllerClassMap, string>) {
|
||||||
|
const className = getValue();
|
||||||
|
|
||||||
function NameCell({ row }: CellProps<IngressControllerClassMap>) {
|
|
||||||
return (
|
return (
|
||||||
<span className="flex flex-nowrap">
|
<span className="flex flex-nowrap">
|
||||||
{row.original.ClassName}
|
{className}
|
||||||
{row.original.New && <Badge className="ml-1">Newly detected</Badge>}
|
{row.original.New && <Badge className="ml-1">Newly detected</Badge>}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,14 +1,10 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import type { IngressControllerClassMap } from '../../types';
|
export const type = columnHelper.accessor('Type', {
|
||||||
|
header: 'Ingress controller type',
|
||||||
export const type: Column<IngressControllerClassMap> = {
|
cell: ({ getValue }) => {
|
||||||
Header: 'Ingress controller type',
|
const type = getValue();
|
||||||
accessor: 'Type',
|
return type || '-';
|
||||||
Cell: ({ row }: CellProps<IngressControllerClassMap>) =>
|
},
|
||||||
row.original.Type || '-',
|
|
||||||
id: 'type',
|
id: 'type',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
Filter: () => null,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Plus, Trash2 } from 'lucide-react';
|
import { Plus, Trash2 } from 'lucide-react';
|
||||||
import { useRouter } from '@uirouter/react';
|
import { useRouter } from '@uirouter/react';
|
||||||
import { useStore } from 'zustand';
|
|
||||||
|
|
||||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||||
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
|
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
|
||||||
|
@ -12,7 +11,7 @@ import { Datatable } from '@@/datatables';
|
||||||
import { Button } from '@@/buttons';
|
import { Button } from '@@/buttons';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
import { createPersistedStore } from '@@/datatables/types';
|
import { createPersistedStore } from '@@/datatables/types';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { DeleteIngressesRequest, Ingress } from '../types';
|
import { DeleteIngressesRequest, Ingress } from '../types';
|
||||||
import { useDeleteIngresses, useIngresses } from '../queries';
|
import { useDeleteIngresses, useIngresses } from '../queries';
|
||||||
|
@ -39,13 +38,13 @@ export function IngressDatatable() {
|
||||||
);
|
);
|
||||||
|
|
||||||
const deleteIngressesMutation = useDeleteIngresses();
|
const deleteIngressesMutation = useDeleteIngresses();
|
||||||
const settings = useStore(settingsStore);
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datatable
|
<Datatable
|
||||||
|
settingsManager={tableState}
|
||||||
dataset={ingressesQuery.data || []}
|
dataset={ingressesQuery.data || []}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
isLoading={ingressesQuery.isLoading}
|
isLoading={ingressesQuery.isLoading}
|
||||||
|
@ -55,12 +54,6 @@ export function IngressDatatable() {
|
||||||
getRowId={(row) => row.Name + row.Type + row.Namespace}
|
getRowId={(row) => row.Name + row.Type + row.Namespace}
|
||||||
renderTableActions={tableActions}
|
renderTableActions={tableActions}
|
||||||
disableSelect={useCheckboxes()}
|
disableSelect={useCheckboxes()}
|
||||||
initialPageSize={settings.pageSize}
|
|
||||||
onPageSizeChange={settings.setPageSize}
|
|
||||||
initialSortBy={settings.sortBy}
|
|
||||||
onSortByChange={settings.setSortBy}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { Ingress } from '../../types';
|
export const className = columnHelper.accessor('ClassName', {
|
||||||
|
header: 'Class Name',
|
||||||
export const className: Column<Ingress> = {
|
|
||||||
Header: 'Class Name',
|
|
||||||
accessor: 'ClassName',
|
|
||||||
id: 'className',
|
id: 'className',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,23 +1,19 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
|
||||||
|
|
||||||
import { formatDate } from '@/portainer/filters/filters';
|
import { formatDate } from '@/portainer/filters/filters';
|
||||||
|
|
||||||
import { Ingress } from '../../types';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
export const created: Column<Ingress> = {
|
export const created = columnHelper.accessor('CreationDate', {
|
||||||
Header: 'Created',
|
header: 'Created',
|
||||||
id: 'created',
|
cell: ({ row, getValue }) => {
|
||||||
accessor: (row) => row.CreationDate,
|
const date = formatDate(getValue());
|
||||||
Cell: ({ row }: CellProps<Ingress>) => {
|
|
||||||
const owner =
|
const owner =
|
||||||
row.original.Labels?.['io.portainer.kubernetes.ingress.owner'];
|
row.original.Labels?.['io.portainer.kubernetes.ingress.owner'];
|
||||||
|
|
||||||
if (owner) {
|
if (owner) {
|
||||||
return `${formatDate(row.original.CreationDate)} by ${owner}`;
|
return `${date} by ${owner}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return formatDate(row.original.CreationDate);
|
return date;
|
||||||
},
|
},
|
||||||
disableFilters: true,
|
id: 'created',
|
||||||
canHide: true,
|
});
|
||||||
};
|
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Ingress } from '../../types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<Ingress>();
|
|
@ -1,4 +1,4 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
import { AlertTriangle, ArrowRight } from 'lucide-react';
|
import { AlertTriangle, ArrowRight } from 'lucide-react';
|
||||||
|
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
|
@ -6,6 +6,41 @@ import { Badge } from '@@/Badge';
|
||||||
|
|
||||||
import { Ingress, TLS, Path } from '../../types';
|
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't exist
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function isHTTP(TLSs: TLS[], host: string) {
|
function isHTTP(TLSs: TLS[], host: string) {
|
||||||
return TLSs.filter((t) => t.Hosts.indexOf(host) !== -1).length === 0;
|
return TLSs.filter((t) => t.Hosts.indexOf(host) !== -1).length === 0;
|
||||||
}
|
}
|
||||||
|
@ -24,33 +59,3 @@ function link(host: string, path: string, isHttp: boolean) {
|
||||||
</a>
|
</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't exist
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return results || <div />;
|
|
||||||
},
|
|
||||||
id: 'ingressRules',
|
|
||||||
disableFilters: true,
|
|
||||||
canHide: true,
|
|
||||||
disableSortBy: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Authorized } from '@/react/hooks/useUser';
|
import { Authorized } from '@/react/hooks/useUser';
|
||||||
|
|
||||||
|
@ -6,28 +6,30 @@ import { Link } from '@@/Link';
|
||||||
|
|
||||||
import { Ingress } from '../../types';
|
import { Ingress } from '../../types';
|
||||||
|
|
||||||
export const name: Column<Ingress> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Name',
|
|
||||||
accessor: 'Name',
|
export const name = columnHelper.accessor('Name', {
|
||||||
Cell: ({ row }: CellProps<Ingress>) => (
|
header: 'Name',
|
||||||
<Authorized
|
cell: Cell,
|
||||||
authorizations="K8sIngressesW"
|
id: 'name',
|
||||||
childrenUnauthorized={row.original.Name}
|
});
|
||||||
>
|
|
||||||
|
function Cell({ row, getValue }: CellContext<Ingress, string>) {
|
||||||
|
const name = getValue();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Authorized authorizations="K8sIngressesW" childrenUnauthorized={name}>
|
||||||
<Link
|
<Link
|
||||||
to="kubernetes.ingresses.edit"
|
to="kubernetes.ingresses.edit"
|
||||||
params={{
|
params={{
|
||||||
uid: row.original.UID,
|
uid: row.original.UID,
|
||||||
namespace: row.original.Namespace,
|
namespace: row.original.Namespace,
|
||||||
name: row.original.Name,
|
name,
|
||||||
}}
|
}}
|
||||||
title={row.original.Name}
|
title={name}
|
||||||
>
|
>
|
||||||
{row.original.Name}
|
{name}
|
||||||
</Link>
|
</Link>
|
||||||
</Authorized>
|
</Authorized>
|
||||||
),
|
);
|
||||||
id: 'name',
|
}
|
||||||
disableFilters: true,
|
|
||||||
canHide: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,33 +1,40 @@
|
||||||
import { CellProps, Column, Row } from 'react-table';
|
import { CellContext, Row } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { filterHOC } from '@/react/components/datatables/Filter';
|
|
||||||
|
|
||||||
|
import { filterHOC } from '@@/datatables/Filter';
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
|
|
||||||
import { Ingress } from '../../types';
|
import { Ingress } from '../../types';
|
||||||
|
|
||||||
export const namespace: Column<Ingress> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Namespace',
|
|
||||||
accessor: 'Namespace',
|
export const namespace = columnHelper.accessor('Namespace', {
|
||||||
Cell: ({ row }: CellProps<Ingress>) => (
|
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
|
<Link
|
||||||
to="kubernetes.resourcePools.resourcePool"
|
to="kubernetes.resourcePools.resourcePool"
|
||||||
params={{
|
params={{
|
||||||
id: row.original.Namespace,
|
id: namespace,
|
||||||
}}
|
}}
|
||||||
title={row.original.Namespace}
|
title={namespace}
|
||||||
>
|
>
|
||||||
{row.original.Namespace}
|
{namespace}
|
||||||
</Link>
|
</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));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { Ingress } from '../../types';
|
export const type = columnHelper.accessor('Type', {
|
||||||
|
header: 'Type',
|
||||||
export const type: Column<Ingress> = {
|
|
||||||
Header: 'Type',
|
|
||||||
accessor: 'Type',
|
|
||||||
id: 'type',
|
id: 'type',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,39 +1,31 @@
|
||||||
import { History } from 'lucide-react';
|
import { History } from 'lucide-react';
|
||||||
import { useStore } from 'zustand';
|
|
||||||
|
|
||||||
import { NomadEvent } from '@/react/nomad/types';
|
import { NomadEvent } from '@/react/nomad/types';
|
||||||
|
|
||||||
import { Datatable } from '@@/datatables';
|
import { Datatable } from '@@/datatables';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
|
||||||
import { createPersistedStore } from '@@/datatables/types';
|
import { createPersistedStore } from '@@/datatables/types';
|
||||||
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { useColumns } from './columns';
|
import { columns } from './columns';
|
||||||
|
|
||||||
export interface EventsDatatableProps {
|
export interface EventsDatatableProps {
|
||||||
data: NomadEvent[];
|
data: NomadEvent[];
|
||||||
isLoading: boolean;
|
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) {
|
export function EventsDatatable({ data, isLoading }: EventsDatatableProps) {
|
||||||
const columns = useColumns();
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
const settings = useStore(settingsStore);
|
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Datatable
|
<Datatable
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
settingsManager={tableState}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataset={data}
|
dataset={data}
|
||||||
initialPageSize={settings.pageSize}
|
|
||||||
onPageSizeChange={settings.setPageSize}
|
|
||||||
initialSortBy={settings.sortBy}
|
|
||||||
onSortByChange={settings.setSortBy}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
titleIcon={History}
|
titleIcon={History}
|
||||||
title="Events"
|
title="Events"
|
||||||
totalCount={data.length}
|
totalCount={data.length}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { Column } from 'react-table';
|
|
||||||
|
|
||||||
import { NomadEvent } from '@/react/nomad/types';
|
|
||||||
import { isoDate } from '@/portainer/filters/filters';
|
import { isoDate } from '@/portainer/filters/filters';
|
||||||
|
|
||||||
export const date: Column<NomadEvent> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Date',
|
|
||||||
accessor: (row) => (row.Date ? isoDate(row.Date) : '-'),
|
export const date = columnHelper.accessor('Date', {
|
||||||
|
header: 'Date',
|
||||||
id: 'date',
|
id: 'date',
|
||||||
disableFilters: true,
|
cell: ({ getValue }) => {
|
||||||
canHide: true,
|
const date = getValue();
|
||||||
};
|
return date ? isoDate(date) : '-';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { NomadEvent } from '@/react/nomad/types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<NomadEvent>();
|
|
@ -1,9 +1,5 @@
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { date } from './date';
|
import { date } from './date';
|
||||||
import { type } from './type';
|
import { type } from './type';
|
||||||
import { message } from './message';
|
import { message } from './message';
|
||||||
|
|
||||||
export function useColumns() {
|
export const columns = [date, type, message];
|
||||||
return useMemo(() => [date, type, message], []);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { NomadEvent } from '@/react/nomad/types';
|
export const message = columnHelper.accessor('Message', {
|
||||||
|
header: 'Message',
|
||||||
export const message: Column<NomadEvent> = {
|
|
||||||
Header: 'Message',
|
|
||||||
accessor: 'Message',
|
|
||||||
id: 'message',
|
id: 'message',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { NomadEvent } from '@/react/nomad/types';
|
export const type = columnHelper.accessor('Type', {
|
||||||
|
header: 'Type',
|
||||||
export const type: Column<NomadEvent> = {
|
|
||||||
Header: 'Type',
|
|
||||||
accessor: 'Type',
|
|
||||||
id: 'type',
|
id: 'type',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { useStore } from 'zustand';
|
|
||||||
import { Clock } from 'lucide-react';
|
import { Clock } from 'lucide-react';
|
||||||
|
|
||||||
import { Job } from '@/react/nomad/types';
|
import { Job } from '@/react/nomad/types';
|
||||||
|
|
||||||
import { useRepeater } from '@@/datatables/useRepeater';
|
import { useRepeater } from '@@/datatables/useRepeater';
|
||||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||||
import { TableSettingsMenu } from '@@/datatables';
|
import { TableSettingsMenu } from '@@/datatables/TableSettingsMenu';
|
||||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
import { useTableState } from '@@/datatables/useTableState';
|
||||||
|
|
||||||
import { TasksDatatable } from './TasksDatatable';
|
import { TasksDatatable } from './TasksDatatable';
|
||||||
import { columns } from './columns';
|
import { columns } from './columns';
|
||||||
|
@ -19,7 +18,7 @@ export interface JobsDatatableProps {
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const storageKey = 'jobs';
|
const storageKey = 'nomad_jobs';
|
||||||
const settingsStore = createStore(storageKey);
|
const settingsStore = createStore(storageKey);
|
||||||
|
|
||||||
export function JobsDatatable({
|
export function JobsDatatable({
|
||||||
|
@ -27,20 +26,14 @@ export function JobsDatatable({
|
||||||
refreshData,
|
refreshData,
|
||||||
isLoading,
|
isLoading,
|
||||||
}: JobsDatatableProps) {
|
}: JobsDatatableProps) {
|
||||||
const [search, setSearch] = useSearchBarState(storageKey);
|
const tableState = useTableState(settingsStore, storageKey);
|
||||||
const settings = useStore(settingsStore);
|
useRepeater(tableState.autoRefreshRate, refreshData);
|
||||||
useRepeater(settings.autoRefreshRate, refreshData);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpandableDatatable<Job>
|
<ExpandableDatatable
|
||||||
dataset={jobs}
|
dataset={jobs}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
initialPageSize={settings.pageSize}
|
settingsManager={tableState}
|
||||||
onPageSizeChange={settings.setPageSize}
|
|
||||||
initialSortBy={settings.sortBy}
|
|
||||||
onSortByChange={settings.setSortBy}
|
|
||||||
searchValue={search}
|
|
||||||
onSearchChange={setSearch}
|
|
||||||
title="Nomad Jobs"
|
title="Nomad Jobs"
|
||||||
titleIcon={Clock}
|
titleIcon={Clock}
|
||||||
disableSelect
|
disableSelect
|
||||||
|
@ -49,9 +42,10 @@ export function JobsDatatable({
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
renderTableSettings={() => (
|
renderTableSettings={() => (
|
||||||
<TableSettingsMenu>
|
<TableSettingsMenu>
|
||||||
<JobsDatatableSettings settings={settings} />
|
<JobsDatatableSettings settings={tableState} />
|
||||||
</TableSettingsMenu>
|
</TableSettingsMenu>
|
||||||
)}
|
)}
|
||||||
|
expandOnRowClick
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,20 +2,18 @@ import { Task } from '@/react/nomad/types';
|
||||||
|
|
||||||
import { NestedDatatable } from '@@/datatables/NestedDatatable';
|
import { NestedDatatable } from '@@/datatables/NestedDatatable';
|
||||||
|
|
||||||
import { useColumns } from './columns';
|
import { columns } from './columns';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
data: Task[];
|
data: Task[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TasksDatatable({ data }: Props) {
|
export function TasksDatatable({ data }: Props) {
|
||||||
const columns = useColumns();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NestedDatatable
|
<NestedDatatable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataset={data}
|
dataset={data}
|
||||||
defaultSortBy="taskName"
|
initialSortBy={{ id: 'taskName', desc: false }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
import { CellProps, Column } from 'react-table';
|
|
||||||
import { Clock, FileText } from 'lucide-react';
|
import { Clock, FileText } from 'lucide-react';
|
||||||
|
import { CellContext } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { Task } from '@/react/nomad/types';
|
import { Task } from '@/react/nomad/types';
|
||||||
|
|
||||||
import { Link } from '@@/Link';
|
import { Link } from '@@/Link';
|
||||||
import { Icon } from '@@/Icon';
|
import { Icon } from '@@/Icon';
|
||||||
|
|
||||||
export const actions: Column<Task> = {
|
import { columnHelper } from './helper';
|
||||||
Header: 'Task Actions',
|
|
||||||
id: 'actions',
|
|
||||||
disableFilters: true,
|
|
||||||
canHide: true,
|
|
||||||
disableResizing: true,
|
|
||||||
width: '5px',
|
|
||||||
sortType: 'string',
|
|
||||||
Filter: () => null,
|
|
||||||
Cell: ActionsCell,
|
|
||||||
};
|
|
||||||
|
|
||||||
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 = {
|
const params = {
|
||||||
allocationID: row.original.AllocationID,
|
allocationID: row.original.AllocationID,
|
||||||
taskName: row.original.TaskName,
|
taskName: row.original.TaskName,
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { Task } from '@/react/nomad/types';
|
export const allocationID = columnHelper.accessor('AllocationID', {
|
||||||
|
header: 'Allocation ID',
|
||||||
export const allocationID: Column<Task> = {
|
|
||||||
Header: 'Allocation ID',
|
|
||||||
accessor: (row) => row.AllocationID || '-',
|
|
||||||
id: 'allocationID',
|
id: 'allocationID',
|
||||||
disableFilters: true,
|
cell: ({ getValue }) => {
|
||||||
canHide: true,
|
const value = getValue();
|
||||||
};
|
return value || '-';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createColumnHelper } from '@tanstack/react-table';
|
||||||
|
|
||||||
|
import { Task } from '@/react/nomad/types';
|
||||||
|
|
||||||
|
export const columnHelper = createColumnHelper<Task>();
|
|
@ -1,5 +1,3 @@
|
||||||
import { useMemo } from 'react';
|
|
||||||
|
|
||||||
import { taskStatus } from './taskStatus';
|
import { taskStatus } from './taskStatus';
|
||||||
import { taskName } from './taskName';
|
import { taskName } from './taskName';
|
||||||
import { taskGroup } from './taskGroup';
|
import { taskGroup } from './taskGroup';
|
||||||
|
@ -7,9 +5,11 @@ import { allocationID } from './allocationID';
|
||||||
import { started } from './started';
|
import { started } from './started';
|
||||||
import { actions } from './actions';
|
import { actions } from './actions';
|
||||||
|
|
||||||
export function useColumns() {
|
export const columns = [
|
||||||
return useMemo(
|
taskStatus,
|
||||||
() => [taskStatus, taskName, taskGroup, allocationID, actions, started],
|
taskName,
|
||||||
[]
|
taskGroup,
|
||||||
);
|
allocationID,
|
||||||
}
|
actions,
|
||||||
|
started,
|
||||||
|
];
|
||||||
|
|
|
@ -1,19 +1,17 @@
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { Column } from 'react-table';
|
|
||||||
|
|
||||||
import { Task } from '@/react/nomad/types';
|
import { Task } from '@/react/nomad/types';
|
||||||
import { isoDate } from '@/portainer/filters/filters';
|
import { isoDate } from '@/portainer/filters/filters';
|
||||||
|
|
||||||
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
function accessor(row: Task) {
|
function accessor(row: Task) {
|
||||||
const momentDate = moment(row.StartedAt);
|
const momentDate = moment(row.StartedAt);
|
||||||
const isValid = momentDate.unix() > 0;
|
const isValid = momentDate.unix() > 0;
|
||||||
return isValid ? isoDate(momentDate) : '-';
|
return isValid ? isoDate(momentDate) : '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const started: Column<Task> = {
|
export const started = columnHelper.accessor(accessor, {
|
||||||
accessor,
|
header: 'Started',
|
||||||
Header: 'Started',
|
|
||||||
id: 'startedName',
|
id: 'startedName',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { Task } from '@/react/nomad/types';
|
export const taskGroup = columnHelper.accessor('TaskGroup', {
|
||||||
|
header: 'Task Group',
|
||||||
export const taskGroup: Column<Task> = {
|
|
||||||
Header: 'Task Group',
|
|
||||||
accessor: (row) => row.TaskGroup || '-',
|
|
||||||
id: 'taskGroup',
|
id: 'taskGroup',
|
||||||
disableFilters: true,
|
cell: ({ getValue }) => {
|
||||||
canHide: true,
|
const value = getValue();
|
||||||
};
|
return value || '-';
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import { Column } from 'react-table';
|
import { columnHelper } from './helper';
|
||||||
|
|
||||||
import { Task } from '@/react/nomad/types';
|
export const taskName = columnHelper.accessor('TaskName', {
|
||||||
|
header: 'Task Name',
|
||||||
export const taskName: Column<Task> = {
|
|
||||||
Header: 'Task Name',
|
|
||||||
accessor: (row) => row.TaskName || '-',
|
|
||||||
id: 'taskName',
|
id: 'taskName',
|
||||||
disableFilters: true,
|
});
|
||||||
canHide: true,
|
|
||||||
};
|
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue