1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-03 04:45:21 +02:00

refactor(ui/datatables): migrate views to use datatable component [EE-4064] (#7609)

This commit is contained in:
Chaim Lev-Ari 2022-11-22 14:16:34 +02:00 committed by GitHub
parent 0f0513c684
commit fe8e834dbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 1714 additions and 2717 deletions

View file

@ -1,28 +1,10 @@
import { Fragment, useEffect } from 'react';
import {
useFilters,
useGlobalFilter,
usePagination,
useSortBy,
useTable,
} from 'react-table';
import { useStore } from 'zustand';
import { NomadEvent } from '@/react/nomad/types';
import { useDebouncedValue } from '@/react/hooks/useDebouncedValue';
import { PaginationControls } from '@@/PaginationControls';
import {
Table,
TableContainer,
TableHeaderRow,
TableRow,
TableTitle,
} from '@@/datatables';
import { multiple } from '@@/datatables/filter-types';
import { useTableSettings } from '@@/datatables/useTableSettings';
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
import { TableFooter } from '@@/datatables/TableFooter';
import { TableContent } from '@@/datatables/TableContent';
import { Datatable } from '@@/datatables';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { createPersistedStore } from '@@/datatables/types';
import { useColumns } from './columns';
@ -31,133 +13,31 @@ export interface EventsDatatableProps {
isLoading: boolean;
}
export interface EventsTableSettings {
autoRefreshRate: number;
pageSize: number;
sortBy: { id: string; desc: boolean };
}
const storageKey = 'events';
const settingsStore = createPersistedStore(storageKey, 'Date');
export function EventsDatatable({ data, isLoading }: EventsDatatableProps) {
const { settings, setTableSettings } =
useTableSettings<EventsTableSettings>();
const [searchBarValue, setSearchBarValue] = useSearchBarState('events');
const columns = useColumns();
const debouncedSearchValue = useDebouncedValue(searchBarValue);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
gotoPage,
setPageSize,
setGlobalFilter,
state: { pageIndex, pageSize },
} = useTable<NomadEvent>(
{
defaultCanFilter: false,
columns,
data,
filterTypes: { multiple },
initialState: {
pageSize: settings.pageSize || 10,
sortBy: [settings.sortBy],
globalFilter: searchBarValue,
},
},
useFilters,
useGlobalFilter,
useSortBy,
usePagination
);
useEffect(() => {
setGlobalFilter(debouncedSearchValue);
}, [debouncedSearchValue, setGlobalFilter]);
const tableProps = getTableProps();
const tbodyProps = getTableBodyProps();
const settings = useStore(settingsStore);
const [search, setSearch] = useSearchBarState(storageKey);
return (
<TableContainer>
<TableTitle icon="fa-history" label="Events" />
<SearchBar value={searchBarValue} onChange={handleSearchBarChange} />
<Table
className={tableProps.className}
role={tableProps.role}
style={tableProps.style}
>
<thead>
{headerGroups.map((headerGroup) => {
const { key, className, role, style } =
headerGroup.getHeaderGroupProps();
return (
<TableHeaderRow<NomadEvent>
key={key}
className={className}
role={role}
style={style}
headers={headerGroup.headers}
onSortChange={handleSortChange}
/>
);
})}
</thead>
<tbody
className={tbodyProps.className}
role={tbodyProps.role}
style={tbodyProps.style}
>
<TableContent
rows={page}
prepareRow={prepareRow}
isLoading={isLoading}
emptyContent="No events found"
renderRow={(row, { key, className, role, style }) => (
<Fragment key={key}>
<TableRow<NomadEvent>
cells={row.cells}
key={key}
className={className}
role={role}
style={style}
/>
</Fragment>
)}
/>
</tbody>
</Table>
<TableFooter>
<PaginationControls
showAll
pageLimit={pageSize}
page={pageIndex + 1}
onPageChange={(p) => gotoPage(p - 1)}
totalCount={data.length}
onPageLimitChange={handlePageSizeChange}
/>
</TableFooter>
</TableContainer>
<Datatable
isLoading={isLoading}
columns={columns}
dataset={data}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
titleIcon="fa-history"
title="Events"
totalCount={data.length}
getRowId={(row) => `${row.Date}-${row.Message}-${row.Type}`}
disableSelect
/>
);
function handlePageSizeChange(pageSize: number) {
setPageSize(pageSize);
setTableSettings((settings) => ({ ...settings, pageSize }));
}
function handleSearchBarChange(value: string) {
setSearchBarValue(value);
}
function handleSortChange(id: string, desc: boolean) {
setTableSettings((settings) => ({
...settings,
sortBy: { id, desc },
}));
}
}

View file

@ -1,9 +1,7 @@
import { useCurrentStateAndParams } from '@uirouter/react';
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { NomadEventsList } from '@/react/nomad/types';
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
import { PageHeader } from '@@/PageHeader';
import { EventsDatatable } from './EventsDatatable';
@ -27,14 +25,8 @@ export function EventsView() {
{ label: 'Events' },
];
const defaultSettings = {
pageSize: 10,
sortBy: {},
};
return (
<>
{/* header */}
<PageHeader
title="Event list"
breadcrumbs={breadcrumbs}
@ -43,20 +35,7 @@ export function EventsView() {
onReload={invalidateQuery}
/>
<div className="row">
<div className="col-sm-12">
<TableSettingsProvider
defaults={defaultSettings}
storageKey="nomad-events"
>
{/* events table */}
<EventsDatatable
data={(query.data || []) as NomadEventsList}
isLoading={query.isLoading}
/>
</TableSettingsProvider>
</div>
</div>
<EventsDatatable data={query.data || []} isLoading={query.isLoading} />
</>
);
}

View file

@ -1,40 +1,16 @@
import { Fragment, useEffect } from 'react';
import {
useExpanded,
useFilters,
useGlobalFilter,
usePagination,
useSortBy,
useTable,
} from 'react-table';
import { useRowSelectColumn } from '@lineup-lite/hooks';
import { useStore } from 'zustand';
import { Clock } from 'react-feather';
import { Job } from '@/react/nomad/types';
import { useDebouncedValue } from '@/react/hooks/useDebouncedValue';
import { PaginationControls } from '@@/PaginationControls';
import {
Table,
TableActions,
TableContainer,
TableHeaderRow,
TableRow,
TableTitle,
TableSettingsMenu,
TableTitleActions,
} from '@@/datatables';
import { multiple } from '@@/datatables/filter-types';
import { useTableSettings } from '@@/datatables/useTableSettings';
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
import { useRowSelect } from '@@/datatables/useRowSelect';
import { TableFooter } from '@@/datatables/TableFooter';
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
import { TableContent } from '@@/datatables/TableContent';
import { useRepeater } from '@@/datatables/useRepeater';
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
import { TableSettingsMenu } from '@@/datatables';
import { useSearchBarState } from '@@/datatables/SearchBar';
import { JobsTableSettings } from './types';
import { TasksDatatable } from './TasksDatatable';
import { useColumns } from './columns';
import { columns } from './columns';
import { createStore } from './datatable-store';
import { JobsDatatableSettings } from './JobsDatatableSettings';
export interface JobsDatatableProps {
@ -43,162 +19,39 @@ export interface JobsDatatableProps {
isLoading?: boolean;
}
const storageKey = 'jobs';
const settingsStore = createStore(storageKey);
export function JobsDatatable({
jobs,
refreshData,
isLoading,
}: JobsDatatableProps) {
const { settings, setTableSettings } = useTableSettings<JobsTableSettings>();
const [searchBarValue, setSearchBarValue] = useSearchBarState('jobs');
const columns = useColumns();
const debouncedSearchValue = useDebouncedValue(searchBarValue);
const [search, setSearch] = useSearchBarState(storageKey);
const settings = useStore(settingsStore);
useRepeater(settings.autoRefreshRate, refreshData);
const {
getTableProps,
getTableBodyProps,
headerGroups,
page,
prepareRow,
selectedFlatRows,
gotoPage,
setPageSize,
setGlobalFilter,
state: { pageIndex, pageSize },
} = useTable<Job>(
{
defaultCanFilter: false,
columns,
data: jobs,
filterTypes: { multiple },
initialState: {
pageSize: settings.pageSize || 10,
sortBy: [settings.sortBy],
globalFilter: searchBarValue,
},
isRowSelectable() {
return false;
},
autoResetExpanded: false,
autoResetSelectedRows: false,
selectColumnWidth: 5,
getRowId(job, relativeIndex) {
return `${job.ID}-${relativeIndex}`;
},
},
useFilters,
useGlobalFilter,
useSortBy,
useExpanded,
usePagination,
useRowSelect,
useRowSelectColumn
);
useEffect(() => {
setGlobalFilter(debouncedSearchValue);
}, [debouncedSearchValue, setGlobalFilter]);
const tableProps = getTableProps();
const tbodyProps = getTableBodyProps();
return (
<TableContainer>
<TableTitle icon="fa-cubes" label="Nomad Jobs">
<TableTitleActions>
<TableSettingsMenu>
<JobsDatatableSettings />
</TableSettingsMenu>
</TableTitleActions>
</TableTitle>
<TableActions />
<SearchBar value={searchBarValue} onChange={handleSearchBarChange} />
<Table
className={tableProps.className}
role={tableProps.role}
style={tableProps.style}
>
<thead>
{headerGroups.map((headerGroup) => {
const { key, className, role, style } =
headerGroup.getHeaderGroupProps();
return (
<TableHeaderRow<Job>
key={key}
className={className}
role={role}
style={style}
headers={headerGroup.headers}
onSortChange={handleSortChange}
/>
);
})}
</thead>
<tbody
className={tbodyProps.className}
role={tbodyProps.role}
style={tbodyProps.style}
>
<TableContent
rows={page}
prepareRow={prepareRow}
isLoading={isLoading}
emptyContent="No jobs found"
renderRow={(row, { key, className, role, style }) => (
<Fragment key={key}>
<TableRow<Job>
cells={row.cells}
key={key}
className={className}
role={role}
style={style}
/>
{row.isExpanded && (
<tr>
<td />
<td colSpan={row.cells.length - 1}>
<TasksDatatable data={row.original.Tasks} />
</td>
</tr>
)}
</Fragment>
)}
/>
</tbody>
</Table>
<TableFooter>
<SelectedRowsCount value={selectedFlatRows.length} />
<PaginationControls
showAll
pageLimit={pageSize}
page={pageIndex + 1}
onPageChange={(p) => gotoPage(p - 1)}
totalCount={jobs.length}
onPageLimitChange={handlePageSizeChange}
/>
</TableFooter>
</TableContainer>
<ExpandableDatatable<Job>
dataset={jobs}
columns={columns}
initialPageSize={settings.pageSize}
onPageSizeChange={settings.setPageSize}
initialSortBy={settings.sortBy}
onSortByChange={settings.setSortBy}
searchValue={search}
onSearchChange={setSearch}
title="Nomad Jobs"
titleIcon={Clock}
disableSelect
emptyContentLabel="No jobs found"
renderSubRow={(row) => <TasksDatatable data={row.original.Tasks} />}
isLoading={isLoading}
renderTableSettings={() => (
<TableSettingsMenu>
<JobsDatatableSettings settings={settings} />
</TableSettingsMenu>
)}
/>
);
function handlePageSizeChange(pageSize: number) {
setPageSize(pageSize);
setTableSettings((settings) => ({ ...settings, pageSize }));
}
function handleSearchBarChange(value: string) {
setSearchBarValue(value);
}
function handleSortChange(id: string, desc: boolean) {
setTableSettings((settings) => ({
...settings,
sortBy: { id, desc },
}));
}
}

View file

@ -1,11 +1,12 @@
import { TableSettingsMenuAutoRefresh } from '@@/datatables/TableSettingsMenuAutoRefresh';
import { useTableSettings } from '@@/datatables/useTableSettings';
import { JobsTableSettings } from './types';
import { TableSettings } from './types';
export function JobsDatatableSettings() {
const { settings, setTableSettings } = useTableSettings<JobsTableSettings>();
interface Props {
settings: TableSettings;
}
export function JobsDatatableSettings({ settings }: Props) {
return (
<TableSettingsMenuAutoRefresh
value={settings.autoRefreshRate}
@ -14,6 +15,6 @@ export function JobsDatatableSettings() {
);
function handleRefreshRateChange(autoRefreshRate: number) {
setTableSettings({ autoRefreshRate });
settings.setAutoRefreshRate(autoRefreshRate);
}
}

View file

@ -1,97 +1,21 @@
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
import { useState } from 'react';
import { Task } from '@/react/nomad/types';
import { Table, TableContainer, TableHeaderRow, TableRow } from '@@/datatables';
import { InnerDatatable } from '@@/datatables/InnerDatatable';
import { NestedDatatable } from '@@/datatables/NestedDatatable';
import { useColumns } from './columns';
export interface TasksTableProps {
export interface Props {
data: Task[];
}
export function TasksDatatable({ data }: TasksTableProps) {
export function TasksDatatable({ data }: Props) {
const columns = useColumns();
const [sortBy, setSortBy] = useState({ id: 'taskName', desc: false });
const { getTableProps, getTableBodyProps, headerGroups, page, prepareRow } =
useTable<Task>(
{
columns,
data,
initialState: {
sortBy: [sortBy],
},
},
useFilters,
useSortBy,
usePagination
);
const tableProps = getTableProps();
const tbodyProps = getTableBodyProps();
return (
<InnerDatatable>
<TableContainer>
<Table
className={tableProps.className}
role={tableProps.role}
style={tableProps.style}
>
<thead>
{headerGroups.map((headerGroup) => {
const { key, className, role, style } =
headerGroup.getHeaderGroupProps();
return (
<TableHeaderRow<Task>
key={key}
className={className}
role={role}
style={style}
headers={headerGroup.headers}
onSortChange={handleSortChange}
/>
);
})}
</thead>
<tbody
className={tbodyProps.className}
role={tbodyProps.role}
style={tbodyProps.style}
>
{data.length > 0 ? (
page.map((row) => {
prepareRow(row);
const { key, className, role, style } = row.getRowProps();
return (
<TableRow<Task>
key={key}
cells={row.cells}
className={className}
role={role}
style={style}
/>
);
})
) : (
<tr>
<td colSpan={5} className="text-center text-muted">
no tasks
</td>
</tr>
)}
</tbody>
</Table>
</TableContainer>
</InnerDatatable>
<NestedDatatable
columns={columns}
dataset={data}
defaultSortBy="taskName"
/>
);
function handleSortChange(id: string, desc: boolean) {
setSortBy({ id, desc });
}
}

View file

@ -3,6 +3,7 @@ import { CellProps, Column } from 'react-table';
import { Task } from '@/react/nomad/types';
import { Link } from '@@/Link';
import { Icon } from '@@/Icon';
export const actions: Column<Task> = {
Header: 'Task Actions',
@ -25,7 +26,7 @@ export function ActionsCell({ row }: CellProps<Task>) {
};
return (
<div className="text-center">
<div className="text-center vertical-center">
{/* events */}
<Link
to="nomad.events"
@ -33,12 +34,12 @@ export function ActionsCell({ row }: CellProps<Task>) {
title="Events"
className="space-right"
>
<i className="fa fa-history space-right" aria-hidden="true" />
<Icon icon="clock" feather className="space-right icon" />
</Link>
{/* logs */}
<Link to="nomad.logs" params={params} title="Logs">
<i className="fa fa-file-alt space-right" aria-hidden="true" />
<Icon icon="file-text" feather className="space-right icon" />
</Link>
</div>
);

View file

@ -1,8 +1,7 @@
import * as notifications from '@/portainer/services/notifications';
import { EnvironmentId } from '@/react/portainer/environments/types';
import { Job } from '@/react/nomad/types';
import { deleteJob } from '../../../jobs.service';
import { deleteJob } from '@/react/nomad/jobs/jobs.service';
export async function deleteJobs(environmentID: EnvironmentId, jobs: Job[]) {
return Promise.all(

View file

@ -1,4 +1,5 @@
import { CellProps, Column } from 'react-table';
import { Clock } from 'react-feather';
import { Job } from '@/react/nomad/types';
@ -18,7 +19,7 @@ export function ActionsCell({ row }: CellProps<Job>) {
return (
// eslint-disable-next-line react/jsx-props-no-spreading
<div className="text-center" {...row.getToggleRowExpandedProps()}>
<i className="fa fa-history space-right" aria-hidden="true" />
<Clock className="feather" />
</div>
);
}

View file

@ -1,11 +1,7 @@
import { useMemo } from 'react';
import { name } from './name';
import { status } from './status';
import { created } from './created';
import { actions } from './actions';
import { namespace } from './namespace';
export function useColumns() {
return useMemo(() => [name, status, namespace, actions, created], []);
}
export const columns = [name, status, namespace, actions, created];

View file

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

View file

@ -1,5 +1,8 @@
export interface JobsTableSettings {
autoRefreshRate: number;
pageSize: number;
sortBy: { id: string; desc: boolean };
}
import {
BasicTableSettings,
RefreshableTableSettings,
} from '@@/datatables/types';
export interface TableSettings
extends BasicTableSettings,
RefreshableTableSettings {}

View file

@ -1,7 +1,6 @@
import { useEnvironmentId } from '@/react/hooks/useEnvironmentId';
import { PageHeader } from '@@/PageHeader';
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
import { useJobs } from './useJobs';
import { JobsDatatable } from './JobsDatatable';
@ -10,12 +9,6 @@ export function JobsView() {
const environmentId = useEnvironmentId();
const jobsQuery = useJobs(environmentId);
const defaultSettings = {
autoRefreshRate: 10,
pageSize: 10,
sortBy: { id: 'name', desc: false },
};
async function reloadData() {
await jobsQuery.refetch();
}
@ -30,17 +23,11 @@ export function JobsView() {
onReload={reloadData}
/>
<div className="row">
<div className="col-sm-12">
<TableSettingsProvider defaults={defaultSettings} storageKey="jobs">
<JobsDatatable
jobs={jobsQuery.data || []}
refreshData={reloadData}
isLoading={jobsQuery.isLoading}
/>
</TableSettingsProvider>
</div>
</div>
<JobsDatatable
jobs={jobsQuery.data || []}
refreshData={reloadData}
isLoading={jobsQuery.isLoading}
/>
</>
);
}