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