mirror of
https://github.com/portainer/portainer.git
synced 2025-07-23 07:19:41 +02:00
refactor(containers): migrate view to react [EE-2212] (#6577)
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
This commit is contained in:
parent
5ee570e075
commit
bed4257194
71 changed files with 1616 additions and 875 deletions
238
app/react/components/datatables/Datatable.tsx
Normal file
238
app/react/components/datatables/Datatable.tsx
Normal file
|
@ -0,0 +1,238 @@
|
|||
import {
|
||||
useTable,
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
Column,
|
||||
Row,
|
||||
TableInstance,
|
||||
TableState,
|
||||
} from 'react-table';
|
||||
import { ReactNode } from 'react';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
|
||||
import { Table } from './Table';
|
||||
import { multiple } from './filter-types';
|
||||
import { SearchBar, useSearchBarState } from './SearchBar';
|
||||
import { SelectedRowsCount } from './SelectedRowsCount';
|
||||
import { TableSettingsProvider } from './useZustandTableSettings';
|
||||
import { useRowSelect } from './useRowSelect';
|
||||
import { PaginationTableSettings, SortableTableSettings } from './types';
|
||||
|
||||
interface DefaultTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
||||
|
||||
interface TitleOptionsVisible {
|
||||
title: string;
|
||||
icon?: string;
|
||||
hide?: never;
|
||||
}
|
||||
|
||||
type TitleOptions = TitleOptionsVisible | { hide: true };
|
||||
|
||||
interface Props<
|
||||
D extends Record<string, unknown>,
|
||||
TSettings extends DefaultTableSettings
|
||||
> {
|
||||
dataset: D[];
|
||||
storageKey: string;
|
||||
columns: readonly Column<D>[];
|
||||
renderTableSettings?(instance: TableInstance<D>): ReactNode;
|
||||
renderTableActions?(selectedRows: D[]): ReactNode;
|
||||
settingsStore: TSettings;
|
||||
disableSelect?: boolean;
|
||||
getRowId?(row: D): string;
|
||||
isRowSelectable?(row: Row<D>): boolean;
|
||||
emptyContentLabel?: string;
|
||||
titleOptions: TitleOptions;
|
||||
initialTableState?: Partial<TableState<D>>;
|
||||
isLoading?: boolean;
|
||||
totalCount?: number;
|
||||
}
|
||||
|
||||
export function Datatable<
|
||||
D extends Record<string, unknown>,
|
||||
TSettings extends DefaultTableSettings
|
||||
>({
|
||||
columns,
|
||||
dataset,
|
||||
storageKey,
|
||||
renderTableSettings,
|
||||
renderTableActions,
|
||||
settingsStore,
|
||||
disableSelect,
|
||||
getRowId = defaultGetRowId,
|
||||
isRowSelectable = () => true,
|
||||
titleOptions,
|
||||
emptyContentLabel,
|
||||
initialTableState = {},
|
||||
isLoading,
|
||||
totalCount = dataset.length,
|
||||
}: Props<D, TSettings>) {
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
||||
|
||||
const tableInstance = useTable<D>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: dataset,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
pageSize: settingsStore.pageSize || 10,
|
||||
sortBy: [settingsStore.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
...initialTableState,
|
||||
},
|
||||
isRowSelectable,
|
||||
autoResetSelectedRows: false,
|
||||
getRowId,
|
||||
stateReducer: (newState, action) => {
|
||||
switch (action.type) {
|
||||
case 'setGlobalFilter':
|
||||
setSearchBarValue(action.filterValue);
|
||||
break;
|
||||
case 'toggleSortBy':
|
||||
settingsStore.setSortBy(action.columnId, action.desc);
|
||||
break;
|
||||
case 'setPageSize':
|
||||
settingsStore.setPageSize(action.pageSize);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return newState;
|
||||
},
|
||||
},
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
!disableSelect ? useRowSelectColumn : emptyPlugin
|
||||
);
|
||||
|
||||
const {
|
||||
selectedFlatRows,
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = tableInstance;
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
const selectedItems = selectedFlatRows.map((row) => row.original);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableSettingsProvider settings={settingsStore}>
|
||||
<Table.Container>
|
||||
{isTitleVisible(titleOptions) && (
|
||||
<Table.Title label={titleOptions.title} icon={titleOptions.icon}>
|
||||
<SearchBar value={searchBarValue} onChange={setGlobalFilter} />
|
||||
{renderTableActions && (
|
||||
<Table.Actions>
|
||||
{renderTableActions(selectedItems)}
|
||||
</Table.Actions>
|
||||
)}
|
||||
<Table.TitleActions>
|
||||
{!!renderTableSettings && renderTableSettings(tableInstance)}
|
||||
</Table.TitleActions>
|
||||
</Table.Title>
|
||||
)}
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<Table.HeaderRow<D>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<Table.Content<D>
|
||||
rows={page}
|
||||
isLoading={isLoading}
|
||||
prepareRow={prepareRow}
|
||||
emptyContent={emptyContentLabel}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<Table.Row<D>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
<Table.Footer>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={setPageSize}
|
||||
/>
|
||||
</Table.Footer>
|
||||
</Table.Container>
|
||||
</TableSettingsProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function isTitleVisible(
|
||||
titleSettings: TitleOptions
|
||||
): titleSettings is TitleOptionsVisible {
|
||||
return !titleSettings.hide;
|
||||
}
|
||||
|
||||
function defaultGetRowId<D extends Record<string, unknown>>(row: D): string {
|
||||
if (row.id && (typeof row.id === 'string' || typeof row.id === 'number')) {
|
||||
return row.id.toString();
|
||||
}
|
||||
|
||||
if (row.Id && (typeof row.Id === 'string' || typeof row.Id === 'number')) {
|
||||
return row.Id.toString();
|
||||
}
|
||||
|
||||
if (row.ID && (typeof row.ID === 'string' || typeof row.ID === 'number')) {
|
||||
return row.ID.toString();
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
function emptyPlugin() {}
|
||||
|
||||
emptyPlugin.pluginName = 'emptyPlugin';
|
|
@ -1,9 +1,14 @@
|
|||
import {
|
||||
SettableQuickActionsTableSettings,
|
||||
QuickAction,
|
||||
} from '@/react/docker/containers/ListView/ContainersDatatable/types';
|
||||
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
|
||||
import { useTableSettings } from './useTableSettings';
|
||||
import { useTableSettings } from './useZustandTableSettings';
|
||||
|
||||
export interface Action {
|
||||
id: string;
|
||||
id: QuickAction;
|
||||
label: string;
|
||||
}
|
||||
|
||||
|
@ -11,13 +16,9 @@ interface Props {
|
|||
actions: Action[];
|
||||
}
|
||||
|
||||
export interface QuickActionsSettingsType {
|
||||
hiddenQuickActions: string[];
|
||||
}
|
||||
|
||||
export function QuickActionsSettings({ actions }: Props) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<QuickActionsSettingsType>();
|
||||
const { settings } =
|
||||
useTableSettings<SettableQuickActionsTableSettings<QuickAction>>();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -33,16 +34,17 @@ export function QuickActionsSettings({ actions }: Props) {
|
|||
</>
|
||||
);
|
||||
|
||||
function toggleAction(key: string, value: boolean) {
|
||||
setTableSettings(({ hiddenQuickActions = [], ...settings }) => ({
|
||||
...settings,
|
||||
hiddenQuickActions: value
|
||||
? hiddenQuickActions.filter((id) => id !== key)
|
||||
: [...hiddenQuickActions, key],
|
||||
}));
|
||||
function toggleAction(key: QuickAction, visible: boolean) {
|
||||
if (!visible) {
|
||||
settings.setHiddenQuickActions([...settings.hiddenQuickActions, key]);
|
||||
} else {
|
||||
settings.setHiddenQuickActions(
|
||||
settings.hiddenQuickActions.filter((action) => action !== key)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAction(id: string, label: string): Action {
|
||||
export function buildAction(id: QuickAction, label: string): Action {
|
||||
return { id, label };
|
||||
}
|
||||
|
|
23
app/react/components/datatables/RowContext.tsx
Normal file
23
app/react/components/datatables/RowContext.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||
|
||||
export function createRowContext<TContext>() {
|
||||
const Context = createContext<TContext | null>(null);
|
||||
|
||||
return { RowProvider, useRowContext };
|
||||
|
||||
function RowProvider({
|
||||
children,
|
||||
context,
|
||||
}: PropsWithChildren<{ context: TContext }>) {
|
||||
return <Context.Provider value={context}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
function useRowContext() {
|
||||
const context = useContext(Context);
|
||||
if (!context) {
|
||||
throw new Error('should be nested under RowProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
}
|
|
@ -2,9 +2,18 @@ import clsx from 'clsx';
|
|||
import { PropsWithChildren } from 'react';
|
||||
import { TableProps } from 'react-table';
|
||||
|
||||
import { useTableContext } from './TableContainer';
|
||||
import { useTableContext, TableContainer } from './TableContainer';
|
||||
import { TableActions } from './TableActions';
|
||||
import { TableTitleActions } from './TableTitleActions';
|
||||
import { TableHeaderCell } from './TableHeaderCell';
|
||||
import { TableSettingsMenu } from './TableSettingsMenu';
|
||||
import { TableTitle } from './TableTitle';
|
||||
import { TableHeaderRow } from './TableHeaderRow';
|
||||
import { TableRow } from './TableRow';
|
||||
import { TableContent } from './TableContent';
|
||||
import { TableFooter } from './TableFooter';
|
||||
|
||||
export function Table({
|
||||
function MainComponent({
|
||||
children,
|
||||
className,
|
||||
role,
|
||||
|
@ -27,3 +36,30 @@ export function Table({
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SubComponents {
|
||||
Container: typeof TableContainer;
|
||||
Actions: typeof TableActions;
|
||||
TitleActions: typeof TableTitleActions;
|
||||
HeaderCell: typeof TableHeaderCell;
|
||||
SettingsMenu: typeof TableSettingsMenu;
|
||||
Title: typeof TableTitle;
|
||||
Row: typeof TableRow;
|
||||
HeaderRow: typeof TableHeaderRow;
|
||||
Content: typeof TableContent;
|
||||
Footer: typeof TableFooter;
|
||||
}
|
||||
|
||||
export const Table: typeof MainComponent & SubComponents =
|
||||
MainComponent as typeof MainComponent & SubComponents;
|
||||
|
||||
Table.Actions = TableActions;
|
||||
Table.TitleActions = TableTitleActions;
|
||||
Table.Container = TableContainer;
|
||||
Table.HeaderCell = TableHeaderCell;
|
||||
Table.SettingsMenu = TableSettingsMenu;
|
||||
Table.Title = TableTitle;
|
||||
Table.Row = TableRow;
|
||||
Table.HeaderRow = TableHeaderRow;
|
||||
Table.Content = TableContent;
|
||||
Table.Footer = TableFooter;
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Cell, TableRowProps } from 'react-table';
|
|||
import { useTableContext } from './TableContainer';
|
||||
|
||||
interface Props<D extends Record<string, unknown> = Record<string, unknown>>
|
||||
extends TableRowProps {
|
||||
extends Omit<TableRowProps, 'key'> {
|
||||
cells: Cell<D>[];
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
import { ComponentType, PropsWithChildren, ReactNode } from 'react';
|
||||
|
||||
import { Icon, IconProps } from '@@/Icon';
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
import { useTableContext } from './TableContainer';
|
||||
|
||||
interface Props extends IconProps {
|
||||
interface Props {
|
||||
icon?: ReactNode | ComponentType<unknown>;
|
||||
featherIcon?: boolean;
|
||||
label: string;
|
||||
}
|
||||
|
||||
|
@ -19,13 +21,15 @@ export function TableTitle({
|
|||
return (
|
||||
<div className="toolBar">
|
||||
<div className="toolBarTitle">
|
||||
<div className="widget-icon">
|
||||
<Icon
|
||||
icon={icon}
|
||||
feather={featherIcon}
|
||||
className="space-right feather"
|
||||
/>
|
||||
</div>
|
||||
{icon && (
|
||||
<div className="widget-icon">
|
||||
<Icon
|
||||
icon={icon}
|
||||
feather={featherIcon}
|
||||
className="space-right feather"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{label}
|
||||
</div>
|
||||
|
|
13
app/react/components/datatables/index.ts
Normal file
13
app/react/components/datatables/index.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
export { Datatable } from './Datatable';
|
||||
|
||||
export { Table } from './Table';
|
||||
export { TableActions } from './TableActions';
|
||||
export { TableTitleActions } from './TableTitleActions';
|
||||
export { TableHeaderCell } from './TableHeaderCell';
|
||||
export { TableSettingsMenu } from './TableSettingsMenu';
|
||||
export { TableTitle } from './TableTitle';
|
||||
export { TableContainer } from './TableContainer';
|
||||
export { TableHeaderRow } from './TableHeaderRow';
|
||||
export { TableRow } from './TableRow';
|
||||
export { TableContent } from './TableContent';
|
||||
export { TableFooter } from './TableFooter';
|
|
@ -1,50 +0,0 @@
|
|||
import { Table as MainComponent } from './Table';
|
||||
import { TableActions } from './TableActions';
|
||||
import { TableTitleActions } from './TableTitleActions';
|
||||
import { TableHeaderCell } from './TableHeaderCell';
|
||||
import { TableSettingsMenu } from './TableSettingsMenu';
|
||||
import { TableTitle } from './TableTitle';
|
||||
import { TableContainer } from './TableContainer';
|
||||
import { TableHeaderRow } from './TableHeaderRow';
|
||||
import { TableRow } from './TableRow';
|
||||
import { TableContent } from './TableContent';
|
||||
import { TableFooter } from './TableFooter';
|
||||
|
||||
interface SubComponents {
|
||||
Container: typeof TableContainer;
|
||||
Actions: typeof TableActions;
|
||||
TitleActions: typeof TableTitleActions;
|
||||
HeaderCell: typeof TableHeaderCell;
|
||||
SettingsMenu: typeof TableSettingsMenu;
|
||||
Title: typeof TableTitle;
|
||||
Row: typeof TableRow;
|
||||
HeaderRow: typeof TableHeaderRow;
|
||||
Content: typeof TableContent;
|
||||
Footer: typeof TableFooter;
|
||||
}
|
||||
|
||||
const Table: typeof MainComponent & SubComponents =
|
||||
MainComponent as typeof MainComponent & SubComponents;
|
||||
|
||||
Table.Actions = TableActions;
|
||||
Table.TitleActions = TableTitleActions;
|
||||
Table.Container = TableContainer;
|
||||
Table.HeaderCell = TableHeaderCell;
|
||||
Table.SettingsMenu = TableSettingsMenu;
|
||||
Table.Title = TableTitle;
|
||||
Table.Row = TableRow;
|
||||
Table.HeaderRow = TableHeaderRow;
|
||||
Table.Content = TableContent;
|
||||
Table.Footer = TableFooter;
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableActions,
|
||||
TableTitleActions,
|
||||
TableHeaderCell,
|
||||
TableSettingsMenu,
|
||||
TableTitle,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
};
|
15
app/react/components/datatables/types-old.ts
Normal file
15
app/react/components/datatables/types-old.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export interface PaginationTableSettings {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface SortableTableSettings {
|
||||
sortBy: { id: string; desc: boolean };
|
||||
}
|
||||
|
||||
export interface SettableColumnsTableSettings {
|
||||
hiddenColumns: string[];
|
||||
}
|
||||
|
||||
export interface RefreshableTableSettings {
|
||||
autoRefreshRate: number;
|
||||
}
|
|
@ -1,19 +1,61 @@
|
|||
export interface PaginationTableSettings {
|
||||
pageSize: number;
|
||||
setPageSize: (pageSize: number) => void;
|
||||
}
|
||||
|
||||
type Set<T> = (
|
||||
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
|
||||
replace?: boolean | undefined
|
||||
) => void;
|
||||
|
||||
export function paginationSettings(
|
||||
set: Set<PaginationTableSettings>
|
||||
): PaginationTableSettings {
|
||||
return {
|
||||
pageSize: 10,
|
||||
setPageSize: (pageSize: number) => set({ pageSize }),
|
||||
};
|
||||
}
|
||||
|
||||
export interface SortableTableSettings {
|
||||
sortBy: { id: string; desc: boolean };
|
||||
setSortBy: (id: string, desc: boolean) => void;
|
||||
}
|
||||
|
||||
export function sortableSettings(
|
||||
set: Set<SortableTableSettings>,
|
||||
initialSortBy = 'name'
|
||||
): SortableTableSettings {
|
||||
return {
|
||||
sortBy: { id: initialSortBy, desc: false },
|
||||
setSortBy: (id: string, desc: boolean) => set({ sortBy: { id, desc } }),
|
||||
};
|
||||
}
|
||||
|
||||
export interface SettableColumnsTableSettings {
|
||||
hiddenColumns: string[];
|
||||
setHiddenColumns: (hiddenColumns: string[]) => void;
|
||||
}
|
||||
|
||||
export interface SettableQuickActionsTableSettings<TAction> {
|
||||
hiddenQuickActions: TAction[];
|
||||
export function hiddenColumnsSettings(
|
||||
set: Set<SettableColumnsTableSettings>
|
||||
): SettableColumnsTableSettings {
|
||||
return {
|
||||
hiddenColumns: [],
|
||||
setHiddenColumns: (hiddenColumns: string[]) => set({ hiddenColumns }),
|
||||
};
|
||||
}
|
||||
|
||||
export interface RefreshableTableSettings {
|
||||
autoRefreshRate: number;
|
||||
setAutoRefreshRate: (autoRefreshRate: number) => void;
|
||||
}
|
||||
|
||||
export function refreshableSettings(
|
||||
set: Set<RefreshableTableSettings>
|
||||
): RefreshableTableSettings {
|
||||
return {
|
||||
autoRefreshRate: 0,
|
||||
setAutoRefreshRate: (autoRefreshRate: number) => set({ autoRefreshRate }),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
|
||||
import { useLocalStorage } from '@/portainer/hooks/useLocalStorage';
|
||||
|
||||
export interface TableSettingsContextInterface<T> {
|
||||
interface TableSettingsContextInterface<T> {
|
||||
settings: T;
|
||||
setTableSettings(partialSettings: Partial<T>): void;
|
||||
setTableSettings(mutation: (settings: T) => T): void;
|
||||
|
|
48
app/react/components/datatables/useZustandTableSettings.tsx
Normal file
48
app/react/components/datatables/useZustandTableSettings.tsx
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Context, createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
|
||||
interface TableSettingsContextInterface<T> {
|
||||
settings: T;
|
||||
}
|
||||
|
||||
const TableSettingsContext = createContext<TableSettingsContextInterface<
|
||||
Record<string, unknown>
|
||||
> | null>(null);
|
||||
|
||||
export function useTableSettings<T>() {
|
||||
const Context = getContextType<T>();
|
||||
|
||||
const context = useContext(Context);
|
||||
|
||||
if (context === null) {
|
||||
throw new Error('must be nested under TableSettingsProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
interface ProviderProps<T> {
|
||||
children: ReactNode;
|
||||
settings: T;
|
||||
}
|
||||
|
||||
export function TableSettingsProvider<T>({
|
||||
children,
|
||||
settings,
|
||||
}: ProviderProps<T>) {
|
||||
const Context = getContextType<T>();
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
settings,
|
||||
}),
|
||||
[settings]
|
||||
);
|
||||
|
||||
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
function getContextType<T>() {
|
||||
return TableSettingsContext as unknown as Context<
|
||||
TableSettingsContextInterface<T>
|
||||
>;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue