mirror of
https://github.com/portainer/portainer.git
synced 2025-08-09 15:55:23 +02:00
refactor(ui/datatables): migrate views to use datatable component [EE-4064] (#7609)
This commit is contained in:
parent
0f0513c684
commit
fe8e834dbf
90 changed files with 1714 additions and 2717 deletions
|
@ -175,159 +175,156 @@ export function EnvironmentList({ onClickItem, onRefresh }: Props) {
|
|||
return (
|
||||
<>
|
||||
{totalAvailable === 0 && <NoEnvironmentsInfoPanel isAdmin={isAdmin} />}
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableContainer>
|
||||
<TableTitle icon="hard-drive" featherIcon label="Environments" />
|
||||
|
||||
<TableActions className={styles.actionBar}>
|
||||
<div className={styles.description}>
|
||||
Click on an environment to manage
|
||||
</div>
|
||||
<div className={styles.actionButton}>
|
||||
<div className={styles.refreshButton}>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
data-cy="home-refreshEndpointsButton"
|
||||
size="medium"
|
||||
color="secondary"
|
||||
className={clsx(
|
||||
'vertical-center !ml-0',
|
||||
styles.refreshEnvironmentsButton
|
||||
)}
|
||||
>
|
||||
<RefreshCcw
|
||||
className="feather icon-sm icon-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Refresh
|
||||
</Button>
|
||||
<TableContainer>
|
||||
<TableTitle icon="hard-drive" featherIcon label="Environments" />
|
||||
|
||||
<TableActions className={styles.actionBar}>
|
||||
<div className={styles.description}>
|
||||
Click on an environment to manage
|
||||
</div>
|
||||
<div className={styles.actionButton}>
|
||||
<div className={styles.refreshButton}>
|
||||
{isAdmin && (
|
||||
<Button
|
||||
onClick={onRefresh}
|
||||
data-cy="home-refreshEndpointsButton"
|
||||
size="medium"
|
||||
color="secondary"
|
||||
className={clsx(
|
||||
'vertical-center !ml-0',
|
||||
styles.refreshEnvironmentsButton
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.kubeconfigButton}>
|
||||
<KubeconfigButton
|
||||
environments={environments}
|
||||
envQueryParams={{
|
||||
...environmentsQueryParams,
|
||||
sort: sortByFilter,
|
||||
order: sortByDescending ? 'desc' : 'asc',
|
||||
}}
|
||||
>
|
||||
<RefreshCcw
|
||||
className="feather icon-sm icon-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div className={clsx(styles.filterSearchbar, 'ml-3')}>
|
||||
<FilterSearchBar
|
||||
value={searchBarValue}
|
||||
onChange={setSearchBarValue}
|
||||
placeholder="Search by name, group, tag, status, URL..."
|
||||
data-cy="home-endpointsSearchInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TableActions>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={platformTypeOptions}
|
||||
onChange={setPlatformTypes}
|
||||
placeHolder="Platform"
|
||||
value={platformTypes}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={connectionTypeOptions}
|
||||
onChange={setConnectionTypes}
|
||||
placeHolder="Connection Type"
|
||||
value={connectionTypes}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={status}
|
||||
onChange={statusOnChange}
|
||||
placeHolder="Status"
|
||||
value={statusState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={uniqueTag}
|
||||
onChange={tagOnChange}
|
||||
placeHolder="Tags"
|
||||
value={tagState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={uniqueGroup}
|
||||
onChange={groupOnChange}
|
||||
placeHolder="Groups"
|
||||
value={groupState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter<string>
|
||||
filterOptions={
|
||||
agentVersionsQuery.data?.map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
})) || []
|
||||
}
|
||||
onChange={setAgentVersions}
|
||||
placeHolder="Agent Version"
|
||||
value={agentVersions}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clearButton}
|
||||
onClick={clearFilter}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
<div className={styles.filterRight}>
|
||||
<SortbySelector
|
||||
filterOptions={sortByOptions}
|
||||
onChange={sortOnchange}
|
||||
onDescending={sortOnDescending}
|
||||
placeHolder="Sort By"
|
||||
sortByDescending={sortByDescending}
|
||||
sortByButton={sortByButton}
|
||||
value={sortByState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="blocklist" data-cy="home-endpointList">
|
||||
{renderItems(
|
||||
isLoading,
|
||||
totalCount,
|
||||
environments.map((env) => (
|
||||
<EnvironmentItem
|
||||
key={env.Id}
|
||||
environment={env}
|
||||
groupName={
|
||||
groupsQuery.data?.find((g) => g.Id === env.GroupId)?.Name
|
||||
}
|
||||
onClick={onClickItem}
|
||||
/>
|
||||
))
|
||||
Refresh
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TableFooter>
|
||||
<PaginationControls
|
||||
showAll={totalCount <= 100}
|
||||
pageLimit={pageLimit}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={setPageLimit}
|
||||
<div className={styles.kubeconfigButton}>
|
||||
<KubeconfigButton
|
||||
environments={environments}
|
||||
envQueryParams={{
|
||||
...environmentsQueryParams,
|
||||
sort: sortByFilter,
|
||||
order: sortByDescending ? 'desc' : 'asc',
|
||||
}}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<div className={clsx(styles.filterSearchbar, 'ml-3')}>
|
||||
<FilterSearchBar
|
||||
value={searchBarValue}
|
||||
onChange={setSearchBarValue}
|
||||
placeholder="Search by name, group, tag, status, URL..."
|
||||
data-cy="home-endpointsSearchInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</TableActions>
|
||||
<div className={styles.filterContainer}>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={platformTypeOptions}
|
||||
onChange={setPlatformTypes}
|
||||
placeHolder="Platform"
|
||||
value={platformTypes}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={connectionTypeOptions}
|
||||
onChange={setConnectionTypes}
|
||||
placeHolder="Connection Type"
|
||||
value={connectionTypes}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={status}
|
||||
onChange={statusOnChange}
|
||||
placeHolder="Status"
|
||||
value={statusState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={uniqueTag}
|
||||
onChange={tagOnChange}
|
||||
placeHolder="Tags"
|
||||
value={tagState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter
|
||||
filterOptions={uniqueGroup}
|
||||
onChange={groupOnChange}
|
||||
placeHolder="Groups"
|
||||
value={groupState}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.filterLeft}>
|
||||
<HomepageFilter<string>
|
||||
filterOptions={
|
||||
agentVersionsQuery.data?.map((v) => ({
|
||||
label: v,
|
||||
value: v,
|
||||
})) || []
|
||||
}
|
||||
onChange={setAgentVersions}
|
||||
placeHolder="Agent Version"
|
||||
value={agentVersions}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.clearButton}
|
||||
onClick={clearFilter}
|
||||
>
|
||||
Clear all
|
||||
</button>
|
||||
<div className={styles.filterRight}>
|
||||
<SortbySelector
|
||||
filterOptions={sortByOptions}
|
||||
onChange={sortOnchange}
|
||||
onDescending={sortOnDescending}
|
||||
placeHolder="Sort By"
|
||||
sortByDescending={sortByDescending}
|
||||
sortByButton={sortByButton}
|
||||
value={sortByState}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="blocklist" data-cy="home-endpointList">
|
||||
{renderItems(
|
||||
isLoading,
|
||||
totalCount,
|
||||
environments.map((env) => (
|
||||
<EnvironmentItem
|
||||
key={env.Id}
|
||||
environment={env}
|
||||
groupName={
|
||||
groupsQuery.data?.find((g) => g.Id === env.GroupId)?.Name
|
||||
}
|
||||
onClick={onClickItem}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<TableFooter>
|
||||
<PaginationControls
|
||||
showAll={totalCount <= 100}
|
||||
pageLimit={pageLimit}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
totalCount={totalCount}
|
||||
onPageLimitChange={setPageLimit}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -46,39 +46,35 @@ export function AccessControlPanel({
|
|||
(!isAdmin && !isPartOfRestrictedUsers && !isLeaderOfAnyRestrictedTeams);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<TableContainer>
|
||||
<TableTitle label="Access control" icon="eye" featherIcon />
|
||||
<AccessControlPanelDetails
|
||||
resourceType={resourceType}
|
||||
resourceControl={resourceControl}
|
||||
/>
|
||||
<TableContainer>
|
||||
<TableTitle label="Access control" icon="eye" featherIcon />
|
||||
<AccessControlPanelDetails
|
||||
resourceType={resourceType}
|
||||
resourceControl={resourceControl}
|
||||
/>
|
||||
|
||||
{!isEditDisabled && !isEditMode && (
|
||||
<div className="row">
|
||||
<div>
|
||||
<Button color="link" onClick={toggleEditMode}>
|
||||
<Icon icon="edit" className="space-right" feather />
|
||||
Change ownership
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!isEditDisabled && !isEditMode && (
|
||||
<div className="row">
|
||||
<div>
|
||||
<Button color="link" onClick={toggleEditMode}>
|
||||
<Icon icon="edit" className="space-right" feather />
|
||||
Change ownership
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditMode && (
|
||||
<AccessControlPanelForm
|
||||
resourceControl={resourceControl}
|
||||
onCancelClick={() => toggleEditMode()}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
environmentId={environmentId}
|
||||
onUpdateSuccess={handleUpdateSuccess}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
</div>
|
||||
</div>
|
||||
{isEditMode && (
|
||||
<AccessControlPanelForm
|
||||
resourceControl={resourceControl}
|
||||
onCancelClick={() => toggleEditMode()}
|
||||
resourceId={resourceId}
|
||||
resourceType={resourceType}
|
||||
environmentId={environmentId}
|
||||
onUpdateSuccess={handleUpdateSuccess}
|
||||
/>
|
||||
)}
|
||||
</TableContainer>
|
||||
);
|
||||
|
||||
async function handleUpdateSuccess() {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Clock, Trash2 } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import {
|
||||
FeatureFlag,
|
||||
|
@ -11,6 +12,7 @@ import { Datatable } from '@@/datatables';
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { useList } from '../queries/list';
|
||||
import { EdgeUpdateSchedule } from '../types';
|
||||
|
@ -20,12 +22,15 @@ import { columns } from './columns';
|
|||
import { createStore } from './datatable-store';
|
||||
|
||||
const storageKey = 'update-schedules-list';
|
||||
const useStore = createStore(storageKey);
|
||||
const settingsStore = createStore(storageKey);
|
||||
|
||||
export function ListView() {
|
||||
useRedirectFeatureFlag(FeatureFlag.EdgeRemoteUpdate);
|
||||
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const listQuery = useList();
|
||||
const store = useStore();
|
||||
|
||||
if (!listQuery.data) {
|
||||
return null;
|
||||
|
@ -40,20 +45,22 @@ export function ListView() {
|
|||
/>
|
||||
|
||||
<Datatable
|
||||
columns={columns}
|
||||
titleOptions={{
|
||||
title: 'Update & rollback',
|
||||
icon: Clock,
|
||||
}}
|
||||
dataset={listQuery.data}
|
||||
settingsStore={store}
|
||||
storageKey={storageKey}
|
||||
columns={columns}
|
||||
title="Update & rollback"
|
||||
titleIcon={Clock}
|
||||
emptyContentLabel="No schedules found"
|
||||
isLoading={listQuery.isLoading}
|
||||
totalCount={listQuery.data.length}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<TableActions selectedRows={selectedRows} />
|
||||
)}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,36 +1,25 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
refreshableSettings,
|
||||
hiddenColumnsSettings,
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
createPersistedStore,
|
||||
BasicTableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
extends BasicTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set),
|
||||
...paginationSettings(set),
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
return createPersistedStore<TableSettings>(
|
||||
storageKey,
|
||||
'time',
|
||||
|
||||
(set) => ({
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -11,17 +11,20 @@ import { withReactQuery } from '@/react-tools/withReactQuery';
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { notificationsStore } from './notifications-store';
|
||||
import { ToastNotification } from './types';
|
||||
import { columns } from './columns';
|
||||
import { createStore } from './datatable-store';
|
||||
|
||||
const storageKey = 'notifications-list';
|
||||
const useSettingsStore = createStore(storageKey, 'time', true);
|
||||
const settingsStore = createPersistedStore(storageKey, {
|
||||
id: 'time',
|
||||
desc: true,
|
||||
});
|
||||
|
||||
export function NotificationsView() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const { user } = useUser();
|
||||
|
||||
const userNotifications: ToastNotification[] =
|
||||
|
@ -29,9 +32,11 @@ export function NotificationsView() {
|
|||
[];
|
||||
|
||||
const breadcrumbs = 'Notifications';
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
const {
|
||||
params: { id },
|
||||
params: { id: activeItemId },
|
||||
} = useCurrentStateAndParams();
|
||||
|
||||
return (
|
||||
|
@ -39,19 +44,22 @@ export function NotificationsView() {
|
|||
<PageHeader title="Notifications" breadcrumbs={breadcrumbs} reload />
|
||||
<Datatable
|
||||
columns={columns}
|
||||
titleOptions={{
|
||||
title: 'Notifications',
|
||||
icon: Bell,
|
||||
}}
|
||||
title="Notifications"
|
||||
titleIcon={Bell}
|
||||
dataset={userNotifications}
|
||||
settingsStore={settingsStore}
|
||||
storageKey="notifications"
|
||||
emptyContentLabel="No notifications found"
|
||||
totalCount={userNotifications.length}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<TableActions selectedRows={selectedRows} />
|
||||
)}
|
||||
initialActiveItem={id}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
getRowId={(row) => row.id}
|
||||
highlightedItemId={activeItemId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/react/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
refreshableSettings,
|
||||
hiddenColumnsSettings,
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
||||
export function createStore(
|
||||
storageKey: string,
|
||||
initialSortBy?: string,
|
||||
desc?: boolean
|
||||
) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set, initialSortBy, desc),
|
||||
...paginationSettings(set),
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
|
@ -1,33 +1,17 @@
|
|||
import { useTable, usePagination, useSortBy } from 'react-table';
|
||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import { List } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
import PortainerError from '@/portainer/error';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { useTableSettings } from '@@/datatables/useTableSettings';
|
||||
import { useRowSelect } from '@@/datatables/useRowSelect';
|
||||
import {
|
||||
Table,
|
||||
TableContainer,
|
||||
TableHeaderRow,
|
||||
TableRow,
|
||||
TableTitle,
|
||||
} from '@@/datatables';
|
||||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types-old';
|
||||
|
||||
import { useFDOProfiles } from './useFDOProfiles';
|
||||
import { useColumns } from './columns';
|
||||
import { FDOProfilesDatatableActions } from './FDOProfilesDatatableActions';
|
||||
import { useFDOProfiles } from './useFDOProfiles';
|
||||
|
||||
export interface FDOProfilesTableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings {}
|
||||
const storageKey = 'fdoProfiles';
|
||||
|
||||
const settingsStore = createPersistedStore(storageKey, 'name');
|
||||
|
||||
export interface FDOProfilesDatatableProps {
|
||||
isFDOEnabled: boolean;
|
||||
|
@ -36,132 +20,33 @@ export interface FDOProfilesDatatableProps {
|
|||
export function FDOProfilesDatatable({
|
||||
isFDOEnabled,
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const { settings, setTableSettings } =
|
||||
useTableSettings<FDOProfilesTableSettings>();
|
||||
const columns = useColumns();
|
||||
|
||||
const { isLoading, profiles, error } = useFDOProfiles();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Profile>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: profiles,
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
},
|
||||
isRowSelectable() {
|
||||
return isFDOEnabled;
|
||||
},
|
||||
selectColumnWidth: 5,
|
||||
},
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useRowSelectColumn
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const { isLoading, profiles } = useFDOProfiles();
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<TableTitle icon="list" featherIcon label="Device Profiles">
|
||||
<Datatable
|
||||
columns={columns}
|
||||
dataset={profiles}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
title="Device Profiles"
|
||||
titleIcon={List}
|
||||
disableSelect={!isFDOEnabled}
|
||||
emptyContentLabel="No profiles found"
|
||||
getRowId={(row) => row.id.toString()}
|
||||
isLoading={isLoading}
|
||||
renderTableActions={(selectedItems) => (
|
||||
<FDOProfilesDatatableActions
|
||||
isFDOEnabled={isFDOEnabled}
|
||||
selectedItems={selectedFlatRows.map((row) => row.original)}
|
||||
selectedItems={selectedItems}
|
||||
/>
|
||||
</TableTitle>
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
return (
|
||||
<TableHeaderRow<Profile>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
{!isLoading && profiles && profiles.length > 0 ? (
|
||||
page.map((row) => {
|
||||
prepareRow(row);
|
||||
const { key, className, role, style } = row.getRowProps();
|
||||
|
||||
return (
|
||||
<TableRow<Profile>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center text-muted">
|
||||
{userMessage(isLoading, error)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={profiles ? profiles.length : 0}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</TableContainer>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings((settings) => ({ ...settings, pageSize }));
|
||||
}
|
||||
}
|
||||
|
||||
function userMessage(isLoading: boolean, error?: PortainerError) {
|
||||
if (isLoading) {
|
||||
return 'Loading...';
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return error.message;
|
||||
}
|
||||
|
||||
return 'No profiles found';
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import { TableSettingsProvider } from '@@/datatables/useTableSettings';
|
||||
|
||||
import {
|
||||
FDOProfilesDatatable,
|
||||
FDOProfilesDatatableProps,
|
||||
} from './FDOProfilesDatatable';
|
||||
|
||||
export function FDOProfilesDatatableContainer({
|
||||
...props
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const defaultSettings = {
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'name', desc: false },
|
||||
};
|
||||
|
||||
return (
|
||||
<TableSettingsProvider defaults={defaultSettings} storageKey="fdoProfiles">
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<FDOProfilesDatatable {...props} />
|
||||
</TableSettingsProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { FDOProfilesDatatable } from './FDOProfilesDatatable';
|
|
@ -11,7 +11,7 @@ import { LoadingButton } from '@@/buttons/LoadingButton';
|
|||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
|
||||
import { FDOProfilesDatatableContainer } from '../FDOProfilesDatatable/FDOProfilesDatatableContainer';
|
||||
import { FDOProfilesDatatable } from '../FDOProfilesDatatable';
|
||||
|
||||
import styles from './SettingsFDO.module.css';
|
||||
import { validationSchema } from './SettingsFDO.validation';
|
||||
|
@ -165,7 +165,7 @@ export function SettingsFDO({ settings, onSubmit }: Props) {
|
|||
Add, Edit and Manage the list of device profiles available
|
||||
during FDO device setup
|
||||
</TextTip>
|
||||
<FDOProfilesDatatableContainer isFDOEnabled={initialFDOEnabled} />
|
||||
<FDOProfilesDatatable isFDOEnabled={initialFDOEnabled} />
|
||||
</div>
|
||||
)}
|
||||
</WidgetBody>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { PageHeader } from '@@/PageHeader';
|
|||
import { useTeams } from '../queries';
|
||||
|
||||
import { CreateTeamForm } from './CreateTeamForm';
|
||||
import { TeamsDatatableContainer } from './TeamsDatatable/TeamsDatatable';
|
||||
import { TeamsDatatable } from './TeamsDatatable';
|
||||
|
||||
export function ListView() {
|
||||
const { isAdmin } = useUser();
|
||||
|
@ -23,7 +23,7 @@ export function ListView() {
|
|||
)}
|
||||
|
||||
{teamsQuery.data && (
|
||||
<TeamsDatatableContainer teams={teamsQuery.data} isAdmin={isAdmin} />
|
||||
<TeamsDatatable teams={teamsQuery.data} isAdmin={isAdmin} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,14 +1,7 @@
|
|||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||
import {
|
||||
Column,
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { Column } from 'react-table';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { Trash2, Users } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
|
@ -16,23 +9,13 @@ import { Team, TeamId } from '@/react/portainer/users/teams/types';
|
|||
import { deleteTeam } from '@/react/portainer/users/teams/teams.service';
|
||||
import { confirmDeletionAsync } from '@/portainer/services/modal.service/confirm';
|
||||
|
||||
import { PaginationControls } from '@@/PaginationControls';
|
||||
import { Checkbox } from '@@/form-components/Checkbox';
|
||||
import { Table } from '@@/datatables';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { SearchBar, useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { SelectedRowsCount } from '@@/datatables/SelectedRowsCount';
|
||||
import {
|
||||
TableSettingsProvider,
|
||||
useTableSettings,
|
||||
} from '@@/datatables/useTableSettings';
|
||||
import { TableContent } from '@@/datatables/TableContent';
|
||||
import { buildNameColumn } from '@@/datatables/NameCell';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
|
||||
import { TableSettings } from './types';
|
||||
|
||||
const tableKey = 'teams';
|
||||
const storageKey = 'teams';
|
||||
|
||||
const columns: readonly Column<Team>[] = [
|
||||
buildNameColumn('Name', 'Id', 'portainer.teams.team'),
|
||||
|
@ -43,168 +26,47 @@ interface Props {
|
|||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
const settingsStore = createPersistedStore(storageKey);
|
||||
|
||||
export function TeamsDatatable({ teams, isAdmin }: Props) {
|
||||
const { handleRemove } = useRemoveMutation();
|
||||
|
||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(tableKey);
|
||||
const { settings, setTableSettings } = useTableSettings<TableSettings>();
|
||||
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
selectedFlatRows,
|
||||
gotoPage,
|
||||
setPageSize,
|
||||
setGlobalFilter,
|
||||
state: { pageIndex, pageSize },
|
||||
} = useTable<Team>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: teams,
|
||||
|
||||
initialState: {
|
||||
pageSize: settings.pageSize || 10,
|
||||
sortBy: [settings.sortBy],
|
||||
globalFilter: searchBarValue,
|
||||
},
|
||||
selectCheckboxComponent: Checkbox,
|
||||
},
|
||||
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination,
|
||||
useRowSelect,
|
||||
isAdmin ? useRowSelectColumn : emptyPlugin
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-12">
|
||||
<Table.Container>
|
||||
<Table.Title icon={Users} label="Teams">
|
||||
<SearchBar
|
||||
value={searchBarValue}
|
||||
onChange={handleSearchBarChange}
|
||||
/>
|
||||
|
||||
{isAdmin && (
|
||||
<Table.Actions>
|
||||
<Button
|
||||
color="dangerlight"
|
||||
onClick={handleRemoveClick}
|
||||
disabled={selectedFlatRows.length === 0}
|
||||
icon={Trash2}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</Table.Actions>
|
||||
)}
|
||||
</Table.Title>
|
||||
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
<Datatable
|
||||
dataset={teams}
|
||||
columns={columns}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
title="Teams"
|
||||
titleIcon={Users}
|
||||
renderTableActions={(selectedRows) =>
|
||||
isAdmin && (
|
||||
<Button
|
||||
color="dangerlight"
|
||||
onClick={() => handleRemoveClick(selectedRows)}
|
||||
disabled={selectedRows.length === 0}
|
||||
icon={Trash2}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<Table.HeaderRow<Team>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<TableContent
|
||||
prepareRow={prepareRow}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<Table.Row<Team>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
)}
|
||||
rows={page}
|
||||
emptyContent="No teams found"
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<TableFooter>
|
||||
<SelectedRowsCount value={selectedFlatRows.length} />
|
||||
<PaginationControls
|
||||
showAll
|
||||
pageLimit={pageSize}
|
||||
page={pageIndex + 1}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
totalCount={teams.length}
|
||||
onPageLimitChange={handlePageSizeChange}
|
||||
/>
|
||||
</TableFooter>
|
||||
</Table.Container>
|
||||
</div>
|
||||
</div>
|
||||
Remove
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
emptyContentLabel="No teams found"
|
||||
/>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(pageSize: number) {
|
||||
setPageSize(pageSize);
|
||||
setTableSettings({ pageSize });
|
||||
}
|
||||
|
||||
function handleSearchBarChange(value: string) {
|
||||
setSearchBarValue(value);
|
||||
setGlobalFilter(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setTableSettings({ sortBy: { id, desc } });
|
||||
}
|
||||
|
||||
function handleRemoveClick() {
|
||||
const ids = selectedFlatRows.map((row) => row.original.Id);
|
||||
function handleRemoveClick(selectedRows: Team[]) {
|
||||
const ids = selectedRows.map((row) => row.Id);
|
||||
handleRemove(ids);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSettings: TableSettings = {
|
||||
pageSize: 10,
|
||||
sortBy: { id: 'name', desc: false },
|
||||
};
|
||||
|
||||
export function TeamsDatatableContainer(props: Props) {
|
||||
return (
|
||||
<TableSettingsProvider<TableSettings>
|
||||
defaults={defaultSettings}
|
||||
storageKey={tableKey}
|
||||
>
|
||||
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
|
||||
<TeamsDatatable {...props} />
|
||||
</TableSettingsProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function useRemoveMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
|
@ -239,6 +101,3 @@ function useRemoveMutation() {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
function emptyPlugin() {}
|
||||
emptyPlugin.pluginName = 'emptyPlugin';
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import {
|
||||
PaginationTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@@/datatables/types-old';
|
||||
|
||||
export interface TableSettings
|
||||
extends PaginationTableSettings,
|
||||
SortableTableSettings {}
|
Loading…
Add table
Add a link
Reference in a new issue