mirror of
https://github.com/portainer/portainer.git
synced 2025-08-08 23:35:31 +02:00
chore(deps): upgrade react-table to v8 [EE-4837] (#8245)
This commit is contained in:
parent
f20d3e72b9
commit
757461d58b
140 changed files with 1805 additions and 2872 deletions
|
@ -1,5 +1,4 @@
|
|||
import { Clock, Trash2 } from 'lucide-react';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { withLimitToBE } from '@/react/hooks/useLimitToBE';
|
||||
|
@ -9,7 +8,7 @@ import { Datatable } from '@@/datatables';
|
|||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Link } from '@@/Link';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
import { useList } from '../queries/list';
|
||||
import { EdgeUpdateSchedule, StatusType } from '../types';
|
||||
|
@ -25,8 +24,7 @@ const settingsStore = createStore(storageKey);
|
|||
export default withLimitToBE(ListView);
|
||||
|
||||
export function ListView() {
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
|
||||
const listQuery = useList(true);
|
||||
|
||||
|
@ -47,6 +45,7 @@ export function ListView() {
|
|||
<Datatable
|
||||
dataset={listQuery.data}
|
||||
columns={columns}
|
||||
settingsManager={tableState}
|
||||
title="Update & rollback"
|
||||
titleIcon={Clock}
|
||||
emptyContentLabel="No schedules found"
|
||||
|
@ -55,12 +54,6 @@ export function ListView() {
|
|||
renderTableActions={(selectedRows) => (
|
||||
<TableActions selectedRows={selectedRows} />
|
||||
)}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
isRowSelectable={(row) => row.original.status === StatusType.Pending}
|
||||
/>
|
||||
</>
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||
|
||||
import { EdgeUpdateListItemResponse } from '../../queries/list';
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const created: Column<EdgeUpdateListItemResponse> = {
|
||||
Header: 'Created',
|
||||
accessor: (row) => isoDateFromTimestamp(row.created),
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
};
|
||||
export const created = columnHelper.accessor('created', {
|
||||
id: 'created',
|
||||
header: 'Created',
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return isoDateFromTimestamp(value);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import _ from 'lodash';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
import { EdgeGroup } from '@/react/edge/edge-groups/types';
|
||||
import { useEdgeGroups } from '@/react/edge/edge-groups/queries/useEdgeGroups';
|
||||
|
||||
import { EdgeUpdateListItemResponse } from '../../queries/list';
|
||||
|
||||
export const groups: Column<EdgeUpdateListItemResponse> = {
|
||||
Header: 'Groups',
|
||||
accessor: 'edgeGroupIds',
|
||||
Cell: GroupsCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
disableSortBy: true,
|
||||
};
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const groups = columnHelper.accessor('edgeGroupIds', {
|
||||
header: 'Groups',
|
||||
cell: GroupsCell,
|
||||
});
|
||||
|
||||
export function GroupsCell({
|
||||
value: groupsIds,
|
||||
}: CellProps<EdgeUpdateListItemResponse, Array<EdgeGroup['Id']>>) {
|
||||
getValue,
|
||||
}: CellContext<EdgeUpdateListItemResponse, Array<EdgeGroup['Id']>>) {
|
||||
const groupsIds = getValue();
|
||||
const groupsQuery = useEdgeGroups();
|
||||
|
||||
const groups = _.compact(
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { EdgeUpdateListItemResponse } from '../../queries/list';
|
||||
|
||||
export const columnHelper = createColumnHelper<EdgeUpdateListItemResponse>();
|
|
@ -1,12 +1,15 @@
|
|||
import { buildNameColumn } from '@@/datatables/NameCell';
|
||||
|
||||
import { EdgeUpdateListItemResponse } from '../../queries/list';
|
||||
|
||||
import { created } from './created';
|
||||
import { groups } from './groups';
|
||||
import { name } from './name';
|
||||
import { scheduleStatus } from './schedule-status';
|
||||
import { scheduledTime } from './scheduled-time';
|
||||
import { scheduleType } from './type';
|
||||
|
||||
export const columns = [
|
||||
name,
|
||||
buildNameColumn<EdgeUpdateListItemResponse>('name', 'id', '.item'),
|
||||
scheduledTime,
|
||||
groups,
|
||||
scheduleType,
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
|
||||
import { Link } from '@@/Link';
|
||||
|
||||
import { EdgeUpdateListItemResponse } from '../../queries/list';
|
||||
|
||||
export const name: Column<EdgeUpdateListItemResponse> = {
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
id: 'name',
|
||||
Cell: NameCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
};
|
||||
|
||||
export function NameCell({
|
||||
value: name,
|
||||
row,
|
||||
}: CellProps<EdgeUpdateListItemResponse>) {
|
||||
return (
|
||||
<Link to=".item" params={{ id: row.original.id }}>
|
||||
{name}
|
||||
</Link>
|
||||
);
|
||||
}
|
|
@ -1,27 +1,26 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
import { EdgeUpdateListItemResponse } from '../../queries/list';
|
||||
import { StatusType } from '../../types';
|
||||
|
||||
export const scheduleStatus: Column<EdgeUpdateListItemResponse> = {
|
||||
Header: 'Status',
|
||||
accessor: (row) => row.status,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
Cell: StatusCell,
|
||||
disableSortBy: true,
|
||||
};
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const scheduleStatus = columnHelper.accessor('status', {
|
||||
header: 'Status',
|
||||
cell: StatusCell,
|
||||
});
|
||||
|
||||
function StatusCell({
|
||||
value: status,
|
||||
getValue,
|
||||
row: {
|
||||
original: { statusMessage },
|
||||
},
|
||||
}: CellProps<
|
||||
}: CellContext<
|
||||
EdgeUpdateListItemResponse,
|
||||
EdgeUpdateListItemResponse['status']
|
||||
>) {
|
||||
const status = getValue();
|
||||
|
||||
switch (status) {
|
||||
case StatusType.Failed:
|
||||
return statusMessage;
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
import { Column } from 'react-table';
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
import { EdgeUpdateListItemResponse } from '../../queries/list';
|
||||
|
||||
export const scheduledTime: Column<EdgeUpdateListItemResponse> = {
|
||||
Header: 'Scheduled Time & Date',
|
||||
accessor: (row) => row.scheduledTime,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
};
|
||||
export const scheduledTime = columnHelper.accessor('scheduledTime', {
|
||||
header: 'Scheduled Time & Date',
|
||||
});
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { ScheduleType } from '../../types';
|
||||
import { EdgeUpdateListItemResponse } from '../../queries/list';
|
||||
|
||||
export const scheduleType: Column<EdgeUpdateListItemResponse> = {
|
||||
Header: 'Type',
|
||||
accessor: (row) => ScheduleType[row.type],
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
};
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const scheduleType = columnHelper.accessor('type', {
|
||||
header: 'Type',
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
|
||||
return ScheduleType[value];
|
||||
},
|
||||
});
|
||||
|
|
|
@ -12,7 +12,7 @@ import { PageHeader } from '@@/PageHeader';
|
|||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
import { notificationsStore } from './notifications-store';
|
||||
import { ToastNotification } from './types';
|
||||
|
@ -32,8 +32,7 @@ export function NotificationsView() {
|
|||
[];
|
||||
|
||||
const breadcrumbs = 'Notifications';
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
|
||||
const {
|
||||
params: { id: activeItemId },
|
||||
|
@ -47,17 +46,12 @@ export function NotificationsView() {
|
|||
title="Notifications"
|
||||
titleIcon={Bell}
|
||||
dataset={userNotifications}
|
||||
settingsManager={tableState}
|
||||
emptyContentLabel="No notifications found"
|
||||
totalCount={userNotifications.length}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<TableActions selectedRows={selectedRows} />
|
||||
)}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
getRowId={(row) => row.id}
|
||||
highlightedItemId={activeItemId}
|
||||
/>
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
import { Column } from 'react-table';
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
|
||||
export const details: Column<ToastNotification> = {
|
||||
Header: 'Details',
|
||||
accessor: 'details',
|
||||
export const details = columnHelper.accessor('details', {
|
||||
header: 'Details',
|
||||
id: 'details',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
Cell: ({ value }: { value: string }) => (
|
||||
<div className="whitespace-normal">{value}</div>
|
||||
),
|
||||
};
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
|
||||
return <div className="whitespace-normal">{value}</div>;
|
||||
},
|
||||
});
|
||||
|
|
5
app/react/portainer/notifications/columns/helper.ts
Normal file
5
app/react/portainer/notifications/columns/helper.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
|
||||
export const columnHelper = createColumnHelper<ToastNotification>();
|
|
@ -1,13 +1,13 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { isoDate } from '@/portainer/filters/filters';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const time: Column<ToastNotification> = {
|
||||
Header: 'Time',
|
||||
accessor: (row) => (row.timeStamp ? isoDate(row.timeStamp) : '-'),
|
||||
export const time = columnHelper.accessor('timeStamp', {
|
||||
header: 'Time',
|
||||
id: 'time',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
};
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
|
||||
return value ? isoDate(value) : '-';
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import { Column } from 'react-table';
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
|
||||
export const title: Column<ToastNotification> = {
|
||||
Header: 'Title',
|
||||
accessor: 'title',
|
||||
export const title = columnHelper.accessor('title', {
|
||||
header: 'Title',
|
||||
id: 'title',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { Column } from 'react-table';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const type: Column<ToastNotification> = {
|
||||
Header: 'Type',
|
||||
accessor: (row) => row.type.charAt(0).toUpperCase() + row.type.slice(1),
|
||||
export const type = columnHelper.accessor('type', {
|
||||
header: 'Type',
|
||||
id: 'type',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
};
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
|
||||
return _.capitalize(value);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { List } from 'lucide-react';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
import { useColumns } from './columns';
|
||||
import { columns } from './columns';
|
||||
import { FDOProfilesDatatableActions } from './FDOProfilesDatatableActions';
|
||||
import { useFDOProfiles } from './useFDOProfiles';
|
||||
|
||||
|
@ -20,21 +19,15 @@ export interface FDOProfilesDatatableProps {
|
|||
export function FDOProfilesDatatable({
|
||||
isFDOEnabled,
|
||||
}: FDOProfilesDatatableProps) {
|
||||
const columns = useColumns();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
|
||||
const { isLoading, profiles } = useFDOProfiles();
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
columns={columns}
|
||||
dataset={profiles}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
settingsManager={tableState}
|
||||
title="Device Profiles"
|
||||
titleIcon={List}
|
||||
disableSelect={!isFDOEnabled}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
export const created: Column<Profile> = {
|
||||
Header: 'Created',
|
||||
accessor: 'dateCreated',
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const created = columnHelper.accessor('dateCreated', {
|
||||
header: 'Created',
|
||||
id: 'created',
|
||||
Cell: ({ value }) => isoDateFromTimestamp(value),
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
Filter: () => null,
|
||||
};
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return isoDateFromTimestamp(value);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
export const columnHelper = createColumnHelper<Profile>();
|
|
@ -1,8 +1,10 @@
|
|||
import { useMemo } from 'react';
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
import { buildNameColumn } from '@@/datatables/NameCell';
|
||||
|
||||
import { created } from './created';
|
||||
import { name } from './name';
|
||||
|
||||
export function useColumns() {
|
||||
return useMemo(() => [name, created], []);
|
||||
}
|
||||
export const columns = [
|
||||
buildNameColumn<Profile>('name', 'id', 'portainer.endpoints.profile.edit'),
|
||||
created,
|
||||
];
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { useSref } from '@uirouter/react';
|
||||
|
||||
import { Profile } from '@/portainer/hostmanagement/fdo/model';
|
||||
|
||||
export const name: Column<Profile> = {
|
||||
Header: 'Name',
|
||||
accessor: 'name',
|
||||
id: 'name',
|
||||
Cell: NameCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: true,
|
||||
sortType: 'string',
|
||||
};
|
||||
|
||||
export function NameCell({
|
||||
value: name,
|
||||
row: { original: profile },
|
||||
}: CellProps<Profile>) {
|
||||
const linkProps = useSref('portainer.endpoints.profile.edit', {
|
||||
id: profile.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<a href={linkProps.href} onClick={linkProps.onClick} title={name}>
|
||||
{name}
|
||||
</a>
|
||||
);
|
||||
}
|
|
@ -31,11 +31,11 @@ export function TeamAssociationSelector({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<div className="col-sm-6">
|
||||
<div className="flex">
|
||||
<div className="w-1/2">
|
||||
<UsersList users={usersNotInTeam} disabled={disabled} teamId={teamId} />
|
||||
</div>
|
||||
<div className="col-sm-6">
|
||||
<div className="w-1/2">
|
||||
<TeamMembersList
|
||||
teamId={teamId}
|
||||
disabled={disabled}
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
import {
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Users, UserX } from 'lucide-react';
|
||||
|
||||
|
@ -16,17 +10,11 @@ import {
|
|||
useTeamMemberships,
|
||||
} from '@/react/portainer/users/teams/queries';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
import { PageSelector } from '@@/PaginationControls/PageSelector';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Table } from '@@/datatables';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { Datatable } from '@@/datatables';
|
||||
|
||||
import { name } from './name-column';
|
||||
import { RowContext, RowProvider } from './RowContext';
|
||||
import { teamRole } from './team-role-column';
|
||||
|
||||
const columns = [name, teamRole];
|
||||
import { columns } from './columns';
|
||||
|
||||
interface Props {
|
||||
users: User[];
|
||||
|
@ -45,39 +33,10 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
|
|||
|
||||
const [search, setSearch] = useState('');
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const [sortBy, setSortBy] = useState({ id: 'name', desc: false });
|
||||
|
||||
const { isAdmin } = useUser();
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
gotoPage,
|
||||
setPageSize: setPageSizeInternal,
|
||||
setGlobalFilter: setGlobalFilterInternal,
|
||||
state: { pageIndex },
|
||||
setSortBy,
|
||||
rows,
|
||||
} = useTable<User>(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: users,
|
||||
initialState: {
|
||||
pageSize,
|
||||
|
||||
sortBy: [{ id: 'name', desc: false }],
|
||||
globalFilter: search,
|
||||
},
|
||||
},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const rowContext = useMemo<RowContext>(
|
||||
() => ({
|
||||
getRole(userId: UserId) {
|
||||
|
@ -88,124 +47,40 @@ export function TeamMembersList({ users, roles, disabled, teamId }: Props) {
|
|||
}),
|
||||
[roles, disabled, teamId]
|
||||
);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Widget.Title icon={Users} title="Team members">
|
||||
Items per page:
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={handlePageSizeChange}
|
||||
className="space-left"
|
||||
>
|
||||
<option value={Number.MAX_SAFE_INTEGER}>All</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
</Widget.Title>
|
||||
<Widget.Taskbar className="col-sm-12 nopadding">
|
||||
<div className="col-sm-12 col-md-6 nopadding">
|
||||
{isAdmin && (
|
||||
<RowProvider context={rowContext}>
|
||||
<Datatable<User>
|
||||
dataset={users}
|
||||
columns={columns}
|
||||
titleIcon={Users}
|
||||
title="Team members"
|
||||
renderTableActions={() =>
|
||||
isAdmin && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleRemoveMembers(rows.map(({ original }) => original.Id))
|
||||
}
|
||||
onClick={() => handleRemoveMembers(users.map((user) => user.Id))}
|
||||
disabled={disabled || users.length === 0}
|
||||
icon={UserX}
|
||||
>
|
||||
Remove all users
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-12 col-md-6 nopadding">
|
||||
<Input
|
||||
type="text"
|
||||
id="filter-users"
|
||||
value={search}
|
||||
onChange={handleSearchBarChange}
|
||||
placeholder="Filter..."
|
||||
className="input-sm"
|
||||
/>
|
||||
</div>
|
||||
</Widget.Taskbar>
|
||||
<Widget.Body className="nopadding">
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<Table.HeaderRow<User>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<Table.Content
|
||||
emptyContent="No users."
|
||||
prepareRow={prepareRow}
|
||||
rows={page}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<RowProvider context={rowContext} key={key}>
|
||||
<Table.Row<User>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
</RowProvider>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
<Table.Footer>
|
||||
{pageSize !== 0 && (
|
||||
<div className="pagination-controls">
|
||||
<PageSelector
|
||||
maxSize={5}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
currentPage={pageIndex + 1}
|
||||
itemsPerPage={pageSize}
|
||||
totalCount={rows.length}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</Table.Footer>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
disableSelect
|
||||
settingsManager={{
|
||||
pageSize,
|
||||
setPageSize,
|
||||
sortBy,
|
||||
setSortBy: handleSetSort,
|
||||
search,
|
||||
setSearch,
|
||||
}}
|
||||
/>
|
||||
</RowProvider>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const pageSize = parseInt(e.target.value, 10);
|
||||
setPageSize(pageSize);
|
||||
setPageSizeInternal(pageSize);
|
||||
}
|
||||
|
||||
function handleSearchBarChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { value } = e.target;
|
||||
setSearch(value);
|
||||
setGlobalFilterInternal(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setSortBy([{ id, desc }]);
|
||||
function handleSetSort(colId: string, desc: boolean) {
|
||||
setSortBy({ id: colId, desc });
|
||||
}
|
||||
|
||||
function handleRemoveMembers(userIds: UserId[]) {
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
|
||||
import { User } from '@/portainer/users/types';
|
||||
|
||||
export const columnHelper = createColumnHelper<User>();
|
|
@ -0,0 +1,4 @@
|
|||
import { name } from './name-column';
|
||||
import { teamRole } from './team-role-column';
|
||||
|
||||
export const columns = [name, teamRole];
|
|
@ -1,5 +1,5 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { MinusCircle } from 'lucide-react';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
import { User, UserId } from '@/portainer/users/types';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
@ -10,23 +10,21 @@ import {
|
|||
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { useRowContext } from './RowContext';
|
||||
import { useRowContext } from '../RowContext';
|
||||
|
||||
export const name: Column<User> = {
|
||||
Header: 'Name',
|
||||
accessor: (row) => row.Username,
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const name = columnHelper.accessor('Username', {
|
||||
header: 'Name',
|
||||
id: 'name',
|
||||
Cell: NameCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
};
|
||||
cell: NameCell,
|
||||
});
|
||||
|
||||
export function NameCell({
|
||||
value: name,
|
||||
getValue,
|
||||
row: { original: user },
|
||||
}: CellProps<User, string>) {
|
||||
}: CellContext<User, string>) {
|
||||
const name = getValue();
|
||||
const { disabled, teamId } = useRowContext();
|
||||
|
||||
const membershipsQuery = useTeamMemberships(teamId);
|
||||
|
@ -42,7 +40,7 @@ export function NameCell({
|
|||
|
||||
<Button
|
||||
color="link"
|
||||
className="space-left nopadding"
|
||||
className="space-left !p-0"
|
||||
onClick={() => handleRemoveMember(user.Id)}
|
||||
disabled={disabled}
|
||||
icon={MinusCircle}
|
|
@ -1,5 +1,5 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { User as UserIcon, UserPlus, UserX } from 'lucide-react';
|
||||
import { CellContext } from '@tanstack/react-table';
|
||||
|
||||
import { User } from '@/portainer/users/types';
|
||||
import { useUser as useCurrentUser } from '@/react/hooks/useUser';
|
||||
|
@ -13,20 +13,22 @@ import {
|
|||
import { Button } from '@@/buttons';
|
||||
import { Icon } from '@@/Icon';
|
||||
|
||||
import { useRowContext } from './RowContext';
|
||||
import { useRowContext } from '../RowContext';
|
||||
|
||||
export const teamRole: Column<User> = {
|
||||
Header: 'Team Role',
|
||||
accessor: 'Id',
|
||||
import { columnHelper } from './helper';
|
||||
|
||||
export const teamRole = columnHelper.accessor('Id', {
|
||||
header: 'Team Role',
|
||||
id: 'role',
|
||||
Cell: RoleCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
};
|
||||
cell: RoleCell,
|
||||
});
|
||||
|
||||
export function RoleCell({
|
||||
row: { original: user },
|
||||
getValue,
|
||||
}: CellContext<User, User['Id']>) {
|
||||
const id = getValue();
|
||||
|
||||
export function RoleCell({ row: { original: user } }: CellProps<User>) {
|
||||
const { getRole, disabled, teamId } = useRowContext();
|
||||
const membershipsQuery = useTeamMemberships(teamId);
|
||||
const updateRoleMutation = useUpdateRoleMutation(
|
||||
|
@ -34,7 +36,7 @@ export function RoleCell({ row: { original: user } }: CellProps<User>) {
|
|||
membershipsQuery.data
|
||||
);
|
||||
|
||||
const role = getRole(user.Id);
|
||||
const role = getRole(id);
|
||||
|
||||
const { isAdmin } = useCurrentUser();
|
||||
|
|
@ -1,9 +1,3 @@
|
|||
import {
|
||||
useGlobalFilter,
|
||||
usePagination,
|
||||
useSortBy,
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { UserPlus, Users } from 'lucide-react';
|
||||
|
||||
|
@ -13,12 +7,8 @@ import { notifySuccess } from '@/portainer/services/notifications';
|
|||
import { useAddMemberMutation } from '@/react/portainer/users/teams/queries';
|
||||
import { TeamId } from '@/react/portainer/users/teams/types';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
import { PageSelector } from '@@/PaginationControls/PageSelector';
|
||||
import { Button } from '@@/buttons';
|
||||
import { Table } from '@@/datatables';
|
||||
import { TableFooter } from '@@/datatables/TableFooter';
|
||||
import { Input } from '@@/form-components/Input';
|
||||
import { Datatable } from '@@/datatables';
|
||||
|
||||
import { name } from './name-column';
|
||||
import { RowProvider } from './RowContext';
|
||||
|
@ -35,158 +25,45 @@ export function UsersList({ users, disabled, teamId }: Props) {
|
|||
const [search, setSearch] = useState('');
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
const addMemberMutation = useAddMemberMutation(teamId);
|
||||
const [sortBy, setSortBy] = useState({ id: 'name', desc: false });
|
||||
|
||||
const { isAdmin } = useUser();
|
||||
const {
|
||||
getTableProps,
|
||||
getTableBodyProps,
|
||||
headerGroups,
|
||||
page,
|
||||
prepareRow,
|
||||
gotoPage,
|
||||
setPageSize: setPageSizeInternal,
|
||||
setGlobalFilter: setGlobalFilterInternal,
|
||||
state: { pageIndex },
|
||||
setSortBy,
|
||||
rows,
|
||||
} = useTable(
|
||||
{
|
||||
defaultCanFilter: false,
|
||||
columns,
|
||||
data: users,
|
||||
initialState: {
|
||||
pageSize,
|
||||
|
||||
sortBy: [{ id: 'name', desc: false }],
|
||||
globalFilter: search,
|
||||
},
|
||||
},
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
usePagination
|
||||
);
|
||||
|
||||
const tableProps = getTableProps();
|
||||
const tbodyProps = getTableBodyProps();
|
||||
const rowContext = useMemo(() => ({ disabled, teamId }), [disabled, teamId]);
|
||||
|
||||
return (
|
||||
<Widget>
|
||||
<Widget.Title icon={Users} title="Users">
|
||||
Items per page:
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={handlePageSizeChange}
|
||||
className="space-left"
|
||||
>
|
||||
<option value={Number.MAX_SAFE_INTEGER}>All</option>
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
</Widget.Title>
|
||||
<Widget.Taskbar className="col-sm-12 nopadding">
|
||||
<div className="col-sm-12 col-md-6 nopadding">
|
||||
{isAdmin && (
|
||||
<RowProvider context={rowContext}>
|
||||
<Datatable<User>
|
||||
dataset={users}
|
||||
columns={columns}
|
||||
titleIcon={Users}
|
||||
title="Users"
|
||||
renderTableActions={() =>
|
||||
isAdmin && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleAddAllMembers(rows.map((row) => row.original.Id))
|
||||
}
|
||||
disabled={disabled || rows.length === 0}
|
||||
onClick={() => handleAddAllMembers(users.map((u) => u.Id))}
|
||||
disabled={disabled || users.length === 0}
|
||||
icon={UserPlus}
|
||||
>
|
||||
Add all users
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="col-sm-12 col-md-6 nopadding">
|
||||
<Input
|
||||
type="text"
|
||||
id="filter-users"
|
||||
value={search}
|
||||
onChange={handleSearchBarChange}
|
||||
placeholder="Filter..."
|
||||
className="input-sm"
|
||||
/>
|
||||
</div>
|
||||
</Widget.Taskbar>
|
||||
<Widget.Body className="nopadding">
|
||||
<Table
|
||||
className={tableProps.className}
|
||||
role={tableProps.role}
|
||||
style={tableProps.style}
|
||||
>
|
||||
<thead>
|
||||
{headerGroups.map((headerGroup) => {
|
||||
const { key, className, role, style } =
|
||||
headerGroup.getHeaderGroupProps();
|
||||
|
||||
return (
|
||||
<Table.HeaderRow<User>
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
headers={headerGroup.headers}
|
||||
onSortChange={handleSortChange}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</thead>
|
||||
<tbody
|
||||
className={tbodyProps.className}
|
||||
role={tbodyProps.role}
|
||||
style={tbodyProps.style}
|
||||
>
|
||||
<Table.Content
|
||||
emptyContent="No users."
|
||||
prepareRow={prepareRow}
|
||||
rows={page}
|
||||
renderRow={(row, { key, className, role, style }) => (
|
||||
<RowProvider context={rowContext} key={key}>
|
||||
<Table.Row<User>
|
||||
cells={row.cells}
|
||||
key={key}
|
||||
className={className}
|
||||
role={role}
|
||||
style={style}
|
||||
/>
|
||||
</RowProvider>
|
||||
)}
|
||||
/>
|
||||
</tbody>
|
||||
</Table>
|
||||
<TableFooter>
|
||||
{pageSize !== 0 && (
|
||||
<div className="pagination-controls">
|
||||
<PageSelector
|
||||
maxSize={5}
|
||||
onPageChange={(p) => gotoPage(p - 1)}
|
||||
currentPage={pageIndex + 1}
|
||||
itemsPerPage={pageSize}
|
||||
totalCount={rows.length}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</TableFooter>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
)
|
||||
}
|
||||
disableSelect
|
||||
settingsManager={{
|
||||
pageSize,
|
||||
setPageSize,
|
||||
sortBy,
|
||||
setSortBy: handleSetSort,
|
||||
search,
|
||||
setSearch,
|
||||
}}
|
||||
/>
|
||||
</RowProvider>
|
||||
);
|
||||
|
||||
function handlePageSizeChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||
const pageSize = parseInt(e.target.value, 10);
|
||||
setPageSize(pageSize);
|
||||
setPageSizeInternal(pageSize);
|
||||
}
|
||||
|
||||
function handleSearchBarChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { value } = e.target;
|
||||
setSearch(value);
|
||||
setGlobalFilterInternal(value);
|
||||
}
|
||||
|
||||
function handleSortChange(id: string, desc: boolean) {
|
||||
setSortBy([{ id, desc }]);
|
||||
function handleSetSort(colId: string, desc: boolean) {
|
||||
setSortBy({ id: colId, desc });
|
||||
}
|
||||
|
||||
function handleAddAllMembers(userIds: UserId[]) {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { CellProps, Column } from 'react-table';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
import { CellContext, ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { User } from '@/portainer/users/types';
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
|
@ -9,21 +9,18 @@ import { Button } from '@@/buttons';
|
|||
|
||||
import { useRowContext } from './RowContext';
|
||||
|
||||
export const name: Column<User> = {
|
||||
Header: 'Name',
|
||||
accessor: (row) => row.Username,
|
||||
export const name: ColumnDef<User, string> = {
|
||||
header: 'Name',
|
||||
accessorFn: (row) => row.Username,
|
||||
id: 'name',
|
||||
Cell: NameCell,
|
||||
disableFilters: true,
|
||||
Filter: () => null,
|
||||
canHide: false,
|
||||
sortType: 'string',
|
||||
cell: NameCell,
|
||||
};
|
||||
|
||||
export function NameCell({
|
||||
value: name,
|
||||
getValue,
|
||||
row: { original: user },
|
||||
}: CellProps<User, string>) {
|
||||
}: CellContext<User, string>) {
|
||||
const name = getValue();
|
||||
const { disabled, teamId } = useRowContext();
|
||||
|
||||
const addMemberMutation = useAddMemberMutation(teamId);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { Column } from 'react-table';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { Trash2, Users } from 'lucide-react';
|
||||
import { useStore } from 'zustand';
|
||||
import { ColumnDef } from '@tanstack/react-table';
|
||||
|
||||
import { notifySuccess } from '@/portainer/services/notifications';
|
||||
import { promiseSequence } from '@/portainer/helpers/promise-utils';
|
||||
|
@ -13,13 +12,13 @@ import { Datatable } from '@@/datatables';
|
|||
import { Button } from '@@/buttons';
|
||||
import { buildNameColumn } from '@@/datatables/NameCell';
|
||||
import { createPersistedStore } from '@@/datatables/types';
|
||||
import { useSearchBarState } from '@@/datatables/SearchBar';
|
||||
import { useTableState } from '@@/datatables/useTableState';
|
||||
|
||||
const storageKey = 'teams';
|
||||
|
||||
const columns: readonly Column<Team>[] = [
|
||||
buildNameColumn('Name', 'Id', 'portainer.teams.team'),
|
||||
] as const;
|
||||
const columns: ColumnDef<Team>[] = [
|
||||
buildNameColumn<Team>('Name', 'Id', 'portainer.teams.team'),
|
||||
];
|
||||
|
||||
interface Props {
|
||||
teams: Team[];
|
||||
|
@ -30,19 +29,13 @@ const settingsStore = createPersistedStore(storageKey);
|
|||
|
||||
export function TeamsDatatable({ teams, isAdmin }: Props) {
|
||||
const { handleRemove } = useRemoveMutation();
|
||||
const settings = useStore(settingsStore);
|
||||
const [search, setSearch] = useSearchBarState(storageKey);
|
||||
const tableState = useTableState(settingsStore, storageKey);
|
||||
|
||||
return (
|
||||
<Datatable
|
||||
<Datatable<Team>
|
||||
dataset={teams}
|
||||
columns={columns}
|
||||
initialPageSize={settings.pageSize}
|
||||
onPageSizeChange={settings.setPageSize}
|
||||
initialSortBy={settings.sortBy}
|
||||
onSortByChange={settings.setSortBy}
|
||||
searchValue={search}
|
||||
onSearchChange={setSearch}
|
||||
settingsManager={tableState}
|
||||
title="Teams"
|
||||
titleIcon={Users}
|
||||
renderTableActions={(selectedRows) =>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue