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'; 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 { 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, 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'; const DEFAULT_PAGE_SIZE = 25; const VIEW_PARAM = 'useractivity'; const enum ActivityView { All, User, System } const getActivityView = (param: string | null) => { if (param === null) return ActivityView.All; if (toBoolean(param)) return ActivityView.User; return ActivityView.System; }; const Activity = () => { const [ searchParams, setSearchParams ] = useSearchParams(); const [ activityView, setActivityView ] = useState( getActivityView(searchParams.get(VIEW_PARAM))); const [ pagination, setPagination ] = useState({ pageIndex: 0, pageSize: DEFAULT_PAGE_SIZE }); const { data: usersData, isLoading: isUsersLoading } = useUsers(); type UsersRecords = Record; const users: UsersRecords = useMemo(() => { if (!usersData) return {}; return usersData.reduce((acc, user) => { const userId = user.Id; if (!userId) return acc; return { ...acc, [userId]: user }; }, {}); }, [usersData]); const activityParams = useMemo(() => ({ startIndex: pagination.pageIndex * pagination.pageSize, limit: pagination.pageSize, hasUserId: activityView !== ActivityView.All ? activityView === ActivityView.User : undefined }), [activityView, pagination.pageIndex, pagination.pageSize]); const { data: logEntries, isLoading: isLogEntriesLoading } = useLogEntires(activityParams); const isLoading = isUsersLoading || isLogEntriesLoading; const columns = useMemo[]>(() => [ { id: 'Date', accessorFn: row => parseISO8601Date(row.Date), header: globalize.translate('LabelTime'), size: 160, Cell: ({ cell }) => toLocaleString(cell.getValue()) }, { accessorKey: 'Severity', header: globalize.translate('LabelLevel'), size: 90, Cell: ({ cell }: { cell: MRT_Cell }) => ( cell.getValue() ? ( ()} /> ) : undefined ), 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 }) => ( row.original.UserId ? ( ) : 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 }) => ( ) }, { accessorKey: 'Type', header: globalize.translate('LabelType'), size: 150 }, { id: 'Actions', accessorFn: row => row.ItemId, header: '', size: 60, Cell: ({ row }: { row: MRT_Row }) => ( row.original.ItemId ? ( ) : undefined ), enableColumnActions: false, enableColumnFilter: false, enableResizing: false, enableSorting: false } ], [ activityView, users ]); const onViewChange = useCallback((_e: React.MouseEvent, newView: ActivityView | null) => { if (newView !== null) { setActivityView(newView); } }, []); useEffect(() => { const currentViewParam = getActivityView(searchParams.get(VIEW_PARAM)); if (currentViewParam !== activityView) { if (activityView === ActivityView.All) { searchParams.delete(VIEW_PARAM); } else { searchParams.set(VIEW_PARAM, `${activityView === ActivityView.User}`); } setSearchParams(searchParams); } }, [ 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: () => ( {globalize.translate('All')} {globalize.translate('LabelUser')} {globalize.translate('LabelSystem')} ) }); return ( {globalize.translate('HeaderActivity')} ); }; export default Activity;