2023-06-27 01:31:50 -04:00
|
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
2023-06-04 02:34:40 -04:00
|
|
|
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 type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
2023-06-27 01:31:50 -04:00
|
|
|
import PermMedia from '@mui/icons-material/PermMedia';
|
2023-06-09 01:20:34 -04:00
|
|
|
import Box from '@mui/material/Box';
|
2023-06-10 02:31:48 -04:00
|
|
|
import IconButton from '@mui/material/IconButton';
|
2023-06-09 01:20:34 -04:00
|
|
|
import ToggleButton from '@mui/material/ToggleButton';
|
|
|
|
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
|
|
|
|
import Typography from '@mui/material/Typography';
|
2023-06-04 02:34:40 -04:00
|
|
|
import { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
2023-06-27 01:31:50 -04:00
|
|
|
import { Link, useSearchParams } from 'react-router-dom';
|
2023-06-04 02:34:40 -04:00
|
|
|
|
|
|
|
import Page from 'components/Page';
|
|
|
|
import UserAvatar from 'components/UserAvatar';
|
|
|
|
import { useApi } from 'hooks/useApi';
|
2023-06-10 02:31:48 -04:00
|
|
|
import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'scripts/datetime';
|
2023-06-04 02:34:40 -04:00
|
|
|
import globalize from 'scripts/globalize';
|
2023-06-09 01:20:34 -04:00
|
|
|
import { toBoolean } from 'utils/string';
|
2023-06-04 02:34:40 -04:00
|
|
|
|
2023-09-20 16:25:11 -04:00
|
|
|
import LogLevelChip from '../components/activityTable/LogLevelChip';
|
|
|
|
import OverviewCell from '../components/activityTable/OverviewCell';
|
|
|
|
import GridActionsCellLink from '../components/dataGrid/GridActionsCellLink';
|
2023-06-27 01:31:50 -04:00
|
|
|
|
2023-06-04 02:34:40 -04:00
|
|
|
const DEFAULT_PAGE_SIZE = 25;
|
2023-06-09 01:20:34 -04:00
|
|
|
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;
|
|
|
|
};
|
2023-06-04 02:34:40 -04:00
|
|
|
|
|
|
|
const getRowId = (row: ActivityLogEntry) => row.Id ?? -1;
|
|
|
|
|
|
|
|
const Activity = () => {
|
|
|
|
const { api } = useApi();
|
2023-06-09 01:20:34 -04:00
|
|
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
2023-06-04 02:34:40 -04:00
|
|
|
|
2023-06-10 02:31:48 -04:00
|
|
|
const [ activityView, setActivityView ] = useState(
|
|
|
|
getActivityView(searchParams.get(VIEW_PARAM)));
|
|
|
|
const [ isLoading, setIsLoading ] = useState(true);
|
|
|
|
const [ paginationModel, setPaginationModel ] = useState({
|
|
|
|
page: 0,
|
|
|
|
pageSize: DEFAULT_PAGE_SIZE
|
|
|
|
});
|
|
|
|
const [ rowCount, setRowCount ] = useState(0);
|
|
|
|
const [ rows, setRows ] = useState<ActivityLogEntry[]>([]);
|
|
|
|
const [ users, setUsers ] = useState<Record<string, UserDto>>({});
|
|
|
|
|
|
|
|
const userColDef: GridColDef[] = activityView !== ActivityView.System ? [
|
|
|
|
{
|
|
|
|
field: 'User',
|
|
|
|
headerName: globalize.translate('LabelUser'),
|
|
|
|
width: 60,
|
|
|
|
valueGetter: ({ row }) => users[row.UserId]?.Name,
|
|
|
|
renderCell: ({ row }) => (
|
2023-06-27 01:31:50 -04:00
|
|
|
<IconButton
|
|
|
|
size='large'
|
|
|
|
color='inherit'
|
|
|
|
sx={{ padding: 0 }}
|
|
|
|
title={users[row.UserId]?.Name ?? undefined}
|
|
|
|
component={Link}
|
2023-09-25 00:00:36 -04:00
|
|
|
to={`/dashboard/users/profile?userId=${row.UserId}`}
|
2023-06-27 01:31:50 -04:00
|
|
|
>
|
|
|
|
<UserAvatar user={users[row.UserId]} />
|
|
|
|
</IconButton>
|
2023-06-10 02:31:48 -04:00
|
|
|
)
|
|
|
|
}
|
|
|
|
] : [];
|
|
|
|
|
2023-06-04 02:34:40 -04:00
|
|
|
const columns: GridColDef[] = [
|
|
|
|
{
|
|
|
|
field: 'Date',
|
|
|
|
headerName: globalize.translate('LabelDate'),
|
2023-06-10 02:31:48 -04:00
|
|
|
width: 90,
|
|
|
|
type: 'date',
|
|
|
|
valueGetter: ({ value }) => parseISO8601Date(value),
|
|
|
|
valueFormatter: ({ value }) => toLocaleDateString(value)
|
|
|
|
},
|
|
|
|
{
|
|
|
|
field: 'Time',
|
|
|
|
headerName: globalize.translate('LabelTime'),
|
|
|
|
width: 100,
|
2023-06-04 02:34:40 -04:00
|
|
|
type: 'dateTime',
|
2023-06-10 02:31:48 -04:00
|
|
|
valueGetter: ({ row }) => parseISO8601Date(row.Date),
|
|
|
|
valueFormatter: ({ value }) => toLocaleTimeString(value)
|
2023-06-04 02:34:40 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
field: 'Severity',
|
|
|
|
headerName: globalize.translate('LabelLevel'),
|
|
|
|
width: 110,
|
2023-06-10 02:31:48 -04:00
|
|
|
renderCell: ({ value }) => (
|
|
|
|
value ? (
|
|
|
|
<LogLevelChip level={value} />
|
2023-06-04 02:34:40 -04:00
|
|
|
) : undefined
|
|
|
|
)
|
|
|
|
},
|
2023-06-10 02:31:48 -04:00
|
|
|
...userColDef,
|
2023-06-04 02:34:40 -04:00
|
|
|
{
|
|
|
|
field: 'Name',
|
|
|
|
headerName: globalize.translate('LabelName'),
|
2024-06-05 17:03:49 -04:00
|
|
|
width: 300
|
2023-06-04 02:34:40 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
field: 'Overview',
|
|
|
|
headerName: globalize.translate('LabelOverview'),
|
2023-06-27 01:31:50 -04:00
|
|
|
width: 200,
|
2023-06-10 02:31:48 -04:00
|
|
|
valueGetter: ({ row }) => row.ShortOverview ?? row.Overview,
|
|
|
|
renderCell: ({ row }) => (
|
|
|
|
<OverviewCell {...row} />
|
|
|
|
)
|
2023-06-04 02:34:40 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
field: 'Type',
|
|
|
|
headerName: globalize.translate('LabelType'),
|
2024-06-05 17:03:49 -04:00
|
|
|
width: 180
|
2023-06-27 01:31:50 -04:00
|
|
|
},
|
|
|
|
{
|
|
|
|
field: 'actions',
|
|
|
|
type: 'actions',
|
2024-06-05 17:03:49 -04:00
|
|
|
width: 50,
|
2023-06-27 01:31:50 -04:00
|
|
|
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;
|
|
|
|
}
|
2023-06-04 02:34:40 -04:00
|
|
|
}
|
|
|
|
];
|
|
|
|
|
2023-06-09 01:20:34 -04:00
|
|
|
const onViewChange = useCallback((_e, newView: ActivityView | null) => {
|
|
|
|
if (newView !== null) {
|
|
|
|
setActivityView(newView);
|
|
|
|
}
|
|
|
|
}, []);
|
|
|
|
|
2023-06-04 02:34:40 -04:00
|
|
|
useEffect(() => {
|
|
|
|
if (api) {
|
|
|
|
const fetchUsers = async () => {
|
|
|
|
const { data } = await getUserApi(api).getUsers();
|
|
|
|
const usersById: Record<string, UserDto> = {};
|
|
|
|
data.forEach(user => {
|
|
|
|
if (user.Id) {
|
|
|
|
usersById[user.Id] = user;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
setUsers(usersById);
|
|
|
|
};
|
|
|
|
|
|
|
|
fetchUsers()
|
|
|
|
.catch(err => {
|
|
|
|
console.error('[activity] failed to fetch users', err);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, [ api ]);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (api) {
|
|
|
|
const fetchActivity = async () => {
|
2023-06-09 01:20:34 -04:00
|
|
|
const params: {
|
|
|
|
startIndex: number,
|
|
|
|
limit: number,
|
|
|
|
hasUserId?: boolean
|
|
|
|
} = {
|
|
|
|
startIndex: paginationModel.page * paginationModel.pageSize,
|
|
|
|
limit: paginationModel.pageSize
|
|
|
|
};
|
|
|
|
if (activityView !== ActivityView.All) {
|
|
|
|
params.hasUserId = activityView === ActivityView.User;
|
|
|
|
}
|
|
|
|
|
2023-06-04 02:34:40 -04:00
|
|
|
const { data } = await getActivityLogApi(api)
|
2023-06-09 01:20:34 -04:00
|
|
|
.getLogEntries(params);
|
2023-06-04 02:34:40 -04:00
|
|
|
|
|
|
|
setRowCount(data.TotalRecordCount ?? 0);
|
|
|
|
setRows(data.Items ?? []);
|
|
|
|
setIsLoading(false);
|
|
|
|
};
|
|
|
|
|
2023-06-10 02:31:48 -04:00
|
|
|
setIsLoading(true);
|
2023-06-04 02:34:40 -04:00
|
|
|
fetchActivity()
|
|
|
|
.catch(err => {
|
|
|
|
console.error('[activity] failed to fetch activity log entries', err);
|
|
|
|
});
|
|
|
|
}
|
2023-06-09 01:20:34 -04:00
|
|
|
}, [ activityView, api, paginationModel ]);
|
|
|
|
|
|
|
|
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 ]);
|
2023-06-04 02:34:40 -04:00
|
|
|
|
|
|
|
return (
|
|
|
|
<Page
|
|
|
|
id='serverActivityPage'
|
|
|
|
title={globalize.translate('HeaderActivity')}
|
|
|
|
className='mainAnimatedPage type-interior'
|
|
|
|
>
|
|
|
|
<div className='content-primary'>
|
2023-06-09 01:20:34 -04:00
|
|
|
<Box
|
|
|
|
sx={{
|
|
|
|
display: 'flex',
|
|
|
|
alignItems: 'baseline',
|
|
|
|
marginY: 2
|
|
|
|
}}
|
|
|
|
>
|
|
|
|
<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>
|
|
|
|
</Box>
|
2023-06-04 02:34:40 -04:00
|
|
|
<DataGrid
|
|
|
|
columns={columns}
|
|
|
|
rows={rows}
|
|
|
|
pageSizeOptions={[ 10, 25, 50, 100 ]}
|
|
|
|
paginationMode='server'
|
|
|
|
paginationModel={paginationModel}
|
|
|
|
onPaginationModelChange={setPaginationModel}
|
|
|
|
rowCount={rowCount}
|
|
|
|
getRowId={getRowId}
|
|
|
|
loading={isLoading}
|
|
|
|
sx={{
|
|
|
|
minHeight: 500
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
</Page>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
export default Activity;
|