mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Migrate to material react table
This commit is contained in:
parent
6b4ad3717f
commit
694ba9718d
4 changed files with 480 additions and 198 deletions
|
@ -1,5 +1,8 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { getActivityLogApi } from '@jellyfin/sdk/lib/utils/api/activity-log-api';
|
||||
import { getUserApi } from '@jellyfin/sdk/lib/utils/api/user-api';
|
||||
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||
import PermMedia from '@mui/icons-material/PermMedia';
|
||||
import Box from '@mui/material/Box';
|
||||
|
@ -7,20 +10,19 @@ import IconButton from '@mui/material/IconButton';
|
|||
import ToggleButton from '@mui/material/ToggleButton';
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||
import { type MRT_Cell, type MRT_ColumnDef, type MRT_Row, MaterialReactTable, useMaterialReactTable } from 'material-react-table';
|
||||
import { Link, useSearchParams } from 'react-router-dom';
|
||||
|
||||
import Page from 'components/Page';
|
||||
import UserAvatar from 'components/UserAvatar';
|
||||
import { useLogEntires } from 'hooks/useLogEntries';
|
||||
import { useUsers } from 'hooks/useUsers';
|
||||
import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'scripts/datetime';
|
||||
import { parseISO8601Date, toLocaleString } from 'scripts/datetime';
|
||||
import globalize from 'lib/globalize';
|
||||
import { toBoolean } from 'utils/string';
|
||||
|
||||
import LogLevelChip from '../components/activityTable/LogLevelChip';
|
||||
import OverviewCell from '../components/activityTable/OverviewCell';
|
||||
import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
|
||||
|
||||
const DEFAULT_PAGE_SIZE = 25;
|
||||
const VIEW_PARAM = 'useractivity';
|
||||
|
@ -37,16 +39,14 @@ const getActivityView = (param: string | null) => {
|
|||
return ActivityView.System;
|
||||
};
|
||||
|
||||
const getRowId = (row: ActivityLogEntry) => row.Id ?? -1;
|
||||
|
||||
const Activity = () => {
|
||||
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||
|
||||
const [ activityView, setActivityView ] = useState(
|
||||
getActivityView(searchParams.get(VIEW_PARAM)));
|
||||
|
||||
const [ paginationModel, setPaginationModel ] = useState({
|
||||
page: 0,
|
||||
const [ pagination, setPagination ] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: DEFAULT_PAGE_SIZE
|
||||
});
|
||||
|
||||
|
@ -68,106 +68,104 @@ const Activity = () => {
|
|||
}, [usersData]);
|
||||
|
||||
const activityParams = useMemo(() => ({
|
||||
startIndex: paginationModel.page * paginationModel.pageSize,
|
||||
limit: paginationModel.pageSize,
|
||||
startIndex: pagination.pageIndex * pagination.pageSize,
|
||||
limit: pagination.pageSize,
|
||||
hasUserId: activityView !== ActivityView.All ? activityView === ActivityView.User : undefined
|
||||
}), [activityView, paginationModel.page, paginationModel.pageSize]);
|
||||
}), [activityView, pagination.pageIndex, pagination.pageSize]);
|
||||
|
||||
const { data: logEntries, isLoading: isLogEntriesLoading } = useLogEntires(activityParams);
|
||||
|
||||
const isLoading = isUsersLoading || isLogEntriesLoading;
|
||||
|
||||
const userColDef: GridColDef[] = activityView !== ActivityView.System ? [
|
||||
const columns = useMemo<MRT_ColumnDef<ActivityLogEntry>[]>(() => [
|
||||
{
|
||||
field: 'User',
|
||||
headerName: globalize.translate('LabelUser'),
|
||||
width: 60,
|
||||
valueGetter: ( value, row ) => users[row.UserId]?.Name,
|
||||
renderCell: ({ row }) => (
|
||||
<IconButton
|
||||
size='large'
|
||||
color='inherit'
|
||||
sx={{ padding: 0 }}
|
||||
title={users[row.UserId]?.Name ?? undefined}
|
||||
component={Link}
|
||||
to={`/dashboard/users/profile?userId=${row.UserId}`}
|
||||
>
|
||||
<UserAvatar user={users[row.UserId]} />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
] : [];
|
||||
|
||||
const columns: GridColDef[] = [
|
||||
{
|
||||
field: 'Date',
|
||||
headerName: globalize.translate('LabelDate'),
|
||||
width: 90,
|
||||
type: 'date',
|
||||
valueGetter: ( value ) => parseISO8601Date(value),
|
||||
valueFormatter: ( value ) => toLocaleDateString(value)
|
||||
id: 'Date',
|
||||
accessorFn: row => parseISO8601Date(row.Date),
|
||||
header: globalize.translate('LabelTime'),
|
||||
size: 160,
|
||||
Cell: ({ cell }) => toLocaleString(cell.getValue<Date>())
|
||||
},
|
||||
{
|
||||
field: 'Time',
|
||||
headerName: globalize.translate('LabelTime'),
|
||||
width: 100,
|
||||
type: 'dateTime',
|
||||
valueGetter: ( value, row ) => parseISO8601Date(row.Date),
|
||||
valueFormatter: ( value ) => toLocaleTimeString(value)
|
||||
},
|
||||
{
|
||||
field: 'Severity',
|
||||
headerName: globalize.translate('LabelLevel'),
|
||||
width: 110,
|
||||
renderCell: ({ value }) => (
|
||||
value ? (
|
||||
<LogLevelChip level={value} />
|
||||
accessorKey: 'Severity',
|
||||
header: globalize.translate('LabelLevel'),
|
||||
size: 90,
|
||||
Cell: ({ cell }: { cell: MRT_Cell<ActivityLogEntry> }) => (
|
||||
cell.getValue<LogLevel | undefined>() ? (
|
||||
<LogLevelChip level={cell.getValue<LogLevel>()} />
|
||||
) : undefined
|
||||
)
|
||||
},
|
||||
...userColDef,
|
||||
{
|
||||
field: 'Name',
|
||||
headerName: globalize.translate('LabelName'),
|
||||
width: 300
|
||||
},
|
||||
{
|
||||
field: 'Overview',
|
||||
headerName: globalize.translate('LabelOverview'),
|
||||
width: 200,
|
||||
valueGetter: ( value, row ) => row.ShortOverview ?? row.Overview,
|
||||
renderCell: ({ row }) => (
|
||||
<OverviewCell {...row} />
|
||||
)
|
||||
},
|
||||
{
|
||||
field: 'Type',
|
||||
headerName: globalize.translate('LabelType'),
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
type: 'actions',
|
||||
width: 50,
|
||||
getActions: ({ row }) => {
|
||||
const actions = [];
|
||||
|
||||
if (row.ItemId) {
|
||||
actions.push(
|
||||
<GridActionsCellLink
|
||||
size='large'
|
||||
icon={<PermMedia />}
|
||||
label={globalize.translate('LabelMediaDetails')}
|
||||
title={globalize.translate('LabelMediaDetails')}
|
||||
to={`/details?id=${row.ItemId}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return actions;
|
||||
),
|
||||
enableResizing: false,
|
||||
muiTableBodyCellProps: {
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'User',
|
||||
accessorFn: row => row.UserId && users[row.UserId]?.Name,
|
||||
header: globalize.translate('LabelUser'),
|
||||
size: 75,
|
||||
Cell: ({ row }: { row: MRT_Row<ActivityLogEntry> }) => (
|
||||
row.original.UserId ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
color='inherit'
|
||||
sx={{ padding: 0 }}
|
||||
title={users[row.original.UserId]?.Name || undefined}
|
||||
component={Link}
|
||||
to={`/dashboard/users/profile?userId=${row.original.UserId}`}
|
||||
>
|
||||
<UserAvatar user={users[row.original.UserId]} />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
),
|
||||
enableResizing: false,
|
||||
visibleInShowHideMenu: activityView !== ActivityView.System,
|
||||
muiTableBodyCellProps: {
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'Name',
|
||||
header: globalize.translate('LabelName'),
|
||||
size: 270
|
||||
},
|
||||
{
|
||||
id: 'Overview',
|
||||
accessorFn: row => row.ShortOverview || row.Overview,
|
||||
header: globalize.translate('LabelOverview'),
|
||||
size: 170,
|
||||
Cell: ({ row }: { row: MRT_Row<ActivityLogEntry> }) => (
|
||||
<OverviewCell {...row.original} />
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: 'Type',
|
||||
header: globalize.translate('LabelType'),
|
||||
size: 150
|
||||
},
|
||||
{
|
||||
id: 'Actions',
|
||||
accessorFn: row => row.ItemId,
|
||||
header: '',
|
||||
size: 60,
|
||||
Cell: ({ row }: { row: MRT_Row<ActivityLogEntry> }) => (
|
||||
row.original.ItemId ? (
|
||||
<IconButton
|
||||
size='large'
|
||||
title={globalize.translate('LabelMediaDetails')}
|
||||
component={Link}
|
||||
to={`/details?id=${row.original.ItemId}`}
|
||||
>
|
||||
<PermMedia fontSize='inherit' />
|
||||
</IconButton>
|
||||
) : undefined
|
||||
),
|
||||
enableColumnActions: false,
|
||||
enableColumnFilter: false,
|
||||
enableResizing: false,
|
||||
enableSorting: false
|
||||
}
|
||||
];
|
||||
], [ activityView, users ]);
|
||||
|
||||
const onViewChange = useCallback((_e: React.MouseEvent<HTMLElement, MouseEvent>, newView: ActivityView | null) => {
|
||||
if (newView !== null) {
|
||||
|
@ -187,6 +185,58 @@ const Activity = () => {
|
|||
}
|
||||
}, [ activityView, searchParams, setSearchParams ]);
|
||||
|
||||
const table = useMaterialReactTable({
|
||||
columns,
|
||||
data: logEntries?.Items || [],
|
||||
|
||||
// Enable custom features
|
||||
enableColumnPinning: true,
|
||||
enableColumnResizing: true,
|
||||
|
||||
// Sticky header/footer
|
||||
enableStickyFooter: true,
|
||||
enableStickyHeader: true,
|
||||
muiTableContainerProps: {
|
||||
sx: {
|
||||
maxHeight: 'calc(100% - 7rem)' // 2 x 3.5rem for header and footer
|
||||
}
|
||||
},
|
||||
|
||||
// State
|
||||
state: {
|
||||
isLoading,
|
||||
pagination,
|
||||
columnVisibility: {
|
||||
User: activityView !== ActivityView.System
|
||||
}
|
||||
},
|
||||
|
||||
// Server pagination
|
||||
manualPagination: true,
|
||||
onPaginationChange: setPagination,
|
||||
rowCount: logEntries?.TotalRecordCount || 0,
|
||||
|
||||
// Custom toolbar contents
|
||||
renderTopToolbarCustomActions: () => (
|
||||
<ToggleButtonGroup
|
||||
size='small'
|
||||
value={activityView}
|
||||
onChange={onViewChange}
|
||||
exclusive
|
||||
>
|
||||
<ToggleButton value={ActivityView.All}>
|
||||
{globalize.translate('All')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ActivityView.User}>
|
||||
{globalize.translate('LabelUser')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ActivityView.System}>
|
||||
{globalize.translate('LabelSystem')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
)
|
||||
});
|
||||
|
||||
return (
|
||||
<Page
|
||||
id='serverActivityPage'
|
||||
|
@ -203,43 +253,14 @@ const Activity = () => {
|
|||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'baseline',
|
||||
marginY: 2
|
||||
marginBottom: 1
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography variant='h2'>
|
||||
{globalize.translate('HeaderActivity')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<ToggleButtonGroup
|
||||
value={activityView}
|
||||
onChange={onViewChange}
|
||||
exclusive
|
||||
>
|
||||
<ToggleButton value={ActivityView.All}>
|
||||
{globalize.translate('All')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ActivityView.User}>
|
||||
{globalize.translate('LabelUser')}
|
||||
</ToggleButton>
|
||||
<ToggleButton value={ActivityView.System}>
|
||||
{globalize.translate('LabelSystem')}
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
<Typography variant='h2'>
|
||||
{globalize.translate('HeaderActivity')}
|
||||
</Typography>
|
||||
</Box>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
rows={logEntries?.Items || []}
|
||||
pageSizeOptions={[ 10, 25, 50, 100 ]}
|
||||
paginationMode='server'
|
||||
paginationModel={paginationModel}
|
||||
onPaginationModelChange={setPaginationModel}
|
||||
rowCount={logEntries?.TotalRecordCount || 0}
|
||||
getRowId={getRowId}
|
||||
loading={isLoading}
|
||||
/>
|
||||
<MaterialReactTable table={table} />
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue