mirror of https://github.com/portainer/portainer
refactor(ui/datatables): migrate views to use datatable component [EE-4064] (#7609)
parent
0f0513c684
commit
fe8e834dbf
|
@ -123,6 +123,10 @@ func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []porta
|
|||
|
||||
endpointCount := len(endpoints)
|
||||
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
|
||||
if start > endpointCount {
|
||||
start = endpointCount
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { useMutation } from 'react-query';
|
||||
|
||||
import { activateDevice } from './open-amt.service';
|
||||
|
||||
export const activateDeviceMutationKey = [
|
||||
'environments',
|
||||
'open-amt',
|
||||
'activate',
|
||||
];
|
||||
|
||||
export function useActivateDeviceMutation() {
|
||||
return useMutation(activateDevice, {
|
||||
mutationKey: activateDeviceMutationKey,
|
||||
meta: {
|
||||
message: 'Unable to associate with OpenAMT',
|
||||
},
|
||||
});
|
||||
}
|
|
@ -40,4 +40,6 @@ export interface LicenseInfo {
|
|||
nodes: number;
|
||||
type: LicenseType;
|
||||
valid: boolean;
|
||||
enforcedAt: number;
|
||||
enforced: boolean;
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ import { useQuery } from 'react-query';
|
|||
|
||||
import { error as notifyError } from '@/portainer/services/notifications';
|
||||
|
||||
import { getNodesCount } from '../services/api/status.service';
|
||||
|
||||
import { getLicenseInfo } from './license.service';
|
||||
import { LicenseInfo } from './types';
|
||||
import { LicenseInfo, LicenseType } from './types';
|
||||
|
||||
export function useLicenseInfo() {
|
||||
const { isLoading, data: info } = useQuery<LicenseInfo, Error>(
|
||||
|
@ -18,3 +20,33 @@ export function useLicenseInfo() {
|
|||
|
||||
return { isLoading, info };
|
||||
}
|
||||
|
||||
function useNodesCounts() {
|
||||
const { isLoading, data } = useQuery(
|
||||
['status', 'nodes'],
|
||||
() => getNodesCount(),
|
||||
{
|
||||
onError(error) {
|
||||
notifyError('Failure', error as Error, 'Failed to get nodes count');
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return { nodesCount: data || 0, isLoading };
|
||||
}
|
||||
|
||||
export function useIntegratedLicenseInfo() {
|
||||
const { isLoading: isLoadingNodes, nodesCount } = useNodesCounts();
|
||||
|
||||
const { isLoading: isLoadingLicense, info } = useLicenseInfo();
|
||||
if (
|
||||
isLoadingLicense ||
|
||||
isLoadingNodes ||
|
||||
!info ||
|
||||
info.type === LicenseType.Trial
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { licenseInfo: info as LicenseInfo, usedNodes: nodesCount };
|
||||
}
|
||||
|
|
|
@ -77,6 +77,11 @@
|
|||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<access-tokens-datatable
|
||||
title-text="Access tokens"
|
||||
title-icon="key"
|
||||
|
@ -86,6 +91,11 @@
|
|||
remove-action="removeAction"
|
||||
ui-can-exit="uiCanExit"
|
||||
></access-tokens-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<theme-settings></theme-settings>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,190 +1,65 @@
|
|||
import { useEffect } from 'react';
|
||||
import {
|
||||
useTable,
|
||||
useSortBy,
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
} from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import { Box, Plus, Trash2 } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { useDebouncedValue } from '@/react/hooks/useDebouncedValue';
|
||||
import { ContainerGroup } from '@/react/azure/types';
|
||||
import { Authorized } from '@/react/hooks/useUser';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import {
|
||||
Table,
|
||||
TableActions,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableTitle,
|
||||
} from '@@/datatables';
|
||||
import { multiple } from '@@/datatables/filter-types';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { useRowSelect } from '@@/datatables/useRowSelect';
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
import { useColumns } from './columns';
|
||||
import { columns } from './columns';
|
||||
|
||||
const tableKey = 'containergroups';
|
||||
|
||||
const settingsStore = createPersistedStore(tableKey, 'name');
|
||||
export interface Props {
|
||||
tableKey: string;
|
||||
dataset: ContainerGroup[];
|
||||
onRemoveClick(containerIds: string[]): void;
|
||||
}
|
||||
|
||||
export function ContainersDatatable({
|
||||
dataset,
|
||||
tableKey,
|
||||
onRemoveClick,
|
||||
}: Props) {
|
||||
const { settings, setTableSettings } = useTableSettings<TableSettings>();
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(tableKey);
|
||||
|
||||
const columns = useColumns();
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<ContainerGroup>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: dataset,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
},
|
||||
selectCheckboxComponent: Checkbox,
|
||||
autoResetSelectedRows: false,
|
||||
getRowId(row) {
|
||||
return row.id;
|
||||
},
|
||||
},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
const debouncedSearchValue = useDebouncedValue(searchBarValue);
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalFilter(debouncedSearchValue);
|
||||
}, [debouncedSearchValue, setGlobalFilter]);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
export function ContainersDatatable({ dataset, onRemoveClick }: Props) {
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(tableKey);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableContainer>
|
||||
<TableTitle icon={Box} label="Containers">
|
||||
<SearchBar
|
||||
value={searchBarValue}
|
||||
onChange={handleSearchBarChange}
|
||||
/>
|
||||
|
||||
<TableActions>
|
||||
<Authorized authorizations="AzureContainerGroupDelete">
|
||||
<Button
|
||||
color="dangerlight"
|
||||
disabled={selectedFlatRows.length === 0}
|
||||
onClick={() =>
|
||||
handleRemoveClick(
|
||||
selectedFlatRows.map((row) => row.original.id)
|
||||
)
|
||||
}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Authorized>
|
||||
|
||||
<Authorized authorizations="AzureContainerGroupCreate">
|
||||
<Link to="azure.containerinstances.new" className="space-left">
|
||||
<Button icon={Plus}>Add container</Button>
|
||||
</Link>
|
||||
</Authorized>
|
||||
</TableActions>
|
||||
</TableTitle>
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<TableHeaderRow<ContainerGroup>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
<Datatable
|
||||
dataset={dataset}
|
||||
columns={columns}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
title="Containers"
|
||||
titleIcon={Box}
|
||||
getRowId={(container) => container.id}
|
||||
emptyContentLabel="No container available."
|
||||
renderTableActions={(selectedRows) => (
|
||||
<>
|
||||
<Authorized authorizations="AzureContainerGroupDelete">
|
||||
<Button
|
||||
color="dangerlight"
|
||||
disabled={selectedRows.length === 0}
|
||||
onClick={() => handleRemoveClick(selectedRows.map((r) => r.id))}
|
||||
icon={Trash2}
|
||||
>
|
||||
<Table.Content
|
||||
prepareRow={prepareRow}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<TableRow<ContainerGroup>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
rows={page}
|
||||
emptyContent="No container available."
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
Remove
|
||||
</Button>
|
||||
</Authorized>
|
||||
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={dataset.length}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
<Authorized authorizations="AzureContainerGroupCreate">
|
||||
<Link to="azure.containerinstances.new" className="space-left">
|
||||
<Button icon={Plus}>Add container</Button>
|
||||
</Link>
|
||||
</Authorized>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
async function handleRemoveClick(containerIds: string[]) {
|
||||
|
@ -197,20 +72,4 @@ export function ContainersDatatable({
|
|||
|
||||
return onRemoveClick(containerIds);
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
setSearchBarValue(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setTableSettings((settings) => ({
|
||||
...settings,
|
||||
sortBy: { id, desc },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,19 +9,10 @@ import { useContainerGroups } from '@/react/azure/queries/useContainerGroups';
|
|||
import { useSubscriptions } from '@/react/azure/queries/useSubscriptions';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
|
||||
import { ContainersDatatable } from './ContainersDatatable';
|
||||
import { TableSettings } from './types';
|
||||
|
||||
export function ListView() {
|
||||
const defaultSettings: TableSettings = {
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'state', desc: false },
|
||||
};
|
||||
|
||||
const tableKey = 'containergroups';
|
||||
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
const subscriptionsQuery = useSubscriptions(environmentId);
|
||||
|
@ -45,13 +36,11 @@ export function ListView() {
|
|||
reload
|
||||
title="Container list"
|
||||
/>
|
||||
<TableSettingsProvider defaults={defaultSettings} storageKey={tableKey}>
|
||||
<ContainersDatatable
|
||||
tableKey={tableKey}
|
||||
dataset={groupsQuery.containerGroups}
|
||||
onRemoveClick={handleRemove}
|
||||
/>
|
||||
</TableSettingsProvider>
|
||||
|
||||
<ContainersDatatable
|
||||
dataset={groupsQuery.containerGroups}
|
||||
onRemoveClick={handleRemove}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { name } from './name';
|
||||
import { location } from './location';
|
||||
import { ports } from './ports';
|
||||
import { ownership } from './ownership';
|
||||
|
||||
export function useColumns() {
|
||||
return useMemo(() => [name, location, ports, ownership], []);
|
||||
}
|
||||
export const columns = [name, location, ports, ownership];
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types-old';
|
||||
|
||||
export interface TableSettings
|
||||
extends PaginationTableSettings,
|
||||
SortableTableSettings {}
|
|
@ -8,79 +8,95 @@ import {
|
|||
Row,
|
||||
TableInstance,
|
||||
TableState,
|
||||
TableRowProps,
|
||||
useExpanded,
|
||||
} from 'react-table';
|
||||
import { ReactNode, useEffect } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { IconProps } from '@@/Icon';
|
||||
|
||||
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';
|
||||
import { BasicTableSettings } from './types';
|
||||
import { DatatableHeader } from './DatatableHeader';
|
||||
import { DatatableFooter } from './DatatableFooter';
|
||||
import { DatatableContent } from './DatatableContent';
|
||||
import { defaultGetRowId } from './defaultGetRowId';
|
||||
import { emptyPlugin } from './emptyReactTablePlugin';
|
||||
import { useGoToHighlightedRow } from './useGoToHighlightedRow';
|
||||
|
||||
interface DefaultTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
||||
|
||||
interface TitleOptionsVisible {
|
||||
title: string;
|
||||
icon?: IconProps['icon'];
|
||||
featherIcon?: IconProps['featherIcon'];
|
||||
hide?: never;
|
||||
}
|
||||
|
||||
type TitleOptions = TitleOptionsVisible | { hide: true };
|
||||
|
||||
interface Props<
|
||||
D extends Record<string, unknown>,
|
||||
TSettings extends DefaultTableSettings
|
||||
> {
|
||||
export interface Props<D extends Record<string, unknown>> {
|
||||
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;
|
||||
title?: string;
|
||||
titleIcon?: IconProps['icon'];
|
||||
initialTableState?: Partial<TableState<D>>;
|
||||
isLoading?: boolean;
|
||||
totalCount?: number;
|
||||
description?: JSX.Element;
|
||||
initialActiveItem?: string;
|
||||
description?: ReactNode;
|
||||
pageCount?: number;
|
||||
initialSortBy?: BasicTableSettings['sortBy'];
|
||||
initialPageSize?: BasicTableSettings['pageSize'];
|
||||
highlightedItemId?: string;
|
||||
|
||||
searchValue: string;
|
||||
onSearchChange(search: string): void;
|
||||
onSortByChange(colId: string, desc: boolean): void;
|
||||
onPageSizeChange(pageSize: number): void;
|
||||
|
||||
// send state up
|
||||
onPageChange?(page: number): void;
|
||||
|
||||
renderRow?(
|
||||
row: Row<D>,
|
||||
rowProps: TableRowProps,
|
||||
highlightedItemId?: string
|
||||
): ReactNode;
|
||||
expandable?: boolean;
|
||||
noWidget?: boolean;
|
||||
}
|
||||
|
||||
export function Datatable<
|
||||
D extends Record<string, unknown>,
|
||||
TSettings extends DefaultTableSettings
|
||||
>({
|
||||
export function Datatable<D extends Record<string, unknown>>({
|
||||
columns,
|
||||
dataset,
|
||||
storageKey,
|
||||
renderTableSettings,
|
||||
renderTableActions,
|
||||
settingsStore,
|
||||
renderTableSettings = () => null,
|
||||
renderTableActions = () => null,
|
||||
disableSelect,
|
||||
getRowId = defaultGetRowId,
|
||||
isRowSelectable = () => true,
|
||||
titleOptions,
|
||||
title,
|
||||
titleIcon,
|
||||
emptyContentLabel,
|
||||
initialTableState = {},
|
||||
isLoading,
|
||||
totalCount = dataset.length,
|
||||
description,
|
||||
initialActiveItem,
|
||||
}: Props<D, TSettings>) {
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
||||
pageCount,
|
||||
|
||||
initialSortBy,
|
||||
initialPageSize = 10,
|
||||
onPageChange = () => {},
|
||||
|
||||
onPageSizeChange,
|
||||
onSortByChange,
|
||||
searchValue,
|
||||
onSearchChange,
|
||||
|
||||
renderRow = defaultRenderRow,
|
||||
expandable = false,
|
||||
highlightedItemId,
|
||||
noWidget,
|
||||
}: Props<D>) {
|
||||
const isServerSidePagination = typeof pageCount !== 'undefined';
|
||||
|
||||
const tableInstance = useTable<D>(
|
||||
{
|
||||
|
@ -89,183 +105,104 @@ export function Datatable<
|
|||
data: dataset,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
pageSize: settingsStore.pageSize || 10,
|
||||
sortBy: [settingsStore.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
pageSize: initialPageSize,
|
||||
sortBy: initialSortBy ? [initialSortBy] : [],
|
||||
globalFilter: searchValue,
|
||||
...initialTableState,
|
||||
},
|
||||
isRowSelectable,
|
||||
autoResetExpanded: false,
|
||||
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;
|
||||
},
|
||||
...(isServerSidePagination ? { manualPagination: true, pageCount } : {}),
|
||||
},
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
expandable ? useExpanded : emptyPlugin,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
!disableSelect ? useRowSelectColumn : emptyPlugin
|
||||
);
|
||||
|
||||
const {
|
||||
rows,
|
||||
selectedFlatRows,
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = tableInstance;
|
||||
useGoToHighlightedRow(
|
||||
isServerSidePagination,
|
||||
tableInstance.state.pageSize,
|
||||
tableInstance.rows,
|
||||
handlePageChange,
|
||||
highlightedItemId
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialActiveItem && pageSize !== rows.length) {
|
||||
const paginatedData = [...Array(Math.ceil(rows.length / pageSize))].map(
|
||||
(_, i) => rows.slice(pageSize * i, pageSize + pageSize * i)
|
||||
);
|
||||
|
||||
const itemPage = paginatedData.findIndex((sub) =>
|
||||
sub.some((row) => row.id === initialActiveItem)
|
||||
);
|
||||
|
||||
gotoPage(itemPage);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [initialActiveItem]);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
const selectedItems = selectedFlatRows.map((row) => row.original);
|
||||
const selectedItems = tableInstance.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}
|
||||
featherIcon={titleOptions.featherIcon}
|
||||
description={description}
|
||||
>
|
||||
<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={clsx(
|
||||
className,
|
||||
initialActiveItem &&
|
||||
initialActiveItem === row.id &&
|
||||
'active'
|
||||
)}
|
||||
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>
|
||||
<Table.Container noWidget={noWidget}>
|
||||
<DatatableHeader
|
||||
onSearchChange={handleSearchBarChange}
|
||||
searchValue={searchValue}
|
||||
title={title}
|
||||
titleIcon={titleIcon}
|
||||
renderTableActions={() => renderTableActions(selectedItems)}
|
||||
renderTableSettings={() => renderTableSettings(tableInstance)}
|
||||
description={description}
|
||||
/>
|
||||
<DatatableContent<D>
|
||||
tableInstance={tableInstance}
|
||||
renderRow={(row, rowProps) =>
|
||||
renderRow(row, rowProps, highlightedItemId)
|
||||
}
|
||||
emptyContentLabel={emptyContentLabel}
|
||||
isLoading={isLoading}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
|
||||
<DatatableFooter
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
page={tableInstance.state.pageIndex}
|
||||
pageSize={tableInstance.state.pageSize}
|
||||
totalCount={totalCount}
|
||||
totalSelected={selectedItems.length}
|
||||
/>
|
||||
</Table.Container>
|
||||
);
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
tableInstance.setGlobalFilter(value);
|
||||
onSearchChange(value);
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
tableInstance.gotoPage(page);
|
||||
onPageChange(page);
|
||||
}
|
||||
|
||||
function handleSortChange(colId: string, desc: boolean) {
|
||||
onSortByChange(colId, desc);
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
tableInstance.setPageSize(pageSize);
|
||||
onPageSizeChange(pageSize);
|
||||
}
|
||||
}
|
||||
|
||||
function defaultRenderRow<D extends Record<string, unknown>>(
|
||||
row: Row<D>,
|
||||
rowProps: TableRowProps,
|
||||
highlightedItemId?: string
|
||||
) {
|
||||
return (
|
||||
<Table.Row<D>
|
||||
key={rowProps.key}
|
||||
cells={row.cells}
|
||||
className={clsx(rowProps.className, {
|
||||
active: highlightedItemId === row.id,
|
||||
})}
|
||||
role={rowProps.role}
|
||||
style={rowProps.style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { Row, TableInstance, TableRowProps } from 'react-table';
|
||||
|
||||
import { Table } from './Table';
|
||||
|
||||
interface Props<D extends Record<string, unknown>> {
|
||||
tableInstance: TableInstance<D>;
|
||||
renderRow(row: Row<D>, rowProps: TableRowProps): React.ReactNode;
|
||||
onSortChange?(colId: string, desc: boolean): void;
|
||||
isLoading?: boolean;
|
||||
emptyContentLabel?: string;
|
||||
}
|
||||
|
||||
export function DatatableContent<D extends Record<string, unknown>>({
|
||||
tableInstance,
|
||||
renderRow,
|
||||
onSortChange,
|
||||
isLoading,
|
||||
emptyContentLabel,
|
||||
}: Props<D>) {
|
||||
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
|
||||
tableInstance;
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
return (
|
||||
<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}
|
||||
onSortChange={onSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<Table.Content<D>
|
||||
rows={page}
|
||||
isLoading={isLoading}
|
||||
prepareRow={prepareRow}
|
||||
emptyContent={emptyContentLabel}
|
||||
renderRow={renderRow}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
import { PaginationControls } from '@@/PaginationControls';
|
||||
|
||||
import { Table } from './Table';
|
||||
import { SelectedRowsCount } from './SelectedRowsCount';
|
||||
|
||||
interface Props {
|
||||
totalSelected: number;
|
||||
pageSize: number;
|
||||
page: number;
|
||||
onPageChange(page: number): void;
|
||||
totalCount: number;
|
||||
onPageSizeChange(pageSize: number): void;
|
||||
}
|
||||
|
||||
export function DatatableFooter({
|
||||
totalSelected,
|
||||
pageSize,
|
||||
page,
|
||||
onPageChange,
|
||||
totalCount,
|
||||
onPageSizeChange,
|
||||
}: Props) {
|
||||
return (
|
||||
<Table.Footer>
|
||||
<SelectedRowsCount value={totalSelected} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={page + 1}
|
||||
onPageChange={(page) => onPageChange(page - 1)}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={onPageSizeChange}
|
||||
/>
|
||||
</Table.Footer>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
import { IconProps } from '@@/Icon';
|
||||
|
||||
import { SearchBar } from './SearchBar';
|
||||
import { Table } from './Table';
|
||||
|
||||
type Props = {
|
||||
title?: string;
|
||||
titleIcon?: IconProps['icon'];
|
||||
searchValue: string;
|
||||
onSearchChange(value: string): void;
|
||||
renderTableSettings?(): ReactNode;
|
||||
renderTableActions?(): ReactNode;
|
||||
description?: ReactNode;
|
||||
};
|
||||
|
||||
export function DatatableHeader({
|
||||
onSearchChange,
|
||||
renderTableActions,
|
||||
renderTableSettings,
|
||||
searchValue,
|
||||
title,
|
||||
titleIcon,
|
||||
description,
|
||||
}: Props) {
|
||||
if (!title) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table.Title label={title} icon={titleIcon} description={description}>
|
||||
<SearchBar value={searchValue} onChange={onSearchChange} />
|
||||
{renderTableActions && (
|
||||
<Table.Actions>{renderTableActions()}</Table.Actions>
|
||||
)}
|
||||
<Table.TitleActions>
|
||||
{!!renderTableSettings && renderTableSettings()}
|
||||
</Table.TitleActions>
|
||||
</Table.Title>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
import { Row } from 'react-table';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import { ExpandableDatatableTableRow } from './ExpandableDatatableRow';
|
||||
import { Datatable, Props as DatatableProps } from './Datatable';
|
||||
|
||||
interface Props<D extends Record<string, unknown>>
|
||||
extends Omit<DatatableProps<D>, 'renderRow' | 'expandable'> {
|
||||
renderSubRow(row: Row<D>): ReactNode;
|
||||
}
|
||||
|
||||
export function ExpandableDatatable<D extends Record<string, unknown>>({
|
||||
renderSubRow,
|
||||
...props
|
||||
}: Props<D>) {
|
||||
return (
|
||||
<Datatable<D>
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
expandable
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<ExpandableDatatableTableRow<D>
|
||||
key={key}
|
||||
row={row}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
renderSubRow={renderSubRow}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import { CSSProperties, ReactNode } from 'react';
|
||||
import { Row } from 'react-table';
|
||||
|
||||
import { TableRow } from './TableRow';
|
||||
|
||||
interface Props<D extends Record<string, unknown>> {
|
||||
row: Row<D>;
|
||||
className?: string;
|
||||
role?: string;
|
||||
style?: CSSProperties;
|
||||
disableSelect?: boolean;
|
||||
renderSubRow(row: Row<D>): ReactNode;
|
||||
}
|
||||
|
||||
export function ExpandableDatatableTableRow<D extends Record<string, unknown>>({
|
||||
row,
|
||||
className,
|
||||
role,
|
||||
style,
|
||||
disableSelect,
|
||||
renderSubRow,
|
||||
}: Props<D>) {
|
||||
return (
|
||||
<>
|
||||
<TableRow<D>
|
||||
cells={row.cells}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
{row.isExpanded && (
|
||||
<tr>
|
||||
{!disableSelect && <td />}
|
||||
<td colSpan={disableSelect ? row.cells.length : row.cells.length - 1}>
|
||||
{renderSubRow(row)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import {
|
||||
useTable,
|
||||
useFilters,
|
||||
useSortBy,
|
||||
Column,
|
||||
TableState,
|
||||
usePagination,
|
||||
} from 'react-table';
|
||||
|
||||
import { Table } from './Table';
|
||||
import { multiple } from './filter-types';
|
||||
import { NestedTable } from './NestedTable';
|
||||
import { DatatableContent } from './DatatableContent';
|
||||
import { defaultGetRowId } from './defaultGetRowId';
|
||||
|
||||
interface Props<D extends Record<string, unknown>> {
|
||||
dataset: D[];
|
||||
columns: readonly Column<D>[];
|
||||
|
||||
getRowId?(row: D): string;
|
||||
emptyContentLabel?: string;
|
||||
initialTableState?: Partial<TableState<D>>;
|
||||
isLoading?: boolean;
|
||||
defaultSortBy?: string;
|
||||
}
|
||||
|
||||
export function NestedDatatable<D extends Record<string, unknown>>({
|
||||
columns,
|
||||
dataset,
|
||||
getRowId = defaultGetRowId,
|
||||
emptyContentLabel,
|
||||
initialTableState = {},
|
||||
isLoading,
|
||||
defaultSortBy,
|
||||
}: Props<D>) {
|
||||
const tableInstance = useTable<D>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: dataset,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
sortBy: defaultSortBy ? [{ id: defaultSortBy, desc: true }] : [],
|
||||
...initialTableState,
|
||||
},
|
||||
autoResetSelectedRows: false,
|
||||
getRowId,
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
return (
|
||||
<NestedTable>
|
||||
<Table.Container>
|
||||
<DatatableContent<D>
|
||||
tableInstance={tableInstance}
|
||||
isLoading={isLoading}
|
||||
emptyContentLabel={emptyContentLabel}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<Table.Row<D>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Table.Container>
|
||||
</NestedTable>
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import './InnerDatatable.css';
|
||||
import './NestedTable.css';
|
||||
|
||||
export function InnerDatatable({ children }: PropsWithChildren<unknown>) {
|
||||
export function NestedTable({ children }: PropsWithChildren<unknown>) {
|
||||
return <div className="inner-datatable">{children}</div>;
|
||||
}
|
|
@ -5,7 +5,7 @@ import {
|
|||
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
|
||||
import { useTableSettings } from './useZustandTableSettings';
|
||||
import { useTableSettings } from './useTableSettings';
|
||||
|
||||
export interface Action {
|
||||
id: QuickAction;
|
||||
|
@ -17,7 +17,7 @@ interface Props {
|
|||
}
|
||||
|
||||
export function QuickActionsSettings({ actions }: Props) {
|
||||
const { settings } =
|
||||
const settings =
|
||||
useTableSettings<SettableQuickActionsTableSettings<QuickAction>>();
|
||||
|
||||
return (
|
||||
|
|
|
@ -35,6 +35,8 @@ function MainComponent({
|
|||
);
|
||||
}
|
||||
|
||||
MainComponent.displayName = 'Table';
|
||||
|
||||
interface SubComponents {
|
||||
Container: typeof TableContainer;
|
||||
Actions: typeof TableActions;
|
||||
|
|
|
@ -2,12 +2,28 @@ import { PropsWithChildren } from 'react';
|
|||
|
||||
import { Widget, WidgetBody } from '@@/Widget';
|
||||
|
||||
export function TableContainer({ children }: PropsWithChildren<unknown>) {
|
||||
interface Props {
|
||||
// workaround to remove the widget, ideally we should have a different component to wrap the table with a widget
|
||||
noWidget?: boolean;
|
||||
}
|
||||
|
||||
export function TableContainer({
|
||||
children,
|
||||
noWidget = false,
|
||||
}: PropsWithChildren<Props>) {
|
||||
if (noWidget) {
|
||||
return <div className="datatable">{children}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="datatable">
|
||||
<Widget>
|
||||
<WidgetBody className="no-padding">{children}</WidgetBody>
|
||||
</Widget>
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<div className="datatable">
|
||||
<Widget>
|
||||
<WidgetBody className="no-padding">{children}</WidgetBody>
|
||||
</Widget>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ interface Props {
|
|||
icon?: ReactNode | ComponentType<unknown>;
|
||||
featherIcon?: boolean;
|
||||
label: string;
|
||||
description?: JSX.Element;
|
||||
description?: ReactNode;
|
||||
}
|
||||
|
||||
export function TableTitle({
|
||||
|
@ -34,7 +34,7 @@ export function TableTitle({
|
|||
</div>
|
||||
{children}
|
||||
</div>
|
||||
{description && description}
|
||||
{description}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
export 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 '';
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export function emptyPlugin() {}
|
||||
|
||||
emptyPlugin.pluginName = 'emptyPlugin';
|
|
@ -0,0 +1,49 @@
|
|||
import { ChevronDown, ChevronUp } from 'react-feather';
|
||||
import { CellProps, Column, HeaderProps } from 'react-table';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
export function buildExpandColumn<T extends Record<string, unknown>>(
|
||||
isExpandable: (item: T) => boolean
|
||||
): Column<T> {
|
||||
return {
|
||||
id: 'expand',
|
||||
Header: ({
|
||||
filteredFlatRows,
|
||||
getToggleAllRowsExpandedProps,
|
||||
isAllRowsExpanded,
|
||||
}: HeaderProps<T>) => {
|
||||
const hasExpandableItems = filteredFlatRows.some((item) =>
|
||||
isExpandable(item.original)
|
||||
);
|
||||
|
||||
return (
|
||||
hasExpandableItems && (
|
||||
<Button
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...getToggleAllRowsExpandedProps()}
|
||||
color="none"
|
||||
icon={isAllRowsExpanded ? ChevronDown : ChevronUp}
|
||||
/>
|
||||
)
|
||||
);
|
||||
},
|
||||
Cell: ({ row }: CellProps<T>) => (
|
||||
<div className="vertical-center">
|
||||
{isExpandable(row.original) && (
|
||||
<Button
|
||||
/* eslint-disable-next-line react/jsx-props-no-spreading */
|
||||
{...row.getToggleRowExpandedProps()}
|
||||
color="none"
|
||||
icon={row.isExpanded ? ChevronDown : ChevronUp}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
width: 30,
|
||||
disableResizing: true,
|
||||
};
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
export interface PaginationTableSettings {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface SortableTableSettings {
|
||||
sortBy: { id: string; desc: boolean };
|
||||
}
|
||||
|
||||
export interface SettableColumnsTableSettings {
|
||||
hiddenColumns: string[];
|
||||
}
|
||||
|
||||
export interface RefreshableTableSettings {
|
||||
autoRefreshRate: number;
|
||||
}
|
|
@ -1,15 +1,20 @@
|
|||
import { createStore } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
|
||||
export interface PaginationTableSettings {
|
||||
pageSize: number;
|
||||
setPageSize: (pageSize: number) => void;
|
||||
}
|
||||
|
||||
type Set<T> = (
|
||||
type ZustandSetFunc<T> = (
|
||||
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
|
||||
replace?: boolean | undefined
|
||||
) => void;
|
||||
|
||||
export function paginationSettings(
|
||||
set: Set<PaginationTableSettings>
|
||||
set: ZustandSetFunc<PaginationTableSettings>
|
||||
): PaginationTableSettings {
|
||||
return {
|
||||
pageSize: 10,
|
||||
|
@ -23,12 +28,14 @@ export interface SortableTableSettings {
|
|||
}
|
||||
|
||||
export function sortableSettings(
|
||||
set: Set<SortableTableSettings>,
|
||||
initialSortBy = 'name',
|
||||
desc = false
|
||||
set: ZustandSetFunc<SortableTableSettings>,
|
||||
initialSortBy: string | { id: string; desc: boolean }
|
||||
): SortableTableSettings {
|
||||
return {
|
||||
sortBy: { id: initialSortBy, desc },
|
||||
sortBy:
|
||||
typeof initialSortBy === 'string'
|
||||
? { id: initialSortBy, desc: false }
|
||||
: initialSortBy,
|
||||
setSortBy: (id: string, desc: boolean) => set({ sortBy: { id, desc } }),
|
||||
};
|
||||
}
|
||||
|
@ -39,7 +46,7 @@ export interface SettableColumnsTableSettings {
|
|||
}
|
||||
|
||||
export function hiddenColumnsSettings(
|
||||
set: Set<SettableColumnsTableSettings>
|
||||
set: ZustandSetFunc<SettableColumnsTableSettings>
|
||||
): SettableColumnsTableSettings {
|
||||
return {
|
||||
hiddenColumns: [],
|
||||
|
@ -53,10 +60,38 @@ export interface RefreshableTableSettings {
|
|||
}
|
||||
|
||||
export function refreshableSettings(
|
||||
set: Set<RefreshableTableSettings>
|
||||
set: ZustandSetFunc<RefreshableTableSettings>
|
||||
): RefreshableTableSettings {
|
||||
return {
|
||||
autoRefreshRate: 0,
|
||||
setAutoRefreshRate: (autoRefreshRate: number) => set({ autoRefreshRate }),
|
||||
};
|
||||
}
|
||||
|
||||
export interface BasicTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
||||
|
||||
export function createPersistedStore<T extends BasicTableSettings>(
|
||||
storageKey: string,
|
||||
initialSortBy: string | { id: string; desc: boolean } = 'name',
|
||||
create: (set: ZustandSetFunc<T>) => Omit<T, keyof BasicTableSettings> = () =>
|
||||
({} as T)
|
||||
) {
|
||||
return createStore<T>()(
|
||||
persist(
|
||||
(set) =>
|
||||
({
|
||||
...sortableSettings(
|
||||
set as ZustandSetFunc<SortableTableSettings>,
|
||||
initialSortBy
|
||||
),
|
||||
...paginationSettings(set as ZustandSetFunc<PaginationTableSettings>),
|
||||
...create(set),
|
||||
} as T),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import _ from 'lodash';
|
||||
import { useRef, useLayoutEffect, useEffect } from 'react';
|
||||
|
||||
export function useGoToHighlightedRow<T extends { id: string }>(
|
||||
isServerSidePagination: boolean,
|
||||
pageSize: number,
|
||||
rows: Array<T>,
|
||||
goToPage: (page: number) => void,
|
||||
highlightedItemId?: string
|
||||
) {
|
||||
const handlePageChangeRef = useRef(goToPage);
|
||||
useLayoutEffect(() => {
|
||||
handlePageChangeRef.current = goToPage;
|
||||
});
|
||||
|
||||
const highlightedItemIdRef = useRef(highlightedItemId);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isServerSidePagination &&
|
||||
highlightedItemId &&
|
||||
highlightedItemId !== highlightedItemIdRef.current
|
||||
) {
|
||||
const page = getRowPage(highlightedItemId, pageSize, rows);
|
||||
if (page) {
|
||||
handlePageChangeRef.current(page);
|
||||
}
|
||||
highlightedItemIdRef.current = highlightedItemId;
|
||||
}
|
||||
}, [highlightedItemId, isServerSidePagination, rows, pageSize]);
|
||||
}
|
||||
|
||||
function getRowPage<T extends { id: string }>(
|
||||
rowID: string,
|
||||
pageSize: number,
|
||||
rows: Array<T>
|
||||
) {
|
||||
const totalRows = rows.length;
|
||||
|
||||
if (!rowID || pageSize > totalRows) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const paginatedData = _.chunk(rows, pageSize);
|
||||
|
||||
const itemPage = paginatedData.findIndex((sub) =>
|
||||
sub.some((row) => row.id === rowID)
|
||||
);
|
||||
|
||||
return itemPage;
|
||||
}
|
|
@ -1,91 +1,34 @@
|
|||
import {
|
||||
Context,
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Context, createContext, ReactNode, useContext } from 'react';
|
||||
import { StoreApi, useStore } from 'zustand';
|
||||
|
||||
import { useLocalStorage } from '@/react/hooks/useLocalStorage';
|
||||
|
||||
interface TableSettingsContextInterface<T> {
|
||||
settings: T;
|
||||
setTableSettings(partialSettings: Partial<T>): void;
|
||||
setTableSettings(mutation: (settings: T) => T): void;
|
||||
}
|
||||
|
||||
const TableSettingsContext = createContext<TableSettingsContextInterface<
|
||||
Record<string, unknown>
|
||||
> | null>(null);
|
||||
const TableSettingsContext = createContext<StoreApi<object> | null>(null);
|
||||
TableSettingsContext.displayName = 'TableSettingsContext';
|
||||
|
||||
export function useTableSettings<T>() {
|
||||
export function useTableSettings<T extends object>() {
|
||||
const Context = getContextType<T>();
|
||||
|
||||
const context = useContext(Context);
|
||||
|
||||
if (context === null) {
|
||||
throw new Error('must be nested under TableSettingsProvider');
|
||||
}
|
||||
|
||||
return context;
|
||||
return useStore(context);
|
||||
}
|
||||
|
||||
interface ProviderProps<T> {
|
||||
interface ProviderProps<T extends object> {
|
||||
children: ReactNode;
|
||||
defaults?: T;
|
||||
storageKey: string;
|
||||
settings: StoreApi<T>;
|
||||
}
|
||||
|
||||
export function TableSettingsProvider<T>({
|
||||
export function TableSettingsProvider<T extends object>({
|
||||
children,
|
||||
defaults,
|
||||
storageKey,
|
||||
settings,
|
||||
}: ProviderProps<T>) {
|
||||
const Context = getContextType<T>();
|
||||
|
||||
const [storage, setStorage] = useLocalStorage<T>(
|
||||
keyBuilder(storageKey),
|
||||
defaults as T
|
||||
);
|
||||
|
||||
const [settings, setTableSettings] = useState(storage);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(mutation: Partial<T> | ((settings: T) => T)): void => {
|
||||
setTableSettings((settings) => {
|
||||
const newTableSettings =
|
||||
mutation instanceof Function
|
||||
? mutation(settings)
|
||||
: { ...settings, ...mutation };
|
||||
|
||||
setStorage(newTableSettings);
|
||||
|
||||
return newTableSettings;
|
||||
});
|
||||
},
|
||||
[setStorage]
|
||||
);
|
||||
|
||||
const contextValue = useMemo(
|
||||
() => ({
|
||||
settings,
|
||||
setTableSettings: handleChange,
|
||||
}),
|
||||
[settings, handleChange]
|
||||
);
|
||||
|
||||
return <Context.Provider value={contextValue}>{children}</Context.Provider>;
|
||||
|
||||
function keyBuilder(key: string) {
|
||||
return `datatable_TableSettings_${key}`;
|
||||
}
|
||||
return <Context.Provider value={settings}>{children}</Context.Provider>;
|
||||
}
|
||||
|
||||
function getContextType<T>() {
|
||||
return TableSettingsContext as unknown as Context<
|
||||
TableSettingsContextInterface<T>
|
||||
>;
|
||||
function getContextType<T extends object>() {
|
||||
return TableSettingsContext as unknown as Context<StoreApi<T>>;
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import { Context, createContext, ReactNode, useContext, useMemo } from 'react';
|
||||
|
||||
interface TableSettingsContextInterface<T> {
|
||||
settings: T;
|
||||
}
|
||||
|
||||
const TableSettingsContext = createContext<TableSettingsContextInterface<
|
||||
Record<string, unknown>
|
||||
> | null>(null);
|
||||
TableSettingsContext.displayName = 'TableSettingsContext';
|
||||
|
||||
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>
|
||||
>;
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import { useStore } from 'zustand';
|
||||
import { Box } from 'react-feather';
|
||||
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||
|
@ -10,6 +12,8 @@ import {
|
|||
QuickActionsSettings,
|
||||
} from '@@/datatables/QuickActionsSettings';
|
||||
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
|
||||
import { useContainers } from '../../queries/containers';
|
||||
|
||||
|
@ -20,7 +24,7 @@ import { ContainersDatatableActions } from './ContainersDatatableActions';
|
|||
import { RowProvider } from './RowContext';
|
||||
|
||||
const storageKey = 'containers';
|
||||
const useStore = createStore(storageKey);
|
||||
const settingsStore = createStore(storageKey);
|
||||
|
||||
const actions = [
|
||||
buildAction('logs', 'Logs'),
|
||||
|
@ -39,13 +43,15 @@ export function ContainersDatatable({
|
|||
isHostColumnVisible,
|
||||
environment,
|
||||
}: Props) {
|
||||
const settings = useStore();
|
||||
const settings = useStore(settingsStore);
|
||||
const isGPUsColumnVisible = useShowGPUsColumn(environment.Id);
|
||||
const columns = useColumns(isHostColumnVisible, isGPUsColumnVisible);
|
||||
const hidableColumns = _.compact(
|
||||
columns.filter((col) => col.canHide).map((col) => col.id)
|
||||
);
|
||||
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const containersQuery = useContainers(
|
||||
environment.Id,
|
||||
true,
|
||||
|
@ -55,53 +61,57 @@ export function ContainersDatatable({
|
|||
|
||||
return (
|
||||
<RowProvider context={{ environment }}>
|
||||
<Datatable
|
||||
titleOptions={{
|
||||
icon: 'svg-cubes',
|
||||
title: 'Containers',
|
||||
}}
|
||||
settingsStore={settings}
|
||||
columns={columns}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<ContainersDatatableActions
|
||||
selectedItems={selectedRows}
|
||||
isAddActionVisible
|
||||
endpointId={environment.Id}
|
||||
/>
|
||||
)}
|
||||
isLoading={containersQuery.isLoading}
|
||||
isRowSelectable={(row) => !row.original.IsPortainer}
|
||||
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
||||
renderTableSettings={(tableInstance) => {
|
||||
const columnsToHide = tableInstance.allColumns.filter((colInstance) =>
|
||||
hidableColumns?.includes(colInstance.id)
|
||||
);
|
||||
<TableSettingsProvider settings={settingsStore}>
|
||||
<Datatable
|
||||
titleIcon={Box}
|
||||
title="Containers"
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
columns={columns}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<ContainersDatatableActions
|
||||
selectedItems={selectedRows}
|
||||
isAddActionVisible
|
||||
endpointId={environment.Id}
|
||||
/>
|
||||
)}
|
||||
isLoading={containersQuery.isLoading}
|
||||
isRowSelectable={(row) => !row.original.IsPortainer}
|
||||
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
||||
renderTableSettings={(tableInstance) => {
|
||||
const columnsToHide = tableInstance.allColumns.filter(
|
||||
(colInstance) => hidableColumns?.includes(colInstance.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ColumnVisibilityMenu<DockerContainer>
|
||||
columns={columnsToHide}
|
||||
onChange={(hiddenColumns) => {
|
||||
settings.setHiddenColumns(hiddenColumns);
|
||||
tableInstance.setHiddenColumns(hiddenColumns);
|
||||
}}
|
||||
value={settings.hiddenColumns}
|
||||
/>
|
||||
<TableSettingsMenu
|
||||
quickActions={<QuickActionsSettings actions={actions} />}
|
||||
>
|
||||
<ContainersDatatableSettings
|
||||
isRefreshVisible
|
||||
settings={settings}
|
||||
return (
|
||||
<>
|
||||
<ColumnVisibilityMenu<DockerContainer>
|
||||
columns={columnsToHide}
|
||||
onChange={(hiddenColumns) => {
|
||||
settings.setHiddenColumns(hiddenColumns);
|
||||
tableInstance.setHiddenColumns(hiddenColumns);
|
||||
}}
|
||||
value={settings.hiddenColumns}
|
||||
/>
|
||||
</TableSettingsMenu>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
storageKey={storageKey}
|
||||
dataset={containersQuery.data || []}
|
||||
emptyContentLabel="No containers found"
|
||||
/>
|
||||
<TableSettingsMenu
|
||||
quickActions={<QuickActionsSettings actions={actions} />}
|
||||
>
|
||||
<ContainersDatatableSettings
|
||||
isRefreshVisible
|
||||
settings={settings}
|
||||
/>
|
||||
</TableSettingsMenu>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
dataset={containersQuery.data || []}
|
||||
emptyContentLabel="No containers found"
|
||||
/>
|
||||
</TableSettingsProvider>
|
||||
</RowProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useSref } from '@uirouter/react';
|
|||
|
||||
import type { DockerContainer } from '@/react/docker/containers/types';
|
||||
|
||||
import { useTableSettings } from '@@/datatables/useZustandTableSettings';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
|
||||
import { TableSettings } from '../types';
|
||||
|
||||
|
@ -31,7 +31,7 @@ export function NameCell({
|
|||
nodeName: container.NodeName,
|
||||
});
|
||||
|
||||
const { settings } = useTableSettings<TableSettings>();
|
||||
const settings = useTableSettings<TableSettings>();
|
||||
const truncate = settings.truncateContainerName;
|
||||
|
||||
let shortName = name;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useAuthorizations } from '@/react/hooks/useUser';
|
|||
import { ContainerQuickActions } from '@/react/docker/containers/components/ContainerQuickActions';
|
||||
import { DockerContainer } from '@/react/docker/containers/types';
|
||||
|
||||
import { useTableSettings } from '@@/datatables/useZustandTableSettings';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
|
||||
import { TableSettings } from '../types';
|
||||
|
||||
|
@ -22,7 +22,7 @@ export const quickActions: Column<DockerContainer> = {
|
|||
function QuickActionsCell({
|
||||
row: { original: container },
|
||||
}: CellProps<DockerContainer>) {
|
||||
const { settings } = useTableSettings<TableSettings>();
|
||||
const settings = useTableSettings<TableSettings>();
|
||||
|
||||
const { hiddenQuickActions = [] } = settings;
|
||||
|
||||
|
|
|
@ -1,40 +1,26 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
refreshableSettings,
|
||||
hiddenColumnsSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
createPersistedStore,
|
||||
} from '@@/datatables/types';
|
||||
|
||||
import { QuickAction, TableSettings } from './types';
|
||||
|
||||
export const TRUNCATE_LENGTH = 32;
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set),
|
||||
...paginationSettings(set),
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
truncateContainerName: TRUNCATE_LENGTH,
|
||||
setTruncateContainerName(truncateContainerName: number) {
|
||||
set({
|
||||
truncateContainerName,
|
||||
});
|
||||
},
|
||||
return createPersistedStore<TableSettings>(storageKey, 'Name', (set) => ({
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
truncateContainerName: TRUNCATE_LENGTH,
|
||||
setTruncateContainerName(truncateContainerName: number) {
|
||||
set({
|
||||
truncateContainerName,
|
||||
});
|
||||
},
|
||||
|
||||
hiddenQuickActions: [] as QuickAction[],
|
||||
setHiddenQuickActions: (hiddenQuickActions: QuickAction[]) =>
|
||||
set({ hiddenQuickActions }),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
);
|
||||
hiddenQuickActions: [] as QuickAction[],
|
||||
setHiddenQuickActions: (hiddenQuickActions: QuickAction[]) =>
|
||||
set({ hiddenQuickActions }),
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
BasicTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
} from '@@/datatables/types';
|
||||
|
||||
export type QuickAction = 'attach' | 'exec' | 'inspect' | 'logs' | 'stats';
|
||||
|
||||
|
@ -13,8 +12,7 @@ export interface SettableQuickActionsTableSettings<TAction> {
|
|||
}
|
||||
|
||||
export interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
extends BasicTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SettableQuickActionsTableSettings<QuickAction>,
|
||||
RefreshableTableSettings {
|
||||
|
|
|
@ -38,63 +38,59 @@ export function NetworkContainersTable({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<TableContainer>
|
||||
<TableTitle label="Containers in network" icon="server" featherIcon />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable
|
||||
headers={tableHeaders}
|
||||
dataCy="networkDetails-networkContainers"
|
||||
>
|
||||
{networkContainers.map((container) => (
|
||||
<tr key={container.Id}>
|
||||
<td>
|
||||
<Link
|
||||
to="docker.containers.container"
|
||||
params={{
|
||||
id: container.Id,
|
||||
nodeName,
|
||||
}}
|
||||
title={container.Name}
|
||||
>
|
||||
{container.Name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{container.IPv4Address || '-'}</td>
|
||||
<td>{container.IPv6Address || '-'}</td>
|
||||
<td>{container.MacAddress || '-'}</td>
|
||||
<td>
|
||||
<Authorized authorizations="DockerNetworkDisconnect">
|
||||
<Button
|
||||
data-cy={`networkDetails-disconnect${container.Name}`}
|
||||
size="xsmall"
|
||||
color="dangerlight"
|
||||
onClick={() => {
|
||||
if (container.Id) {
|
||||
disconnectContainer.mutate({
|
||||
containerId: container.Id,
|
||||
environmentId,
|
||||
networkId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon="trash-2"
|
||||
feather
|
||||
class-name="icon-secondary icon-md"
|
||||
/>
|
||||
Leave Network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<TableTitle label="Containers in network" icon="server" featherIcon />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable
|
||||
headers={tableHeaders}
|
||||
dataCy="networkDetails-networkContainers"
|
||||
>
|
||||
{networkContainers.map((container) => (
|
||||
<tr key={container.Id}>
|
||||
<td>
|
||||
<Link
|
||||
to="docker.containers.container"
|
||||
params={{
|
||||
id: container.Id,
|
||||
nodeName,
|
||||
}}
|
||||
title={container.Name}
|
||||
>
|
||||
{container.Name}
|
||||
</Link>
|
||||
</td>
|
||||
<td>{container.IPv4Address || '-'}</td>
|
||||
<td>{container.IPv6Address || '-'}</td>
|
||||
<td>{container.MacAddress || '-'}</td>
|
||||
<td>
|
||||
<Authorized authorizations="DockerNetworkDisconnect">
|
||||
<Button
|
||||
data-cy={`networkDetails-disconnect${container.Name}`}
|
||||
size="xsmall"
|
||||
color="dangerlight"
|
||||
onClick={() => {
|
||||
if (container.Id) {
|
||||
disconnectContainer.mutate({
|
||||
containerId: container.Id,
|
||||
environmentId,
|
||||
networkId,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
icon="trash-2"
|
||||
feather
|
||||
class-name="icon-secondary icon-md"
|
||||
/>
|
||||
Leave Network
|
||||
</Button>
|
||||
</Authorized>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -29,86 +29,80 @@ export function NetworkDetailsTable({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<TableContainer>
|
||||
<TableTitle label="Network details" icon="share-2" featherIcon />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-detailsTable">
|
||||
{/* networkRowContent */}
|
||||
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Id">
|
||||
{network.Id}
|
||||
{allowRemoveNetwork && (
|
||||
<Authorized authorizations="DockerNetworkDelete">
|
||||
<Button
|
||||
data-cy="networkDetails-deleteNetwork"
|
||||
size="xsmall"
|
||||
color="danger"
|
||||
onClick={() => onRemoveNetworkClicked()}
|
||||
>
|
||||
<Icon
|
||||
icon="trash-2"
|
||||
feather
|
||||
className="space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete this network
|
||||
</Button>
|
||||
</Authorized>
|
||||
)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Driver">
|
||||
{network.Driver}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Attachable">
|
||||
{String(network.Attachable)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Internal">
|
||||
{String(network.Internal)}
|
||||
</DetailsTable.Row>
|
||||
<TableContainer>
|
||||
<TableTitle label="Network details" icon="share-2" featherIcon />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-detailsTable">
|
||||
{/* networkRowContent */}
|
||||
<DetailsTable.Row label="Name">{network.Name}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Id">
|
||||
{network.Id}
|
||||
{allowRemoveNetwork && (
|
||||
<Authorized authorizations="DockerNetworkDelete">
|
||||
<Button
|
||||
data-cy="networkDetails-deleteNetwork"
|
||||
size="xsmall"
|
||||
color="danger"
|
||||
onClick={() => onRemoveNetworkClicked()}
|
||||
>
|
||||
<Icon
|
||||
icon="trash-2"
|
||||
feather
|
||||
className="space-right"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete this network
|
||||
</Button>
|
||||
</Authorized>
|
||||
)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Driver">{network.Driver}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Scope">{network.Scope}</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Attachable">
|
||||
{String(network.Attachable)}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row label="Internal">
|
||||
{String(network.Internal)}
|
||||
</DetailsTable.Row>
|
||||
|
||||
{/* IPV4 ConfigRowContent */}
|
||||
{ipv4Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
{/* IPV4 ConfigRowContent */}
|
||||
{ipv4Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV4 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV4 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV4 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{/* IPV6 ConfigRowContent */}
|
||||
{ipv6Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
{/* IPV6 ConfigRowContent */}
|
||||
{ipv6Configs.map((config) => (
|
||||
<Fragment key={config.Subnet}>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 Subnet${getConfigDetails(config.Subnet)}`}
|
||||
>
|
||||
{`IPV6 Gateway${getConfigDetails(config.Gateway)}`}
|
||||
</DetailsTable.Row>
|
||||
<DetailsTable.Row
|
||||
label={`IPV6 IP Range${getConfigDetails(config.IPRange)}`}
|
||||
>
|
||||
{`IPV6 Excluded IPs${getAuxiliaryAddresses(
|
||||
config.AuxiliaryAddresses
|
||||
)}`}
|
||||
</DetailsTable.Row>
|
||||
</Fragment>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
function getConfigDetails(configValue?: string) {
|
||||
|
|
|
@ -15,21 +15,17 @@ export function NetworkOptionsTable({ options }: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-lg-12 col-md-12 col-xs-12">
|
||||
<TableContainer>
|
||||
<TableTitle label="Network options" icon="share-2" featherIcon />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||
{networkEntries.map(([key, value]) => (
|
||||
<DetailsTable.Row key={key} label={key}>
|
||||
{value}
|
||||
</DetailsTable.Row>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
<TableContainer>
|
||||
<TableTitle label="Network options" icon="share-2" featherIcon />
|
||||
<Table className="nopadding">
|
||||
<DetailsTable dataCy="networkDetails-networkOptionsTable">
|
||||
{networkEntries.map(([key, value]) => (
|
||||
<DetailsTable.Row key={key} label={key}>
|
||||
{value}
|
||||
</DetailsTable.Row>
|
||||
))}
|
||||
</DetailsTable>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import _ from 'lodash';
|
||||
import { useStore } from 'zustand';
|
||||
import { Box } from 'react-feather';
|
||||
|
||||
import { DockerContainer } from '@/react/docker/containers/types';
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
|
@ -14,12 +16,14 @@ import {
|
|||
QuickActionsSettings,
|
||||
} from '@@/datatables/QuickActionsSettings';
|
||||
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
|
||||
import { useContainers } from '../../containers/queries/containers';
|
||||
import { RowProvider } from '../../containers/ListView/ContainersDatatable/RowContext';
|
||||
|
||||
const storageKey = 'stack-containers';
|
||||
const useStore = createStore(storageKey);
|
||||
const settingsStore = createStore(storageKey);
|
||||
|
||||
const actions = [
|
||||
buildAction('logs', 'Logs'),
|
||||
|
@ -35,9 +39,12 @@ export interface Props {
|
|||
}
|
||||
|
||||
export function StackContainersDatatable({ environment, stackName }: Props) {
|
||||
const settings = useStore();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const isGPUsColumnVisible = useShowGPUsColumn(environment.Id);
|
||||
const columns = useColumns(false, isGPUsColumnVisible);
|
||||
|
||||
const hidableColumns = _.compact(
|
||||
columns.filter((col) => col.canHide).map((col) => col.id)
|
||||
);
|
||||
|
@ -53,49 +60,53 @@ export function StackContainersDatatable({ environment, stackName }: Props) {
|
|||
|
||||
return (
|
||||
<RowProvider context={{ environment }}>
|
||||
<Datatable
|
||||
titleOptions={{
|
||||
icon: 'fa-cubes',
|
||||
title: 'Containers',
|
||||
}}
|
||||
settingsStore={settings}
|
||||
columns={columns}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<ContainersDatatableActions
|
||||
selectedItems={selectedRows}
|
||||
isAddActionVisible={false}
|
||||
endpointId={environment.Id}
|
||||
/>
|
||||
)}
|
||||
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
||||
renderTableSettings={(tableInstance) => {
|
||||
const columnsToHide = tableInstance.allColumns.filter((colInstance) =>
|
||||
hidableColumns?.includes(colInstance.id)
|
||||
);
|
||||
<TableSettingsProvider settings={settingsStore}>
|
||||
<Datatable
|
||||
title="Containers"
|
||||
titleIcon={Box}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
columns={columns}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<ContainersDatatableActions
|
||||
selectedItems={selectedRows}
|
||||
isAddActionVisible={false}
|
||||
endpointId={environment.Id}
|
||||
/>
|
||||
)}
|
||||
initialTableState={{ hiddenColumns: settings.hiddenColumns }}
|
||||
renderTableSettings={(tableInstance) => {
|
||||
const columnsToHide = tableInstance.allColumns.filter(
|
||||
(colInstance) => hidableColumns?.includes(colInstance.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ColumnVisibilityMenu<DockerContainer>
|
||||
columns={columnsToHide}
|
||||
onChange={(hiddenColumns) => {
|
||||
settings.setHiddenColumns(hiddenColumns);
|
||||
tableInstance.setHiddenColumns(hiddenColumns);
|
||||
}}
|
||||
value={settings.hiddenColumns}
|
||||
/>
|
||||
<TableSettingsMenu
|
||||
quickActions={<QuickActionsSettings actions={actions} />}
|
||||
>
|
||||
<ContainersDatatableSettings settings={settings} />
|
||||
</TableSettingsMenu>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
storageKey={storageKey}
|
||||
dataset={containersQuery.data || []}
|
||||
isLoading={containersQuery.isLoading}
|
||||
emptyContentLabel="No containers found"
|
||||
/>
|
||||
return (
|
||||
<>
|
||||
<ColumnVisibilityMenu<DockerContainer>
|
||||
columns={columnsToHide}
|
||||
onChange={(hiddenColumns) => {
|
||||
settings.setHiddenColumns(hiddenColumns);
|
||||
tableInstance.setHiddenColumns(hiddenColumns);
|
||||
}}
|
||||
value={settings.hiddenColumns}
|
||||
/>
|
||||
<TableSettingsMenu
|
||||
quickActions={<QuickActionsSettings actions={actions} />}
|
||||
>
|
||||
<ContainersDatatableSettings settings={settings} />
|
||||
</TableSettingsMenu>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
dataset={containersQuery.data || []}
|
||||
isLoading={containersQuery.isLoading}
|
||||
emptyContentLabel="No containers found"
|
||||
/>
|
||||
</TableSettingsProvider>
|
||||
</RowProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,101 +1,30 @@
|
|||
import { usePagination, useTable } from 'react-table';
|
||||
|
||||
import { Device } from '@/portainer/hostmanagement/open-amt/model';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import PortainerError from '@/portainer/error';
|
||||
|
||||
import { InnerDatatable } from '@@/datatables/InnerDatatable';
|
||||
import { Table, TableContainer, TableHeaderRow, TableRow } from '@@/datatables';
|
||||
import { NestedDatatable } from '@@/datatables/NestedDatatable';
|
||||
|
||||
import { useAMTDevices } from './useAMTDevices';
|
||||
import { RowProvider } from './columns/RowContext';
|
||||
import { useColumns } from './columns';
|
||||
import { columns } from './columns';
|
||||
|
||||
export interface AMTDevicesTableProps {
|
||||
environmentId: EnvironmentId;
|
||||
}
|
||||
|
||||
export function AMTDevicesDatatable({ environmentId }: AMTDevicesTableProps) {
|
||||
const columns = useColumns();
|
||||
|
||||
const { isLoading, devices, error } = useAMTDevices(environmentId);
|
||||
|
||||
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
|
||||
useTable<Device>(
|
||||
{
|
||||
columns,
|
||||
data: devices,
|
||||
},
|
||||
usePagination
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const devicesQuery = useAMTDevices(environmentId);
|
||||
|
||||
return (
|
||||
<InnerDatatable>
|
||||
<TableContainer>
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<TableHeaderRow<Device>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
{!isLoading && devices && devices.length > 0 ? (
|
||||
page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, className, role, style } = row.getRowProps();
|
||||
|
||||
return (
|
||||
<RowProvider key={key} environmentId={environmentId}>
|
||||
<TableRow<Device>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
</RowProvider>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center text-muted">
|
||||
{userMessage(isLoading, error)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</InnerDatatable>
|
||||
<NestedDatatable
|
||||
columns={columns}
|
||||
dataset={devicesQuery.devices}
|
||||
isLoading={devicesQuery.isLoading}
|
||||
emptyContentLabel={userMessage(devicesQuery.error)}
|
||||
defaultSortBy="hostname"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function userMessage(isLoading: boolean, error?: PortainerError) {
|
||||
if (isLoading) {
|
||||
return 'Loading...';
|
||||
}
|
||||
|
||||
function userMessage(error?: PortainerError) {
|
||||
if (error) {
|
||||
return error.message;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { hostname } from './hostname';
|
||||
import { status } from './status';
|
||||
import { powerState } from './power-state';
|
||||
import { actions } from './actions';
|
||||
|
||||
export function useColumns() {
|
||||
return useMemo(() => [hostname, status, powerState, actions], []);
|
||||
}
|
||||
export const columns = [hostname, status, powerState, actions];
|
||||
|
|
|
@ -1,37 +1,26 @@
|
|||
import { useTable, useExpanded, useSortBy, useFilters } from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import _ from 'lodash';
|
||||
import { useStore } from 'zustand';
|
||||
import { Box } from 'react-feather';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
import { EdgeTypes, Environment } from '@/react/portainer/environments/types';
|
||||
import { EnvironmentGroup } from '@/react/portainer/environments/environment-groups/types';
|
||||
import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import {
|
||||
Table,
|
||||
TableActions,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableSettingsMenu,
|
||||
TableTitle,
|
||||
TableTitleActions,
|
||||
} from '@@/datatables';
|
||||
import { multiple } from '@@/datatables/filter-types';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
import { TableSettingsMenu } from '@@/datatables';
|
||||
import { ColumnVisibilityMenu } from '@@/datatables/ColumnVisibilityMenu';
|
||||
import { SearchBar } from '@@/datatables/SearchBar';
|
||||
import { useRowSelect } from '@@/datatables/useRowSelect';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import { InformationPanel } from '@@/InformationPanel';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { AMTDevicesDatatable } from './AMTDevicesDatatable';
|
||||
import { columns } from './columns';
|
||||
import { EdgeDevicesDatatableActions } from './EdgeDevicesDatatableActions';
|
||||
import { EdgeDevicesDatatableSettings } from './EdgeDevicesDatatableSettings';
|
||||
import { RowProvider } from './columns/RowContext';
|
||||
import { useColumns } from './columns';
|
||||
import styles from './EdgeDevicesDatatable.module.css';
|
||||
import { EdgeDeviceTableSettings, Pagination } from './types';
|
||||
import { createStore } from './datatable-store';
|
||||
|
||||
export interface EdgeDevicesTableProps {
|
||||
storageKey: string;
|
||||
|
@ -39,231 +28,123 @@ export interface EdgeDevicesTableProps {
|
|||
isOpenAmtEnabled: boolean;
|
||||
showWaitingRoomLink: boolean;
|
||||
mpsServer: string;
|
||||
dataset: Environment[];
|
||||
groups: EnvironmentGroup[];
|
||||
setLoadingMessage(message: string): void;
|
||||
pagination: Pagination;
|
||||
onChangePagination(pagination: Partial<Pagination>): void;
|
||||
totalCount: number;
|
||||
search: string;
|
||||
onChangeSearch(search: string): void;
|
||||
}
|
||||
const storageKey = 'edgeDevices';
|
||||
|
||||
const settingsStore = createStore(storageKey);
|
||||
|
||||
export function EdgeDevicesDatatable({
|
||||
isFdoEnabled,
|
||||
isOpenAmtEnabled,
|
||||
showWaitingRoomLink,
|
||||
mpsServer,
|
||||
dataset,
|
||||
onChangeSearch,
|
||||
search,
|
||||
groups,
|
||||
setLoadingMessage,
|
||||
pagination,
|
||||
onChangePagination,
|
||||
totalCount,
|
||||
}: EdgeDevicesTableProps) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<EdgeDeviceTableSettings>();
|
||||
const settings = useStore(settingsStore);
|
||||
const [page, setPage] = useState(0);
|
||||
|
||||
const columns = useColumns();
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
rows,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
allColumns,
|
||||
setHiddenColumns,
|
||||
} = useTable<Environment>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: dataset,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
hiddenColumns: settings.hiddenColumns,
|
||||
sortBy: [settings.sortBy],
|
||||
},
|
||||
isRowSelectable() {
|
||||
return true;
|
||||
},
|
||||
autoResetExpanded: false,
|
||||
autoResetSelectedRows: false,
|
||||
getRowId(originalRow: Environment) {
|
||||
return originalRow.Id.toString();
|
||||
},
|
||||
selectColumnWidth: 5,
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
useExpanded,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
const hidableColumns = _.compact(
|
||||
columns.filter((col) => col.canHide).map((col) => col.id)
|
||||
);
|
||||
|
||||
const columnsToHide = allColumns.filter((colInstance) => {
|
||||
const columnDef = columns.find((c) => c.id === colInstance.id);
|
||||
return columnDef?.canHide;
|
||||
});
|
||||
const { environments, isLoading, totalCount } = useEnvironmentList(
|
||||
{
|
||||
edgeDevice: true,
|
||||
search,
|
||||
types: EdgeTypes,
|
||||
excludeSnapshots: true,
|
||||
page: page + 1,
|
||||
pageLimit: settings.pageSize,
|
||||
sort: settings.sortBy.id,
|
||||
order: settings.sortBy.desc ? 'desc' : 'asc',
|
||||
},
|
||||
settings.autoRefreshRate * 1000
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
const someDeviceHasAMTActivated = dataset.some(
|
||||
const someDeviceHasAMTActivated = environments.some(
|
||||
(environment) =>
|
||||
environment.AMTDeviceGUID && environment.AMTDeviceGUID !== ''
|
||||
);
|
||||
|
||||
const groupsById = _.groupBy(groups, 'Id');
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableContainer>
|
||||
<TableTitle icon="box" featherIcon label="Edge Devices">
|
||||
<SearchBar value={search} onChange={handleSearchBarChange} />
|
||||
<TableActions>
|
||||
<EdgeDevicesDatatableActions
|
||||
selectedItems={selectedFlatRows.map((row) => row.original)}
|
||||
isFDOEnabled={isFdoEnabled}
|
||||
isOpenAMTEnabled={isOpenAmtEnabled}
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
showWaitingRoomLink={showWaitingRoomLink}
|
||||
/>
|
||||
</TableActions>
|
||||
<TableTitleActions>
|
||||
<ColumnVisibilityMenu<Environment>
|
||||
columns={columnsToHide}
|
||||
onChange={handleChangeColumnsVisibility}
|
||||
value={settings.hiddenColumns}
|
||||
/>
|
||||
<TableSettingsMenu>
|
||||
<EdgeDevicesDatatableSettings />
|
||||
</TableSettingsMenu>
|
||||
</TableTitleActions>
|
||||
</TableTitle>
|
||||
{isOpenAmtEnabled && someDeviceHasAMTActivated && (
|
||||
<div className={styles.kvmTip}>
|
||||
<TextTip color="blue">
|
||||
For the KVM function to work you need to have the MPS server
|
||||
added to your trusted site list, browse to this{' '}
|
||||
<a
|
||||
href={`https://${mpsServer}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="space-right"
|
||||
>
|
||||
site
|
||||
</a>
|
||||
and add to your trusted site list
|
||||
</TextTip>
|
||||
</div>
|
||||
<>
|
||||
{isOpenAmtEnabled && someDeviceHasAMTActivated && (
|
||||
<InformationPanel>
|
||||
<div className={styles.kvmTip}>
|
||||
<TextTip color="blue">
|
||||
For the KVM function to work you need to have the MPS server added
|
||||
to your trusted site list, browse to this
|
||||
<a
|
||||
href={`https://${mpsServer}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="mx-px"
|
||||
>
|
||||
site
|
||||
</a>
|
||||
and add to your trusted site list
|
||||
</TextTip>
|
||||
</div>
|
||||
</InformationPanel>
|
||||
)}
|
||||
<RowProvider context={{ isOpenAmtEnabled, groups }}>
|
||||
<ExpandableDatatable
|
||||
dataset={environments}
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
totalCount={totalCount}
|
||||
title="Edge Devices"
|
||||
titleIcon={Box}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
renderSubRow={(row) => (
|
||||
<tr>
|
||||
<td />
|
||||
<td colSpan={row.cells.length - 1}>
|
||||
<AMTDevicesDatatable environmentId={row.original.Id} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<TableHeaderRow<Environment>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<Table.Content
|
||||
prepareRow={prepareRow}
|
||||
rows={rows}
|
||||
renderRow={(row, { key, className, role, style }) => {
|
||||
const group = groupsById[row.original.GroupId];
|
||||
|
||||
return (
|
||||
<RowProvider
|
||||
key={key}
|
||||
context={{ isOpenAmtEnabled, groupName: group[0]?.Name }}
|
||||
>
|
||||
<TableRow<Environment>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
{row.isExpanded && (
|
||||
<tr>
|
||||
<td />
|
||||
<td colSpan={row.cells.length - 1}>
|
||||
<AMTDevicesDatatable
|
||||
environmentId={row.original.Id}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</RowProvider>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
isPageInputVisible
|
||||
pageLimit={pagination.pageLimit}
|
||||
page={pagination.page}
|
||||
onPageChange={(p) => gotoPage(p)}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
initialTableState={{ pageIndex: page }}
|
||||
pageCount={Math.ceil(totalCount / settings.pageSize)}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<EdgeDevicesDatatableActions
|
||||
selectedItems={selectedRows}
|
||||
isFDOEnabled={isFdoEnabled}
|
||||
isOpenAMTEnabled={isOpenAmtEnabled}
|
||||
showWaitingRoomLink={showWaitingRoomLink}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
renderTableSettings={(tableInstance) => {
|
||||
const columnsToHide = tableInstance.allColumns.filter(
|
||||
(colInstance) => hidableColumns?.includes(colInstance.id)
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<ColumnVisibilityMenu<Environment>
|
||||
columns={columnsToHide}
|
||||
onChange={(hiddenColumns) => {
|
||||
settings.setHiddenColumns(hiddenColumns);
|
||||
tableInstance.setHiddenColumns(hiddenColumns);
|
||||
}}
|
||||
value={settings.hiddenColumns}
|
||||
/>
|
||||
<TableSettingsMenu>
|
||||
<EdgeDevicesDatatableSettings settings={settings} />
|
||||
</TableSettingsMenu>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
</RowProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
function gotoPage(pageIndex: number) {
|
||||
onChangePagination({ page: pageIndex });
|
||||
}
|
||||
|
||||
function setPageSize(pageSize: number) {
|
||||
onChangePagination({ pageLimit: pageSize });
|
||||
}
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
|
||||
function handleChangeColumnsVisibility(hiddenColumns: string[]) {
|
||||
setHiddenColumns(hiddenColumns);
|
||||
setTableSettings((settings) => ({ ...settings, hiddenColumns }));
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
onChangeSearch(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setTableSettings((settings) => ({
|
||||
...settings,
|
||||
sortBy: { id, desc },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,8 @@ import {
|
|||
} from '@/portainer/services/modal.service/confirm';
|
||||
import { promptAsync } from '@/portainer/services/modal.service/prompt';
|
||||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { activateDevice } from '@/portainer/hostmanagement/open-amt/open-amt.service';
|
||||
import { deleteEndpoint } from '@/react/portainer/environments/environment.service';
|
||||
import { useActivateDeviceMutation } from '@/portainer/hostmanagement/open-amt/queries';
|
||||
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
|
@ -17,7 +17,6 @@ interface Props {
|
|||
selectedItems: Environment[];
|
||||
isFDOEnabled: boolean;
|
||||
isOpenAMTEnabled: boolean;
|
||||
setLoadingMessage(message: string): void;
|
||||
showWaitingRoomLink: boolean;
|
||||
}
|
||||
|
||||
|
@ -30,10 +29,10 @@ export function EdgeDevicesDatatableActions({
|
|||
selectedItems,
|
||||
isOpenAMTEnabled,
|
||||
isFDOEnabled,
|
||||
setLoadingMessage,
|
||||
showWaitingRoomLink,
|
||||
}: Props) {
|
||||
const router = useRouter();
|
||||
const activateDeviceMutation = useActivateDeviceMutation();
|
||||
|
||||
return (
|
||||
<div className="actionBar">
|
||||
|
@ -169,23 +168,13 @@ export function EdgeDevicesDatatableActions({
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoadingMessage(
|
||||
'Activating Active Management Technology on selected device...'
|
||||
);
|
||||
await activateDevice(selectedEnvironment.Id);
|
||||
notifications.success(
|
||||
'Successfully associated with OpenAMT',
|
||||
selectedEnvironment.Name
|
||||
);
|
||||
} catch (err) {
|
||||
notifications.error(
|
||||
'Failure',
|
||||
err as Error,
|
||||
'Unable to associate with OpenAMT'
|
||||
);
|
||||
} finally {
|
||||
setLoadingMessage('');
|
||||
}
|
||||
activateDeviceMutation.mutate(selectedEnvironment.Id, {
|
||||
onSuccess() {
|
||||
notifications.notifySuccess(
|
||||
'Successfully associated with OpenAMT',
|
||||
selectedEnvironment.Name
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,121 +0,0 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { useEnvironmentList } from '@/react/portainer/environments/queries/useEnvironmentList';
|
||||
import { EdgeTypes, Environment } from '@/react/portainer/environments/types';
|
||||
import { useDebouncedValue } from '@/react/hooks/useDebouncedValue';
|
||||
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import {
|
||||
TableSettingsProvider,
|
||||
useTableSettings,
|
||||
} from '@@/datatables/useTableSettings';
|
||||
|
||||
import {
|
||||
EdgeDevicesDatatable,
|
||||
EdgeDevicesTableProps,
|
||||
} from './EdgeDevicesDatatable';
|
||||
import { EdgeDeviceTableSettings, Pagination } from './types';
|
||||
|
||||
export function EdgeDevicesDatatableContainer({
|
||||
...props
|
||||
}: Omit<
|
||||
EdgeDevicesTableProps,
|
||||
| 'dataset'
|
||||
| 'pagination'
|
||||
| 'onChangePagination'
|
||||
| 'totalCount'
|
||||
| 'search'
|
||||
| 'onChangeSearch'
|
||||
>) {
|
||||
const defaultSettings = {
|
||||
autoRefreshRate: 0,
|
||||
hiddenQuickActions: [],
|
||||
hiddenColumns: [],
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'state', desc: false },
|
||||
};
|
||||
|
||||
const storageKey = 'edgeDevices';
|
||||
|
||||
return (
|
||||
<TableSettingsProvider defaults={defaultSettings} storageKey={storageKey}>
|
||||
<Loader storageKey={storageKey}>
|
||||
{({
|
||||
environments,
|
||||
pagination,
|
||||
totalCount,
|
||||
setPagination,
|
||||
search,
|
||||
setSearch,
|
||||
}) => (
|
||||
<EdgeDevicesDatatable
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
storageKey={storageKey}
|
||||
dataset={environments}
|
||||
pagination={pagination}
|
||||
onChangePagination={setPagination}
|
||||
totalCount={totalCount}
|
||||
search={search}
|
||||
onChangeSearch={setSearch}
|
||||
/>
|
||||
)}
|
||||
</Loader>
|
||||
</TableSettingsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
interface LoaderProps {
|
||||
storageKey: string;
|
||||
children: (options: {
|
||||
environments: Environment[];
|
||||
totalCount: number;
|
||||
pagination: Pagination;
|
||||
setPagination(value: Partial<Pagination>): void;
|
||||
search: string;
|
||||
setSearch: (value: string) => void;
|
||||
}) => React.ReactNode;
|
||||
}
|
||||
|
||||
function Loader({ children, storageKey }: LoaderProps) {
|
||||
const { settings } = useTableSettings<EdgeDeviceTableSettings>();
|
||||
const [pagination, setPagination] = useState({
|
||||
pageLimit: settings.pageSize,
|
||||
page: 1,
|
||||
});
|
||||
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const debouncedSearchValue = useDebouncedValue(search);
|
||||
|
||||
const { environments, isLoading, totalCount } = useEnvironmentList(
|
||||
{
|
||||
edgeDevice: true,
|
||||
search: debouncedSearchValue,
|
||||
types: EdgeTypes,
|
||||
excludeSnapshots: true,
|
||||
...pagination,
|
||||
},
|
||||
settings.autoRefreshRate * 1000
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{children({
|
||||
environments,
|
||||
totalCount,
|
||||
pagination,
|
||||
setPagination: handleSetPagination,
|
||||
search,
|
||||
setSearch,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
||||
function handleSetPagination(value: Partial<Pagination>) {
|
||||
setPagination((prev) => ({ ...prev, ...value }));
|
||||
}
|
||||
}
|
|
@ -1,12 +1,11 @@
|
|||
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { RefreshableTableSettings } from '@@/datatables/types';
|
||||
|
||||
import { EdgeDeviceTableSettings } from './types';
|
||||
|
||||
export function EdgeDevicesDatatableSettings() {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<EdgeDeviceTableSettings>();
|
||||
interface Props {
|
||||
settings: RefreshableTableSettings;
|
||||
}
|
||||
|
||||
export function EdgeDevicesDatatableSettings({ settings }: Props) {
|
||||
return (
|
||||
<TableSettingsMenuAutoRefresh
|
||||
value={settings.autoRefreshRate}
|
||||
|
@ -15,6 +14,6 @@ export function EdgeDevicesDatatableSettings() {
|
|||
);
|
||||
|
||||
function handleRefreshRateChange(autoRefreshRate: number) {
|
||||
setTableSettings({ autoRefreshRate });
|
||||
settings.setAutoRefreshRate(autoRefreshRate);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import { EnvironmentGroup } from '@/react/portainer/environments/environment-groups/types';
|
||||
|
||||
import { createRowContext } from '@@/datatables/RowContext';
|
||||
|
||||
interface RowContextState {
|
||||
isOpenAmtEnabled: boolean;
|
||||
groupName?: string;
|
||||
groups: EnvironmentGroup[];
|
||||
}
|
||||
|
||||
const { RowProvider, useRowContext } = createRowContext<RowContextState>();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
import { EnvironmentGroupId } from '@/react/portainer/environments/environment-groups/types';
|
||||
|
||||
import { DefaultFilter } from '@@/datatables/Filter';
|
||||
|
||||
|
@ -15,8 +16,9 @@ export const group: Column<Environment> = {
|
|||
canHide: true,
|
||||
};
|
||||
|
||||
function GroupCell() {
|
||||
const { groupName } = useRowContext();
|
||||
function GroupCell({ value }: { value: EnvironmentGroupId }) {
|
||||
const { groups } = useRowContext();
|
||||
const group = groups.find((g) => g.Id === value);
|
||||
|
||||
return groupName;
|
||||
return group?.Name || '';
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { name } from './name';
|
||||
import { heartbeat } from './heartbeat';
|
||||
import { group } from './group';
|
||||
import { actions } from './actions';
|
||||
|
||||
export function useColumns() {
|
||||
return useMemo(() => [name, heartbeat, group, actions], []);
|
||||
}
|
||||
export const columns = [name, heartbeat, group, actions];
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import {
|
||||
refreshableSettings,
|
||||
hiddenColumnsSettings,
|
||||
createPersistedStore,
|
||||
} from '@@/datatables/types';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return createPersistedStore<TableSettings>(storageKey, 'Name', (set) => ({
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
}));
|
||||
}
|
|
@ -1,17 +1,10 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
BasicTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types-old';
|
||||
} from '@@/datatables/types';
|
||||
|
||||
export interface Pagination {
|
||||
pageLimit: number;
|
||||
page: number;
|
||||
}
|
||||
|
||||
export interface EdgeDeviceTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
export interface TableSettings
|
||||
extends BasicTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { useState } from 'react';
|
||||
import { useIsMutating } from 'react-query';
|
||||
|
||||
import { useSettings } from '@/react/portainer/settings/queries';
|
||||
import { useGroups } from '@/react/portainer/environments/environment-groups/queries';
|
||||
import { activateDeviceMutationKey } from '@/portainer/hostmanagement/open-amt/queries';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { ViewLoading } from '@@/ViewLoading';
|
||||
|
||||
import { EdgeDevicesDatatableContainer } from './EdgeDevicesDatatable/EdgeDevicesDatatableContainer';
|
||||
import { EdgeDevicesDatatable } from './EdgeDevicesDatatable/EdgeDevicesDatatable';
|
||||
|
||||
export function ListView() {
|
||||
const [loadingMessage, setLoadingMessage] = useState('');
|
||||
|
||||
const isActivatingDevice = useIsActivatingDevice();
|
||||
const settingsQuery = useSettings();
|
||||
const groupsQuery = useGroups();
|
||||
|
||||
|
@ -28,11 +28,10 @@ export function ListView() {
|
|||
breadcrumbs={[{ label: 'EdgeDevices' }]}
|
||||
/>
|
||||
|
||||
{loadingMessage ? (
|
||||
<ViewLoading message={loadingMessage} />
|
||||
{isActivatingDevice ? (
|
||||
<ViewLoading message="Activating Active Management Technology on selected device..." />
|
||||
) : (
|
||||
<EdgeDevicesDatatableContainer
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
<EdgeDevicesDatatable
|
||||
isFdoEnabled={
|
||||
settings.EnableEdgeComputeFeatures &&
|
||||
settings.fdoConfiguration.enabled
|
||||
|
@ -54,3 +53,8 @@ export function ListView() {
|
|||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function useIsActivatingDevice() {
|
||||
const count = useIsMutating({ mutationKey: activateDeviceMutationKey });
|
||||
return count > 0;
|
||||
}
|
||||
|
|
|
@ -1,207 +1,70 @@
|
|||
import {
|
||||
Column,
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
||||
import { Datatable as GenericDatatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Table } from '@@/datatables';
|
||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { useAssociateDeviceMutation } from '../queries';
|
||||
import { useAssociateDeviceMutation, useLicenseOverused } from '../queries';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
import { columns } from './columns';
|
||||
|
||||
const columns: readonly Column<Environment>[] = [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: (row) => row.Name,
|
||||
id: 'name',
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
},
|
||||
{
|
||||
Header: 'Edge ID',
|
||||
accessor: (row) => row.EdgeID,
|
||||
id: 'edge-id',
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
},
|
||||
] as const;
|
||||
const storageKey = 'edge-devices-waiting-room';
|
||||
|
||||
const settingsStore = createPersistedStore(storageKey, 'Name');
|
||||
|
||||
interface Props {
|
||||
devices: Environment[];
|
||||
isLoading: boolean;
|
||||
totalCount: number;
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
export function DataTable({
|
||||
devices,
|
||||
storageKey,
|
||||
isLoading,
|
||||
totalCount,
|
||||
}: Props) {
|
||||
export function Datatable({ devices, isLoading, totalCount }: Props) {
|
||||
const associateMutation = useAssociateDeviceMutation();
|
||||
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
||||
const { settings, setTableSettings } = useTableSettings<TableSettings>();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Environment>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: devices,
|
||||
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
},
|
||||
isRowSelectable() {
|
||||
return true;
|
||||
},
|
||||
autoResetSelectedRows: false,
|
||||
getRowId(originalRow: Environment) {
|
||||
return originalRow.Id.toString();
|
||||
},
|
||||
selectColumnWidth: 5,
|
||||
},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const licenseOverused = useLicenseOverused();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Table.Container>
|
||||
<Table.Title label="Edge Devices Waiting Room" icon="">
|
||||
<SearchBar
|
||||
onChange={handleSearchBarChange}
|
||||
value={searchBarValue}
|
||||
/>
|
||||
<Table.Actions>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleAssociateDevice(selectedFlatRows.map((r) => r.original))
|
||||
}
|
||||
disabled={selectedFlatRows.length === 0}
|
||||
>
|
||||
Associate Device
|
||||
</Button>
|
||||
</Table.Actions>
|
||||
</Table.Title>
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
<GenericDatatable
|
||||
columns={columns}
|
||||
dataset={devices}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
title="Edge Devices Waiting Room"
|
||||
emptyContentLabel="No Edge Devices found"
|
||||
renderTableActions={(selectedRows) => (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => handleAssociateDevice(selectedRows)}
|
||||
disabled={selectedRows.length === 0}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
Associate Device
|
||||
</Button>
|
||||
|
||||
return (
|
||||
<Table.HeaderRow<Environment>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<Table.Content
|
||||
emptyContent="No Edge Devices found"
|
||||
prepareRow={prepareRow}
|
||||
rows={page}
|
||||
isLoading={isLoading}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<Table.Row
|
||||
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={handlePageLimitChange}
|
||||
/>
|
||||
</Table.Footer>
|
||||
</Table.Container>
|
||||
</div>
|
||||
</div>
|
||||
{licenseOverused ? (
|
||||
<div className="ml-2 mt-2">
|
||||
<TextTip color="orange">
|
||||
Associating devices is disabled as your node count exceeds your
|
||||
license limit
|
||||
</TextTip>
|
||||
</div>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
isLoading={isLoading}
|
||||
totalCount={totalCount}
|
||||
/>
|
||||
);
|
||||
|
||||
function handleSortChange(colId: string, desc: boolean) {
|
||||
setTableSettings({ sortBy: { id: colId, desc } });
|
||||
}
|
||||
|
||||
function handlePageLimitChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings({ pageSize });
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
setGlobalFilter(value);
|
||||
setSearchBarValue(value);
|
||||
}
|
||||
|
||||
function handleAssociateDevice(devices: Environment[]) {
|
||||
associateMutation.mutate(
|
||||
devices.map((d) => d.Id),
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { Environment } from '@/react/portainer/environments/types';
|
||||
|
||||
export const columns: readonly Column<Environment>[] = [
|
||||
{
|
||||
Header: 'Name',
|
||||
accessor: (row) => row.Name,
|
||||
id: 'name',
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
},
|
||||
{
|
||||
Header: 'Edge ID',
|
||||
accessor: (row) => row.EdgeID,
|
||||
id: 'edge-id',
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
},
|
||||
] as const;
|
|
@ -0,0 +1 @@
|
|||
export { Datatable } from './Datatable';
|
|
@ -1,8 +0,0 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types-old';
|
||||
|
||||
export interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
|
@ -5,14 +5,11 @@ import { EdgeTypes } from '@/react/portainer/environments/types';
|
|||
|
||||
import { InformationPanel } from '@@/InformationPanel';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { DataTable } from './Datatable/Datatable';
|
||||
import { TableSettings } from './Datatable/types';
|
||||
import { Datatable } from './Datatable';
|
||||
|
||||
export function WaitingRoomView() {
|
||||
const storageKey = 'edge-devices-waiting-room';
|
||||
const router = useRouter();
|
||||
const { environments, isLoading, totalCount } = useEnvironmentList({
|
||||
edgeDevice: true,
|
||||
|
@ -44,17 +41,11 @@ export function WaitingRoomView() {
|
|||
</TextTip>
|
||||
</InformationPanel>
|
||||
|
||||
<TableSettingsProvider<TableSettings>
|
||||
defaults={{ pageSize: 10, sortBy: { desc: false, id: 'name' } }}
|
||||
storageKey={storageKey}
|
||||
>
|
||||
<DataTable
|
||||
devices={environments}
|
||||
totalCount={totalCount}
|
||||
isLoading={isLoading}
|
||||
storageKey={storageKey}
|
||||
/>
|
||||
</TableSettingsProvider>
|
||||
<Datatable
|
||||
devices={environments}
|
||||
totalCount={totalCount}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from 'react-query';
|
|||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
import { useIntegratedLicenseInfo } from '@/portainer/license-management/use-license.service';
|
||||
|
||||
export function useAssociateDeviceMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
@ -31,3 +32,11 @@ async function associateDevice(environmentId: EnvironmentId) {
|
|||
throw parseAxiosError(e as Error, 'Failed to associate device');
|
||||
}
|
||||
}
|
||||
|
||||
export function useLicenseOverused() {
|
||||
const integratedInfo = useIntegratedLicenseInfo();
|
||||
if (integratedInfo && integratedInfo.licenseInfo.enforcedAt > 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,21 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import { Database } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { confirmWarn } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button, ButtonGroup } from '@@/buttons';
|
||||
import { Icon } from '@@/Icon';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
|
||||
import { IngressControllerClassMap } from '../types';
|
||||
|
||||
import { useColumns } from './columns';
|
||||
import { createStore } from './datatable-store';
|
||||
|
||||
const useStore = createStore('ingressClasses');
|
||||
const storageKey = 'ingressClasses';
|
||||
const settingsStore = createPersistedStore(storageKey);
|
||||
|
||||
interface Props {
|
||||
onChangeControllers: (
|
||||
|
@ -34,10 +38,11 @@ export function IngressClassDatatable({
|
|||
noIngressControllerLabel,
|
||||
view,
|
||||
}: Props) {
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const [ingControllerFormValues, setIngControllerFormValues] = useState(
|
||||
ingressControllers || []
|
||||
);
|
||||
const settings = useStore();
|
||||
const columns = useColumns();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -76,19 +81,20 @@ export function IngressClassDatatable({
|
|||
<div className="-mx-[15px]">
|
||||
<Datatable
|
||||
dataset={ingControllerFormValues || []}
|
||||
storageKey="ingressClasses"
|
||||
columns={columns}
|
||||
settingsStore={settings}
|
||||
isLoading={isLoading}
|
||||
emptyContentLabel={noIngressControllerLabel}
|
||||
titleOptions={{
|
||||
icon: 'database',
|
||||
title: 'Ingress controllers',
|
||||
featherIcon: true,
|
||||
}}
|
||||
title="Ingress Controllers"
|
||||
titleIcon={Database}
|
||||
getRowId={(row) => `${row.Name}-${row.ClassName}-${row.Type}`}
|
||||
renderTableActions={(selectedRows) => renderTableActions(selectedRows)}
|
||||
description={renderIngressClassDescription()}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
|
||||
export const TRUNCATE_LENGTH = 32;
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set),
|
||||
...paginationSettings(set),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { Plus, Trash2 } from 'react-feather';
|
||||
import { useRouter } from '@uirouter/react';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { useNamespaces } from '@/react/kubernetes/namespaces/queries';
|
||||
|
@ -9,11 +10,12 @@ import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm
|
|||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { DeleteIngressesRequest, Ingress } from '../types';
|
||||
import { useDeleteIngresses, useIngresses } from '../queries';
|
||||
|
||||
import { createStore } from './datatable-store';
|
||||
import { useColumns } from './columns';
|
||||
|
||||
import '../style.css';
|
||||
|
@ -22,10 +24,11 @@ interface SelectedIngress {
|
|||
Namespace: string;
|
||||
Name: string;
|
||||
}
|
||||
const storageKey = 'ingressClassesNameSpace';
|
||||
|
||||
const useStore = createStore('ingresses');
|
||||
const settingsStore = createPersistedStore(storageKey);
|
||||
|
||||
export function IngressDataTable() {
|
||||
export function IngressDatatable() {
|
||||
const environmentId = useEnvironmentId();
|
||||
|
||||
const nsResult = useNamespaces(environmentId);
|
||||
|
@ -34,28 +37,30 @@ export function IngressDataTable() {
|
|||
Object.keys(nsResult?.data || {})
|
||||
);
|
||||
|
||||
const settings = useStore();
|
||||
|
||||
const columns = useColumns();
|
||||
const deleteIngressesMutation = useDeleteIngresses();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
dataset={ingressesQuery.data || []}
|
||||
storageKey="ingressClassesNameSpace"
|
||||
columns={columns}
|
||||
settingsStore={settings}
|
||||
isLoading={ingressesQuery.isLoading}
|
||||
emptyContentLabel="No supported ingresses found"
|
||||
titleOptions={{
|
||||
icon: 'svg-route',
|
||||
title: 'Ingresses',
|
||||
}}
|
||||
title="Ingresses"
|
||||
titleIcon="svg-route"
|
||||
getRowId={(row) => row.Name + row.Type + row.Namespace}
|
||||
renderTableActions={tableActions}
|
||||
disableSelect={useCheckboxes()}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -67,14 +72,7 @@ export function IngressDataTable() {
|
|||
className="btn-wrapper"
|
||||
color="dangerlight"
|
||||
disabled={selectedFlatRows.length === 0}
|
||||
onClick={() =>
|
||||
handleRemoveClick(
|
||||
selectedFlatRows.map((row) => ({
|
||||
Name: row.Name,
|
||||
Namespace: row.Namespace,
|
||||
}))
|
||||
)
|
||||
}
|
||||
onClick={() => handleRemoveClick(selectedFlatRows)}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove
|
|
@ -1,26 +0,0 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
import { TableSettings } from '../types';
|
||||
|
||||
export const TRUNCATE_LENGTH = 32;
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set),
|
||||
...paginationSettings(set),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { IngressDataTable } from './IngressDataTable';
|
||||
import { IngressDatatable } from './IngressDatatable';
|
||||
|
||||
export function IngressesDatatableView() {
|
||||
return (
|
||||
|
@ -14,7 +14,7 @@ export function IngressesDatatableView() {
|
|||
]}
|
||||
reload
|
||||
/>
|
||||
<IngressDataTable />
|
||||
<IngressDatatable />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,28 +1,10 @@
|
|||
import { Fragment, useEffect } from 'react';
|
||||
import {
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { NomadEvent } from '@/react/nomad/types';
|
||||
import { useDebouncedValue } from '@/react/hooks/useDebouncedValue';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableTitle,
|
||||
} from '@@/datatables';
|
||||
import { multiple } from '@@/datatables/filter-types';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { TableContent } from '@@/datatables/TableContent';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
|
||||
import { useColumns } from './columns';
|
||||
|
||||
|
@ -31,133 +13,31 @@ export interface EventsDatatableProps {
|
|||
isLoading: boolean;
|
||||
}
|
||||
|
||||
export interface EventsTableSettings {
|
||||
autoRefreshRate: number;
|
||||
pageSize: number;
|
||||
sortBy: { id: string; desc: boolean };
|
||||
}
|
||||
const storageKey = 'events';
|
||||
|
||||
const settingsStore = createPersistedStore(storageKey, 'Date');
|
||||
|
||||
export function EventsDatatable({ data, isLoading }: EventsDatatableProps) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<EventsTableSettings>();
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState('events');
|
||||
const columns = useColumns();
|
||||
const debouncedSearchValue = useDebouncedValue(searchBarValue);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<NomadEvent>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
},
|
||||
},
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalFilter(debouncedSearchValue);
|
||||
}, [debouncedSearchValue, setGlobalFilter]);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle icon="fa-history" label="Events" />
|
||||
|
||||
<SearchBar value={searchBarValue} onChange={handleSearchBarChange} />
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<TableHeaderRow<NomadEvent>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<TableContent
|
||||
rows={page}
|
||||
prepareRow={prepareRow}
|
||||
isLoading={isLoading}
|
||||
emptyContent="No events found"
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<Fragment key={key}>
|
||||
<TableRow<NomadEvent>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
</Fragment>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<TableFooter>
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={data.length}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
<Datatable
|
||||
isLoading={isLoading}
|
||||
columns={columns}
|
||||
dataset={data}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
titleIcon="fa-history"
|
||||
title="Events"
|
||||
totalCount={data.length}
|
||||
getRowId={(row) => `${row.Date}-${row.Message}-${row.Type}`}
|
||||
disableSelect
|
||||
/>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
setSearchBarValue(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setTableSettings((settings) => ({
|
||||
...settings,
|
||||
sortBy: { id, desc },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||
|
||||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
import { NomadEventsList } from '@/react/nomad/types';
|
||||
|
||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
|
||||
import { EventsDatatable } from './EventsDatatable';
|
||||
|
@ -27,14 +25,8 @@ export function EventsView() {
|
|||
{ label: 'Events' },
|
||||
];
|
||||
|
||||
const defaultSettings = {
|
||||
pageSize: 10,
|
||||
sortBy: {},
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* header */}
|
||||
<PageHeader
|
||||
title="Event list"
|
||||
breadcrumbs={breadcrumbs}
|
||||
|
@ -43,20 +35,7 @@ export function EventsView() {
|
|||
onReload={invalidateQuery}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableSettingsProvider
|
||||
defaults={defaultSettings}
|
||||
storageKey="nomad-events"
|
||||
>
|
||||
{/* events table */}
|
||||
<EventsDatatable
|
||||
data={(query.data || []) as NomadEventsList}
|
||||
isLoading={query.isLoading}
|
||||
/>
|
||||
</TableSettingsProvider>
|
||||
</div>
|
||||
</div>
|
||||
<EventsDatatable data={query.data || []} isLoading={query.isLoading} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,40 +1,16 @@
|
|||
import { Fragment, useEffect } from 'react';
|
||||
import {
|
||||
useExpanded,
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import { useStore } from 'zustand';
|
||||
import { Clock } from 'react-feather';
|
||||
|
||||
import { Job } from '@/react/nomad/types';
|
||||
import { useDebouncedValue } from '@/react/hooks/useDebouncedValue';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import {
|
||||
Table,
|
||||
TableActions,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableTitle,
|
||||
TableSettingsMenu,
|
||||
TableTitleActions,
|
||||
} from '@@/datatables';
|
||||
import { multiple } from '@@/datatables/filter-types';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { useRowSelect } from '@@/datatables/useRowSelect';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import { TableContent } from '@@/datatables/TableContent';
|
||||
import { useRepeater } from '@@/datatables/useRepeater';
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
import { TableSettingsMenu } from '@@/datatables';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { JobsTableSettings } from './types';
|
||||
import { TasksDatatable } from './TasksDatatable';
|
||||
import { useColumns } from './columns';
|
||||
import { columns } from './columns';
|
||||
import { createStore } from './datatable-store';
|
||||
import { JobsDatatableSettings } from './JobsDatatableSettings';
|
||||
|
||||
export interface JobsDatatableProps {
|
||||
|
@ -43,162 +19,39 @@ export interface JobsDatatableProps {
|
|||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const storageKey = 'jobs';
|
||||
const settingsStore = createStore(storageKey);
|
||||
|
||||
export function JobsDatatable({
|
||||
jobs,
|
||||
refreshData,
|
||||
isLoading,
|
||||
}: JobsDatatableProps) {
|
||||
const { settings, setTableSettings } = useTableSettings<JobsTableSettings>();
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState('jobs');
|
||||
const columns = useColumns();
|
||||
const debouncedSearchValue = useDebouncedValue(searchBarValue);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const settings = useStore(settingsStore);
|
||||
useRepeater(settings.autoRefreshRate, refreshData);
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Job>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: jobs,
|
||||
filterTypes: { multiple },
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
},
|
||||
isRowSelectable() {
|
||||
return false;
|
||||
},
|
||||
autoResetExpanded: false,
|
||||
autoResetSelectedRows: false,
|
||||
selectColumnWidth: 5,
|
||||
getRowId(job, relativeIndex) {
|
||||
return `${job.ID}-${relativeIndex}`;
|
||||
},
|
||||
},
|
||||
useFilters,
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
useExpanded,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setGlobalFilter(debouncedSearchValue);
|
||||
}, [debouncedSearchValue, setGlobalFilter]);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle icon="fa-cubes" label="Nomad Jobs">
|
||||
<TableTitleActions>
|
||||
<TableSettingsMenu>
|
||||
<JobsDatatableSettings />
|
||||
</TableSettingsMenu>
|
||||
</TableTitleActions>
|
||||
</TableTitle>
|
||||
|
||||
<TableActions />
|
||||
|
||||
<SearchBar value={searchBarValue} onChange={handleSearchBarChange} />
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<TableHeaderRow<Job>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<TableContent
|
||||
rows={page}
|
||||
prepareRow={prepareRow}
|
||||
isLoading={isLoading}
|
||||
emptyContent="No jobs found"
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<Fragment key={key}>
|
||||
<TableRow<Job>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
|
||||
{row.isExpanded && (
|
||||
<tr>
|
||||
<td />
|
||||
<td colSpan={row.cells.length - 1}>
|
||||
<TasksDatatable data={row.original.Tasks} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</Fragment>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={jobs.length}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
<ExpandableDatatable<Job>
|
||||
dataset={jobs}
|
||||
columns={columns}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
title="Nomad Jobs"
|
||||
titleIcon={Clock}
|
||||
disableSelect
|
||||
emptyContentLabel="No jobs found"
|
||||
renderSubRow={(row) => <TasksDatatable data={row.original.Tasks} />}
|
||||
isLoading={isLoading}
|
||||
renderTableSettings={() => (
|
||||
<TableSettingsMenu>
|
||||
<JobsDatatableSettings settings={settings} />
|
||||
</TableSettingsMenu>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
setSearchBarValue(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setTableSettings((settings) => ({
|
||||
...settings,
|
||||
sortBy: { id, desc },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
|
||||
import { JobsTableSettings } from './types';
|
||||
import { TableSettings } from './types';
|
||||
|
||||
export function JobsDatatableSettings() {
|
||||
const { settings, setTableSettings } = useTableSettings<JobsTableSettings>();
|
||||
interface Props {
|
||||
settings: TableSettings;
|
||||
}
|
||||
|
||||
export function JobsDatatableSettings({ settings }: Props) {
|
||||
return (
|
||||
<TableSettingsMenuAutoRefresh
|
||||
value={settings.autoRefreshRate}
|
||||
|
@ -14,6 +15,6 @@ export function JobsDatatableSettings() {
|
|||
);
|
||||
|
||||
function handleRefreshRateChange(autoRefreshRate: number) {
|
||||
setTableSettings({ autoRefreshRate });
|
||||
settings.setAutoRefreshRate(autoRefreshRate);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,97 +1,21 @@
|
|||
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { Task } from '@/react/nomad/types';
|
||||
|
||||
import { Table, TableContainer, TableHeaderRow, TableRow } from '@@/datatables';
|
||||
import { InnerDatatable } from '@@/datatables/InnerDatatable';
|
||||
import { NestedDatatable } from '@@/datatables/NestedDatatable';
|
||||
|
||||
import { useColumns } from './columns';
|
||||
|
||||
export interface TasksTableProps {
|
||||
export interface Props {
|
||||
data: Task[];
|
||||
}
|
||||
|
||||
export function TasksDatatable({ data }: TasksTableProps) {
|
||||
export function TasksDatatable({ data }: Props) {
|
||||
const columns = useColumns();
|
||||
const [sortBy, setSortBy] = useState({ id: 'taskName', desc: false });
|
||||
|
||||
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
|
||||
useTable<Task>(
|
||||
{
|
||||
columns,
|
||||
data,
|
||||
initialState: {
|
||||
sortBy: [sortBy],
|
||||
},
|
||||
},
|
||||
useFilters,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
|
||||
return (
|
||||
<InnerDatatable>
|
||||
<TableContainer>
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<TableHeaderRow<Task>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
{data.length > 0 ? (
|
||||
page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, className, role, style } = row.getRowProps();
|
||||
|
||||
return (
|
||||
<TableRow<Task>
|
||||
key={key}
|
||||
cells={row.cells}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center text-muted">
|
||||
no tasks
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</InnerDatatable>
|
||||
<NestedDatatable
|
||||
columns={columns}
|
||||
dataset={data}
|
||||
defaultSortBy="taskName"
|
||||
/>
|
||||
);
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setSortBy({ id, desc });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import { CellProps, Column } from 'react-table';
|
|||
import { Task } from '@/react/nomad/types';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
export const actions: Column<Task> = {
|
||||
Header: 'Task Actions',
|
||||
|
@ -25,7 +26,7 @@ export function ActionsCell({ row }: CellProps<Task>) {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="text-center vertical-center">
|
||||
{/* events */}
|
||||
<Link
|
||||
to="nomad.events"
|
||||
|
@ -33,12 +34,12 @@ export function ActionsCell({ row }: CellProps<Task>) {
|
|||
title="Events"
|
||||
className="space-right"
|
||||
>
|
||||
<i className="fa fa-history space-right" aria-hidden="true" />
|
||||
<Icon icon="clock" feather className="space-right icon" />
|
||||
</Link>
|
||||
|
||||
{/* logs */}
|
||||
<Link to="nomad.logs" params={params} title="Logs">
|
||||
<i className="fa fa-file-alt space-right" aria-hidden="true" />
|
||||
<Icon icon="file-text" feather className="space-right icon" />
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import * as notifications from '@/portainer/services/notifications';
|
||||
import { EnvironmentId } from '@/react/portainer/environments/types';
|
||||
import { Job } from '@/react/nomad/types';
|
||||
|
||||
import { deleteJob } from '../../../jobs.service';
|
||||
import { deleteJob } from '@/react/nomad/jobs/jobs.service';
|
||||
|
||||
export async function deleteJobs(environmentID: EnvironmentId, jobs: Job[]) {
|
||||
return Promise.all(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { Clock } from 'react-feather';
|
||||
|
||||
import { Job } from '@/react/nomad/types';
|
||||
|
||||
|
@ -18,7 +19,7 @@ export function ActionsCell({ row }: CellProps<Job>) {
|
|||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<div className="text-center" {...row.getToggleRowExpandedProps()}>
|
||||
<i className="fa fa-history space-right" aria-hidden="true" />
|
||||
<Clock className="feather" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { useMemo } from 'react';
|
||||
|
||||
import { name } from './name';
|
||||
import { status } from './status';
|
||||
import { created } from './created';
|
||||
import { actions } from './actions';
|
||||
import { namespace } from './namespace';
|
||||
|
||||
export function useColumns() {
|
||||
return useMemo(() => [name, status, namespace, actions, created], []);
|
||||
}
|
||||
export const columns = [name, status, namespace, actions, created];
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { refreshableSettings, createPersistedStore } from '@@/datatables/types';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return createPersistedStore<TableSettings>(
|
||||
storageKey,
|
||||
'SubmitTime',
|
||||
(set) => ({
|
||||
...refreshableSettings(set),
|
||||
})
|
||||
);
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
export interface JobsTableSettings {
|
||||
autoRefreshRate: number;
|
||||
pageSize: number;
|
||||
sortBy: { id: string; desc: boolean };
|
||||
}
|
||||
import {
|
||||
BasicTableSettings,
|
||||
RefreshableTableSettings,
|
||||
} from '@@/datatables/types';
|
||||
|
||||
export interface TableSettings
|
||||
extends BasicTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
|
||||
import { useJobs } from './useJobs';
|
||||
import { JobsDatatable } from './JobsDatatable';
|
||||
|
@ -10,12 +9,6 @@ export function JobsView() {
|
|||
const environmentId = useEnvironmentId();
|
||||
const jobsQuery = useJobs(environmentId);
|
||||
|
||||
const defaultSettings = {
|
||||
autoRefreshRate: 10,
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'name', desc: false },
|
||||
};
|
||||
|
||||
async function reloadData() {
|
||||
await jobsQuery.refetch();
|
||||
}
|
||||
|
@ -30,17 +23,11 @@ export function JobsView() {
|
|||
onReload={reloadData}
|
||||
/>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableSettingsProvider defaults={defaultSettings} storageKey="jobs">
|
||||
<JobsDatatable
|
||||
jobs={jobsQuery.data || []}
|
||||
refreshData={reloadData}
|
||||
isLoading={jobsQuery.isLoading}
|
||||
/>
|
||||
</TableSettingsProvider>
|
||||
</div>
|
||||
</div>
|
||||
<JobsDatatable
|
||||
jobs={jobsQuery.data || []}
|
||||
refreshData={reloadData}
|
||||
isLoading={jobsQuery.isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -175,159 +175,156 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
|||
return (
|
||||
<>
|
||||
{totalAvailable === 0 && <NoEnvironmentsInfoPanel isAdmin={isAdmin} />}
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableContainer>
|
||||
<TableTitle icon="hard-drive" featherIcon label="Environments" />
|
||||
|
||||
<TableActions className={styles.actionBar}>
|
||||
<div className={styles.description}>
|
||||
Click on an environment to manage
|
||||
</div>
|
||||
<div className={styles.actionButton}>
|
||||
<div className={styles.refreshButton}>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
data-cy="home-refreshEndpointsButton"
|
||||
size="medium"
|
||||
color="secondary"
|
||||
className={clsx(
|
||||
'vertical-center !ml-0',
|
||||
styles.refreshEnvironmentsButton
|
||||
)}
|
||||
>
|
||||
<RefreshCcw
|
||||
className="feather icon-sm icon-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Refresh
|
||||
</Button>
|
||||
<TableContainer>
|
||||
<TableTitle icon="hard-drive" featherIcon label="Environments" />
|
||||
|
||||
<TableActions className={styles.actionBar}>
|
||||
<div className={styles.description}>
|
||||
Click on an environment to manage
|
||||
</div>
|
||||
<div className={styles.actionButton}>
|
||||
<div className={styles.refreshButton}>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
data-cy="home-refreshEndpointsButton"
|
||||
size="medium"
|
||||
color="secondary"
|
||||
className={clsx(
|
||||
'vertical-center !ml-0',
|
||||
styles.refreshEnvironmentsButton
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.kubeconfigButton}>
|
||||
<KubeconfigButton
|
||||
environments={environments}
|
||||
envQueryParams={{
|
||||
...environmentsQueryParams,
|
||||
sort: sortByFilter,
|
||||
order: sortByDescending ? 'desc' : 'asc',
|
||||
}}
|
||||
>
|
||||
<RefreshCcw
|
||||
className="feather icon-sm icon-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(styles.filterSearchbar, 'ml-3')}>
|
||||
<FilterSearchBar
|
||||
value={searchBarValue}
|
||||
onChange={setSearchBarValue}
|
||||
placeholder="Search by name, group, tag, status, URL..."
|
||||
data-cy="home-endpointsSearchInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TableActions>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={platformTypeOptions}
|
||||
onChange={setPlatformTypes}
|
||||
placeHolder="Platform"
|
||||
value={platformTypes}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={connectionTypeOptions}
|
||||
onChange={setConnectionTypes}
|
||||
placeHolder="Connection Type"
|
||||
value={connectionTypes}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={status}
|
||||
onChange={statusOnChange}
|
||||
placeHolder="Status"
|
||||
value={statusState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={uniqueTag}
|
||||
onChange={tagOnChange}
|
||||
placeHolder="Tags"
|
||||
value={tagState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={uniqueGroup}
|
||||
onChange={groupOnChange}
|
||||
placeHolder="Groups"
|
||||
value={groupState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter<string>
|
||||
filterOptions={
|
||||
agentVersionsQuery.data?.map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
})) || []
|
||||
}
|
||||
onChange={setAgentVersions}
|
||||
placeHolder="Agent Version"
|
||||
value={agentVersions}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clearButton}
|
||||
onClick={clearFilter}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
<div className={styles.filterRight}>
|
||||
<SortbySelector
|
||||
filterOptions={sortByOptions}
|
||||
onChange={sortOnchange}
|
||||
onDescending={sortOnDescending}
|
||||
placeHolder="Sort By"
|
||||
sortByDescending={sortByDescending}
|
||||
sortByButton={sortByButton}
|
||||
value={sortByState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="blocklist" data-cy="home-endpointList">
|
||||
{renderItems(
|
||||
isLoading,
|
||||
totalCount,
|
||||
environments.map((env) => (
|
||||
<EnvironmentItem
|
||||
key={env.Id}
|
||||
environment={env}
|
||||
groupName={
|
||||
groupsQuery.data?.find((g) => g.Id === env.GroupId)?.Name
|
||||
}
|
||||
onClick={onClickItem}
|
||||
/>
|
||||
))
|
||||
Refresh
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TableFooter>
|
||||
<PaginationControls
|
||||
showAll={totalCount <= 100}
|
||||
pageLimit={pageLimit}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={setPageLimit}
|
||||
<div className={styles.kubeconfigButton}>
|
||||
<KubeconfigButton
|
||||
environments={environments}
|
||||
envQueryParams={{
|
||||
...environmentsQueryParams,
|
||||
sort: sortByFilter,
|
||||
order: sortByDescending ? 'desc' : 'asc',
|
||||
}}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<div className={clsx(styles.filterSearchbar, 'ml-3')}>
|
||||
<FilterSearchBar
|
||||
value={searchBarValue}
|
||||
onChange={setSearchBarValue}
|
||||
placeholder="Search by name, group, tag, status, URL..."
|
||||
data-cy="home-endpointsSearchInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TableActions>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={platformTypeOptions}
|
||||
onChange={setPlatformTypes}
|
||||
placeHolder="Platform"
|
||||
value={platformTypes}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={connectionTypeOptions}
|
||||
onChange={setConnectionTypes}
|
||||
placeHolder="Connection Type"
|
||||
value={connectionTypes}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={status}
|
||||
onChange={statusOnChange}
|
||||
placeHolder="Status"
|
||||
value={statusState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={uniqueTag}
|
||||
onChange={tagOnChange}
|
||||
placeHolder="Tags"
|
||||
value={tagState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={uniqueGroup}
|
||||
onChange={groupOnChange}
|
||||
placeHolder="Groups"
|
||||
value={groupState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter<string>
|
||||
filterOptions={
|
||||
agentVersionsQuery.data?.map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
})) || []
|
||||
}
|
||||
onChange={setAgentVersions}
|
||||
placeHolder="Agent Version"
|
||||
value={agentVersions}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clearButton}
|
||||
onClick={clearFilter}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
<div className={styles.filterRight}>
|
||||
<SortbySelector
|
||||
filterOptions={sortByOptions}
|
||||
onChange={sortOnchange}
|
||||
onDescending={sortOnDescending}
|
||||
placeHolder="Sort By"
|
||||
sortByDescending={sortByDescending}
|
||||
sortByButton={sortByButton}
|
||||
value={sortByState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="blocklist" data-cy="home-endpointList">
|
||||
{renderItems(
|
||||
isLoading,
|
||||
totalCount,
|
||||
environments.map((env) => (
|
||||
<EnvironmentItem
|
||||
key={env.Id}
|
||||
environment={env}
|
||||
groupName={
|
||||
groupsQuery.data?.find((g) => g.Id === env.GroupId)?.Name
|
||||
}
|
||||
onClick={onClickItem}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TableFooter>
|
||||
<PaginationControls
|
||||
showAll={totalCount <= 100}
|
||||
pageLimit={pageLimit}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={setPageLimit}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -46,39 +46,35 @@ export function AccessControlPanel({
|
|||
(!isAdmin && !isPartOfRestrictedUsers && !isLeaderOfAnyRestrictedTeams);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableContainer>
|
||||
<TableTitle label="Access control" icon="eye" featherIcon />
|
||||
<AccessControlPanelDetails
|
||||
resourceType={resourceType}
|
||||
resourceControl={resourceControl}
|
||||
/>
|
||||
<TableContainer>
|
||||
<TableTitle label="Access control" icon="eye" featherIcon />
|
||||
<AccessControlPanelDetails
|
||||
resourceType={resourceType}
|
||||
resourceControl={resourceControl}
|
||||
/>
|
||||
|
||||
{!isEditDisabled && !isEditMode && (
|
||||
<div className="row">
|
||||
<div>
|
||||
<Button color="link" onClick={toggleEditMode}>
|
||||
<Icon icon="edit" className="space-right" feather />
|
||||
Change ownership
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isEditDisabled && !isEditMode && (
|
||||
<div className="row">
|
||||
<div>
|
||||
<Button color="link" onClick={toggleEditMode}>
|
||||
<Icon icon="edit" className="space-right" feather />
|
||||
Change ownership
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditMode && (
|
||||
<AccessControlPanelForm
|
||||
resourceControl={resourceControl}
|
||||
onCancelClick={() => toggleEditMode()}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
environmentId={environmentId}
|
||||
onUpdateSuccess={handleUpdateSuccess}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
{isEditMode && (
|
||||
<AccessControlPanelForm
|
||||
resourceControl={resourceControl}
|
||||
onCancelClick={() => toggleEditMode()}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
environmentId={environmentId}
|
||||
onUpdateSuccess={handleUpdateSuccess}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
async function handleUpdateSuccess() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Clock, Trash2 } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import {
|
||||
FeatureFlag,
|
||||
|
@ -11,6 +12,7 @@ import { Datatable } from '@@/datatables';
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { useList } from '../queries/list';
|
||||
import { EdgeUpdateSchedule } from '../types';
|
||||
|
@ -20,12 +22,15 @@ import { columns } from './columns';
|
|||
import { createStore } from './datatable-store';
|
||||
|
||||
const storageKey = 'update-schedules-list';
|
||||
const useStore = createStore(storageKey);
|
||||
const settingsStore = createStore(storageKey);
|
||||
|
||||
export function ListView() {
|
||||
useRedirectFeatureFlag(FeatureFlag.EdgeRemoteUpdate);
|
||||
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const listQuery = useList();
|
||||
const store = useStore();
|
||||
|
||||
if (!listQuery.data) {
|
||||
return null;
|
||||
|
@ -40,20 +45,22 @@ export function ListView() {
|
|||
/>
|
||||
|
||||
<Datatable
|
||||
columns={columns}
|
||||
titleOptions={{
|
||||
title: 'Update & rollback',
|
||||
icon: Clock,
|
||||
}}
|
||||
dataset={listQuery.data}
|
||||
settingsStore={store}
|
||||
storageKey={storageKey}
|
||||
columns={columns}
|
||||
title="Update & rollback"
|
||||
titleIcon={Clock}
|
||||
emptyContentLabel="No schedules found"
|
||||
isLoading={listQuery.isLoading}
|
||||
totalCount={listQuery.data.length}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<TableActions selectedRows={selectedRows} />
|
||||
)}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,36 +1,25 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
refreshableSettings,
|
||||
hiddenColumnsSettings,
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
createPersistedStore,
|
||||
BasicTableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
extends BasicTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set),
|
||||
...paginationSettings(set),
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
return createPersistedStore<TableSettings>(
|
||||
storageKey,
|
||||
'time',
|
||||
|
||||
(set) => ({
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,17 +11,20 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { notificationsStore } from './notifications-store';
|
||||
import { ToastNotification } from './types';
|
||||
import { columns } from './columns';
|
||||
import { createStore } from './datatable-store';
|
||||
|
||||
const storageKey = 'notifications-list';
|
||||
const useSettingsStore = createStore(storageKey, 'time', true);
|
||||
const settingsStore = createPersistedStore(storageKey, {
|
||||
id: 'time',
|
||||
desc: true,
|
||||
});
|
||||
|
||||
export function NotificationsView() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const { user } = useUser();
|
||||
|
||||
const userNotifications: ToastNotification[] =
|
||||
|
@ -29,9 +32,11 @@ export function NotificationsView() {
|
|||
[];
|
||||
|
||||
const breadcrumbs = 'Notifications';
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const {
|
||||
params: { id },
|
||||
params: { id: activeItemId },
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
return (
|
||||
|
@ -39,19 +44,22 @@ export function NotificationsView() {
|
|||
<PageHeader title="Notifications" breadcrumbs={breadcrumbs} reload />
|
||||
<Datatable
|
||||
columns={columns}
|
||||
titleOptions={{
|
||||
title: 'Notifications',
|
||||
icon: Bell,
|
||||
}}
|
||||
title="Notifications"
|
||||
titleIcon={Bell}
|
||||
dataset={userNotifications}
|
||||
settingsStore={settingsStore}
|
||||
storageKey="notifications"
|
||||
emptyContentLabel="No notifications found"
|
||||
totalCount={userNotifications.length}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<TableActions selectedRows={selectedRows} />
|
||||
)}
|
||||
initialActiveItem={id}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
getRowId={(row) => row.id}
|
||||
highlightedItemId={activeItemId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
refreshableSettings,
|
||||
hiddenColumnsSettings,
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
||||
export function createStore(
|
||||
storageKey: string,
|
||||
initialSortBy?: string,
|
||||
desc?: boolean
|
||||
) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set, initialSortBy, desc),
|
||||
...paginationSettings(set),
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,33 +1,17 @@
|
|||
import { useTable, usePagination, useSortBy } from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import { List } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { useRowSelect } from '@@/datatables/useRowSelect';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableTitle,
|
||||
} from '@@/datatables';
|
||||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types-old';
|
||||
|
||||
import { useFDOProfiles } from './useFDOProfiles';
|
||||
import { useColumns } from './columns';
|
||||
import { FDOProfilesDatatableActions } from './FDOProfilesDatatableActions';
|
||||
import { useFDOProfiles } from './useFDOProfiles';
|
||||
|
||||
export interface FDOProfilesTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
||||
const storageKey = 'fdoProfiles';
|
||||
|
||||
const settingsStore = createPersistedStore(storageKey, 'name');
|
||||
|
||||
export interface FDOProfilesDatatableProps {
|
||||
isFDOEnabled: boolean;
|
||||
|
@ -36,132 +20,33 @@ export interface FDOProfilesDatatableProps {
|
|||
export function FDOProfilesDatatable({
|
||||
isFDOEnabled,
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<FDOProfilesTableSettings>();
|
||||
const columns = useColumns();
|
||||
|
||||
const { isLoading, profiles, error } = useFDOProfiles();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Profile>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: profiles,
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
},
|
||||
isRowSelectable() {
|
||||
return isFDOEnabled;
|
||||
},
|
||||
selectColumnWidth: 5,
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const { isLoading, profiles } = useFDOProfiles();
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle icon="list" featherIcon label="Device Profiles">
|
||||
<Datatable
|
||||
columns={columns}
|
||||
dataset={profiles}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
title="Device Profiles"
|
||||
titleIcon={List}
|
||||
disableSelect={!isFDOEnabled}
|
||||
emptyContentLabel="No profiles found"
|
||||
getRowId={(row) => row.id.toString()}
|
||||
isLoading={isLoading}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<FDOProfilesDatatableActions
|
||||
isFDOEnabled={isFDOEnabled}
|
||||
selectedItems={selectedFlatRows.map((row) => row.original)}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
</TableTitle>
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<TableHeaderRow<Profile>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
{!isLoading && profiles && profiles.length > 0 ? (
|
||||
page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, className, role, style } = row.getRowProps();
|
||||
|
||||
return (
|
||||
<TableRow<Profile>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center text-muted">
|
||||
{userMessage(isLoading, error)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={profiles ? profiles.length : 0}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
}
|
||||
|
||||
function userMessage(isLoading: boolean, error?: PortainerError) {
|
||||
if (isLoading) {
|
||||
return 'Loading...';
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return 'No profiles found';
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
|
||||
import {
|
||||
FDOProfilesDatatable,
|
||||
FDOProfilesDatatableProps,
|
||||
} from './FDOProfilesDatatable';
|
||||
|
||||
export function FDOProfilesDatatableContainer({
|
||||
...props
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const defaultSettings = {
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'name', desc: false },
|
||||
};
|
||||
|
||||
return (
|
||||
<TableSettingsProvider defaults={defaultSettings} storageKey="fdoProfiles">
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FDOProfilesDatatable {...props} />
|
||||
</TableSettingsProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { FDOProfilesDatatable } from './FDOProfilesDatatable';
|
|
@ -11,7 +11,7 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
|
|||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
|
||||
import { FDOProfilesDatatableContainer } from '../FDOProfilesDatatable/FDOProfilesDatatableContainer';
|
||||
import { FDOProfilesDatatable } from '../FDOProfilesDatatable';
|
||||
|
||||
import styles from './SettingsFDO.module.css';
|
||||
import { validationSchema } from './SettingsFDO.validation';
|
||||
|
@ -165,7 +165,7 @@ export function SettingsFDO({ settings, onSubmit }: Props) {
|
|||
Add, Edit and Manage the list of device profiles available
|
||||
during FDO device setup
|
||||
</TextTip>
|
||||
<FDOProfilesDatatableContainer isFDOEnabled={initialFDOEnabled} />
|
||||
<FDOProfilesDatatable isFDOEnabled={initialFDOEnabled} />
|
||||
</div>
|
||||
)}
|
||||
</WidgetBody>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { PageHeader } from '@@/PageHeader';
|
|||
import { useTeams } from '../queries';
|
||||
|
||||
import { CreateTeamForm } from './CreateTeamForm';
|
||||
import { TeamsDatatableContainer } from './TeamsDatatable/TeamsDatatable';
|
||||
import { TeamsDatatable } from './TeamsDatatable';
|
||||
|
||||
export function ListView() {
|
||||
const { isAdmin } = useUser();
|
||||
|
@ -23,7 +23,7 @@ export function ListView() {
|
|||
)}
|
||||
|
||||
{teamsQuery.data && (
|
||||
<TeamsDatatableContainer teams={teamsQuery.data} isAdmin={isAdmin} />
|
||||
<TeamsDatatable teams={teamsQuery.data} isAdmin={isAdmin} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import {
|
||||
Column,
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { Column } from 'react-table';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { Trash2, Users } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
|
@ -16,23 +9,13 @@ import { Team, TeamId } from '@/react/portainer/users/teams/types';
|
|||
import { deleteTeam } from '@/react/portainer/users/teams/teams.service';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
import { Table } from '@@/datatables';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import {
|
||||
TableSettingsProvider,
|
||||
useTableSettings,
|
||||
} from '@@/datatables/useTableSettings';
|
||||
import { TableContent } from '@@/datatables/TableContent';
|
||||
import { buildNameColumn } from '@@/datatables/NameCell';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
|
||||
const tableKey = 'teams';
|
||||
const storageKey = 'teams';
|
||||
|
||||
const columns: readonly Column<Team>[] = [
|
||||
buildNameColumn('Name', 'Id', 'portainer.teams.team'),
|
||||
|
@ -43,168 +26,47 @@ interface Props {
|
|||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const settingsStore = createPersistedStore(storageKey);
|
||||
|
||||
export function TeamsDatatable({ teams, isAdmin }: Props) {
|
||||
const { handleRemove } = useRemoveMutation();
|
||||
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(tableKey);
|
||||
const { settings, setTableSettings } = useTableSettings<TableSettings>();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Team>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: teams,
|
||||
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
},
|
||||
selectCheckboxComponent: Checkbox,
|
||||
},
|
||||
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
isAdmin ? useRowSelectColumn : emptyPlugin
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Table.Container>
|
||||
<Table.Title icon={Users} label="Teams">
|
||||
<SearchBar
|
||||
value={searchBarValue}
|
||||
onChange={handleSearchBarChange}
|
||||
/>
|
||||
|
||||
{isAdmin && (
|
||||
<Table.Actions>
|
||||
<Button
|
||||
color="dangerlight"
|
||||
onClick={handleRemoveClick}
|
||||
disabled={selectedFlatRows.length === 0}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Table.Actions>
|
||||
)}
|
||||
</Table.Title>
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
<Datatable
|
||||
dataset={teams}
|
||||
columns={columns}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
title="Teams"
|
||||
titleIcon={Users}
|
||||
renderTableActions={(selectedRows) =>
|
||||
isAdmin && (
|
||||
<Button
|
||||
color="dangerlight"
|
||||
onClick={() => handleRemoveClick(selectedRows)}
|
||||
disabled={selectedRows.length === 0}
|
||||
icon={Trash2}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<Table.HeaderRow<Team>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<TableContent
|
||||
prepareRow={prepareRow}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<Table.Row<Team>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
rows={page}
|
||||
emptyContent="No teams found"
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={teams.length}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</Table.Container>
|
||||
</div>
|
||||
</div>
|
||||
Remove
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
emptyContentLabel="No teams found"
|
||||
/>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings({ pageSize });
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
setSearchBarValue(value);
|
||||
setGlobalFilter(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setTableSettings({ sortBy: { id, desc } });
|
||||
}
|
||||
|
||||
function handleRemoveClick() {
|
||||
const ids = selectedFlatRows.map((row) => row.original.Id);
|
||||
function handleRemoveClick(selectedRows: Team[]) {
|
||||
const ids = selectedRows.map((row) => row.Id);
|
||||
handleRemove(ids);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSettings: TableSettings = {
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'name', desc: false },
|
||||
};
|
||||
|
||||
export function TeamsDatatableContainer(props: Props) {
|
||||
return (
|
||||
<TableSettingsProvider<TableSettings>
|
||||
defaults={defaultSettings}
|
||||
storageKey={tableKey}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<TeamsDatatable {...props} />
|
||||
</TableSettingsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function useRemoveMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
@ -239,6 +101,3 @@ function useRemoveMutation() {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
function emptyPlugin() {}
|
||||
emptyPlugin.pluginName = 'emptyPlugin';
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types-old';
|
||||
|
||||
export interface TableSettings
|
||||
extends PaginationTableSettings,
|
||||
SortableTableSettings {}
|
|
@ -30,6 +30,8 @@ const licenseInfo: LicenseInfo = {
|
|||
expiresAt: Number.MAX_SAFE_INTEGER,
|
||||
productEdition: Edition.EE,
|
||||
valid: true,
|
||||
enforcedAt: 0,
|
||||
enforced: false,
|
||||
};
|
||||
|
||||
export const handlers = [
|
||||
|
|
|
@ -143,7 +143,7 @@
|
|||
"xterm": "^3.8.0",
|
||||
"yaml": "^1.10.2",
|
||||
"yup": "^0.32.11",
|
||||
"zustand": "^4.0.0"
|
||||
"zustand": "^4.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-cli": "^4.0.4",
|
||||
|
|
|
@ -19129,10 +19129,10 @@ z-schema@^5.0.1:
|
|||
optionalDependencies:
|
||||
commander "^2.7.1"
|
||||
|
||||
zustand@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.0.0.tgz#739cba69209ffe67b31e7d6741c25b51496114a7"
|
||||
integrity sha512-OrsfQTnRXF1LZ9/vR/IqN9ws5EXUhb149xmPjErZnUrkgxS/gAHGy2dPNIVkVvoxrVe1sIydn4JjF0dYHmGeeQ==
|
||||
zustand@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.1.1.tgz#5a61cc755a002df5f041840a414ae6e9a99ee22b"
|
||||
integrity sha512-h4F3WMqsZgvvaE0n3lThx4MM81Ls9xebjvrABNzf5+jb3/03YjNTSgZXeyrvXDArMeV9untvWXRw1tY+ntPYbA==
|
||||
dependencies:
|
||||
use-sync-external-store "1.2.0"
|
||||
|
||||
|
|
Loading…
Reference in New Issue