mirror of
https://github.com/portainer/portainer.git
synced 2025-08-02 20:35:25 +02:00
refactor(ui/datatables): migrate views to use datatable component [EE-4064] (#7609)
This commit is contained in:
parent
0f0513c684
commit
fe8e834dbf
90 changed files with 1714 additions and 2717 deletions
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue