1
0
Fork 0
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:
Chaim Lev-Ari 2022-08-11 07:33:29 +03:00 committed by GitHub
parent 5ee570e075
commit bed4257194
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 1616 additions and 875 deletions

View 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';

View file

@ -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 };
}

View 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;
}
}

View file

@ -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;

View file

@ -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>[];
}

View file

@ -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>

View 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';

View file

@ -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,
};

View 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;
}

View file

@ -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 }),
};
}

View file

@ -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;

View 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>
>;
}