mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #4667 from thornbill/dashboard-activity
Dashboard activity
This commit is contained in:
commit
dc515335cc
19 changed files with 507 additions and 29 deletions
48
package-lock.json
generated
48
package-lock.json
generated
|
@ -21,6 +21,7 @@
|
||||||
"@loadable/component": "5.15.3",
|
"@loadable/component": "5.15.3",
|
||||||
"@mui/icons-material": "5.11.16",
|
"@mui/icons-material": "5.11.16",
|
||||||
"@mui/material": "5.13.3",
|
"@mui/material": "5.13.3",
|
||||||
|
"@mui/x-data-grid": "6.6.0",
|
||||||
"@react-hook/resize-observer": "1.2.6",
|
"@react-hook/resize-observer": "1.2.6",
|
||||||
"@tanstack/react-query": "4.29.12",
|
"@tanstack/react-query": "4.29.12",
|
||||||
"@tanstack/react-query-devtools": "4.29.12",
|
"@tanstack/react-query-devtools": "4.29.12",
|
||||||
|
@ -3337,6 +3338,31 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
|
||||||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@mui/x-data-grid": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-RCAdQM4D0RWLnFCtuv6pJEFqUtH3WmlTU2Av+p3Ir2Utw03FQzc81oOpOZNv/6SXv+rMa+i42pb6fW0U5Xz0AQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.21.0",
|
||||||
|
"@mui/utils": "^5.13.1",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"reselect": "^4.1.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@mui/material": "^5.4.1",
|
||||||
|
"@mui/system": "^5.4.1",
|
||||||
|
"react": "^17.0.0 || ^18.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
@ -14657,6 +14683,11 @@
|
||||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
|
||||||
|
},
|
||||||
"node_modules/resize-observer-polyfill": {
|
"node_modules/resize-observer-polyfill": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
|
@ -22686,6 +22717,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@mui/x-data-grid": {
|
||||||
|
"version": "6.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-6.6.0.tgz",
|
||||||
|
"integrity": "sha512-RCAdQM4D0RWLnFCtuv6pJEFqUtH3WmlTU2Av+p3Ir2Utw03FQzc81oOpOZNv/6SXv+rMa+i42pb6fW0U5Xz0AQ==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.21.0",
|
||||||
|
"@mui/utils": "^5.13.1",
|
||||||
|
"clsx": "^1.2.1",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"reselect": "^4.1.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@nodelib/fs.scandir": {
|
"@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
|
@ -31054,6 +31097,11 @@
|
||||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"reselect": {
|
||||||
|
"version": "4.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz",
|
||||||
|
"integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="
|
||||||
|
},
|
||||||
"resize-observer-polyfill": {
|
"resize-observer-polyfill": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
"@loadable/component": "5.15.3",
|
"@loadable/component": "5.15.3",
|
||||||
"@mui/icons-material": "5.11.16",
|
"@mui/icons-material": "5.11.16",
|
||||||
"@mui/material": "5.13.3",
|
"@mui/material": "5.13.3",
|
||||||
|
"@mui/x-data-grid": "6.6.0",
|
||||||
"@react-hook/resize-observer": "1.2.6",
|
"@react-hook/resize-observer": "1.2.6",
|
||||||
"@tanstack/react-query": "4.29.12",
|
"@tanstack/react-query": "4.29.12",
|
||||||
"@tanstack/react-query-devtools": "4.29.12",
|
"@tanstack/react-query-devtools": "4.29.12",
|
||||||
|
|
|
@ -36,6 +36,9 @@ const ExperimentalApp = () => {
|
||||||
|
|
||||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
{/* Redirects for old paths */}
|
||||||
|
<Route path='serveractivity.html' element={<Navigate replace to='/dashboard/activity' />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
import Avatar from '@mui/material/Avatar';
|
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import { useTheme } from '@mui/material/styles';
|
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
@ -8,10 +6,10 @@ import { useApi } from 'hooks/useApi';
|
||||||
import globalize from 'scripts/globalize';
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
import AppUserMenu, { ID } from './menus/AppUserMenu';
|
import AppUserMenu, { ID } from './menus/AppUserMenu';
|
||||||
|
import UserAvatar from 'components/UserAvatar';
|
||||||
|
|
||||||
const UserMenuButton = () => {
|
const UserMenuButton = () => {
|
||||||
const theme = useTheme();
|
const { user } = useApi();
|
||||||
const { api, user } = useApi();
|
|
||||||
|
|
||||||
const [ userMenuAnchorEl, setUserMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
const [ userMenuAnchorEl, setUserMenuAnchorEl ] = useState<null | HTMLElement>(null);
|
||||||
const isUserMenuOpen = Boolean(userMenuAnchorEl);
|
const isUserMenuOpen = Boolean(userMenuAnchorEl);
|
||||||
|
@ -37,18 +35,7 @@ const UserMenuButton = () => {
|
||||||
color='inherit'
|
color='inherit'
|
||||||
sx={{ padding: 0 }}
|
sx={{ padding: 0 }}
|
||||||
>
|
>
|
||||||
<Avatar
|
<UserAvatar user={user} />
|
||||||
alt={user?.Name || undefined}
|
|
||||||
src={
|
|
||||||
api && user?.Id ?
|
|
||||||
`${api.basePath}/Users/${user.Id}/Images/Primary?tag=${user.PrimaryImageTag}` :
|
|
||||||
undefined
|
|
||||||
}
|
|
||||||
sx={{
|
|
||||||
bgcolor: theme.palette.primary.dark,
|
|
||||||
color: 'inherit'
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
17
src/apps/experimental/components/GridActionsCellLink.tsx
Normal file
17
src/apps/experimental/components/GridActionsCellLink.tsx
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import React, { type RefAttributes } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { GridActionsCellItem, type GridActionsCellItemProps } from '@mui/x-data-grid';
|
||||||
|
|
||||||
|
type GridActionsCellLinkProps = { to: string } & GridActionsCellItemProps & RefAttributes<HTMLButtonElement>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link component to use in mui's data-grid action column due to a current bug with passing props to custom link components.
|
||||||
|
* @see https://github.com/mui/mui-x/issues/4654
|
||||||
|
*/
|
||||||
|
const GridActionsCellLink = ({ to, ...props }: GridActionsCellLinkProps) => (
|
||||||
|
<Link to={to}>
|
||||||
|
<GridActionsCellItem {...props} />
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default GridActionsCellLink;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level';
|
||||||
|
import Chip from '@mui/material/Chip';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
|
||||||
|
const LogLevelChip = ({ level }: { level: LogLevel }) => {
|
||||||
|
let color: 'info' | 'warning' | 'error' | undefined = undefined;
|
||||||
|
switch (level) {
|
||||||
|
case LogLevel.Information:
|
||||||
|
color = 'info';
|
||||||
|
break;
|
||||||
|
case LogLevel.Warning:
|
||||||
|
color = 'warning';
|
||||||
|
break;
|
||||||
|
case LogLevel.Error:
|
||||||
|
case LogLevel.Critical:
|
||||||
|
color = 'error';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const levelText = globalize.translate(`LogLevel.${level}`);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Chip
|
||||||
|
size='small'
|
||||||
|
color={color}
|
||||||
|
label={levelText}
|
||||||
|
title={levelText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default LogLevelChip;
|
|
@ -0,0 +1,64 @@
|
||||||
|
import type { ActivityLogEntry } from '@jellyfin/sdk/lib/generated-client/models/activity-log-entry';
|
||||||
|
import Info from '@mui/icons-material/Info';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import ClickAwayListener from '@mui/material/ClickAwayListener';
|
||||||
|
import IconButton from '@mui/material/IconButton';
|
||||||
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
|
import React, { FC, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
const OverviewCell: FC<ActivityLogEntry> = ({ Overview, ShortOverview }) => {
|
||||||
|
const displayValue = ShortOverview ?? Overview;
|
||||||
|
const [ open, setOpen ] = useState(false);
|
||||||
|
|
||||||
|
const onTooltipClose = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onTooltipOpen = useCallback(() => {
|
||||||
|
setOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!displayValue) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis'
|
||||||
|
}}
|
||||||
|
component='div'
|
||||||
|
title={displayValue}
|
||||||
|
>
|
||||||
|
{displayValue}
|
||||||
|
</Box>
|
||||||
|
{ShortOverview && Overview && (
|
||||||
|
<ClickAwayListener onClickAway={onTooltipClose}>
|
||||||
|
<Tooltip
|
||||||
|
title={Overview}
|
||||||
|
placement='top'
|
||||||
|
arrow
|
||||||
|
onClose={onTooltipClose}
|
||||||
|
open={open}
|
||||||
|
disableFocusListener
|
||||||
|
disableHoverListener
|
||||||
|
disableTouchListener
|
||||||
|
>
|
||||||
|
<IconButton onClick={onTooltipOpen}>
|
||||||
|
<Info />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ClickAwayListener>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OverviewCell;
|
|
@ -40,7 +40,7 @@ const DevicesDrawerSection = () => {
|
||||||
</ListItemLink>
|
</ListItemLink>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem disablePadding>
|
<ListItem disablePadding>
|
||||||
<ListItemLink to='/serveractivity.html'>
|
<ListItemLink to='/dashboard/activity'>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Analytics />
|
<Analytics />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { AsyncRoute } from '../../../../components/router/AsyncRoute';
|
import { AsyncRoute, AsyncRouteType } from 'components/router/AsyncRoute';
|
||||||
|
|
||||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||||
|
{ path: 'dashboard/activity', page: 'dashboard/activity', type: AsyncRouteType.Experimental },
|
||||||
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
|
{ path: 'notificationsettings.html', page: 'dashboard/notifications' },
|
||||||
{ path: 'usernew.html', page: 'user/usernew' },
|
{ path: 'usernew.html', page: 'user/usernew' },
|
||||||
{ path: 'userprofiles.html', page: 'user/userprofiles' },
|
{ path: 'userprofiles.html', page: 'user/userprofiles' },
|
||||||
|
|
273
src/apps/experimental/routes/dashboard/activity.tsx
Normal file
273
src/apps/experimental/routes/dashboard/activity.tsx
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
import React, { useCallback, useEffect, 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 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 { DataGrid, type GridColDef } from '@mui/x-data-grid';
|
||||||
|
import { Link, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Page from 'components/Page';
|
||||||
|
import UserAvatar from 'components/UserAvatar';
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'scripts/datetime';
|
||||||
|
import globalize from 'scripts/globalize';
|
||||||
|
import { toBoolean } from 'utils/string';
|
||||||
|
|
||||||
|
import LogLevelChip from '../../components/activityTable/LogLevelChip';
|
||||||
|
import OverviewCell from '../../components/activityTable/OverviewCell';
|
||||||
|
import GridActionsCellLink from '../../components/GridActionsCellLink';
|
||||||
|
|
||||||
|
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 getRowId = (row: ActivityLogEntry) => row.Id ?? -1;
|
||||||
|
|
||||||
|
const Activity = () => {
|
||||||
|
const { api } = useApi();
|
||||||
|
const [ searchParams, setSearchParams ] = useSearchParams();
|
||||||
|
|
||||||
|
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 }) => (
|
||||||
|
<IconButton
|
||||||
|
size='large'
|
||||||
|
color='inherit'
|
||||||
|
sx={{ padding: 0 }}
|
||||||
|
title={users[row.UserId]?.Name ?? undefined}
|
||||||
|
component={Link}
|
||||||
|
to={`/useredit.html?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)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'Time',
|
||||||
|
headerName: globalize.translate('LabelTime'),
|
||||||
|
width: 100,
|
||||||
|
type: 'dateTime',
|
||||||
|
valueGetter: ({ row }) => parseISO8601Date(row.Date),
|
||||||
|
valueFormatter: ({ value }) => toLocaleTimeString(value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'Severity',
|
||||||
|
headerName: globalize.translate('LabelLevel'),
|
||||||
|
width: 110,
|
||||||
|
renderCell: ({ value }) => (
|
||||||
|
value ? (
|
||||||
|
<LogLevelChip level={value} />
|
||||||
|
) : undefined
|
||||||
|
)
|
||||||
|
},
|
||||||
|
...userColDef,
|
||||||
|
{
|
||||||
|
field: 'Name',
|
||||||
|
headerName: globalize.translate('LabelName'),
|
||||||
|
width: 200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'Overview',
|
||||||
|
headerName: globalize.translate('LabelOverview'),
|
||||||
|
width: 200,
|
||||||
|
valueGetter: ({ row }) => row.ShortOverview ?? row.Overview,
|
||||||
|
renderCell: ({ row }) => (
|
||||||
|
<OverviewCell {...row} />
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'Type',
|
||||||
|
headerName: globalize.translate('LabelType'),
|
||||||
|
width: 120
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'actions',
|
||||||
|
type: 'actions',
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const onViewChange = useCallback((_e, newView: ActivityView | null) => {
|
||||||
|
if (newView !== null) {
|
||||||
|
setActivityView(newView);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
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 () => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await getActivityLogApi(api)
|
||||||
|
.getLogEntries(params);
|
||||||
|
|
||||||
|
setRowCount(data.TotalRecordCount ?? 0);
|
||||||
|
setRows(data.Items ?? []);
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
fetchActivity()
|
||||||
|
.catch(err => {
|
||||||
|
console.error('[activity] failed to fetch activity log entries', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [ 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 ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
id='serverActivityPage'
|
||||||
|
title={globalize.translate('HeaderActivity')}
|
||||||
|
className='mainAnimatedPage type-interior'
|
||||||
|
>
|
||||||
|
<div className='content-primary'>
|
||||||
|
<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>
|
||||||
|
<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;
|
|
@ -163,12 +163,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||||
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
controller: 'dashboard/scheduledtasks/scheduledtasks',
|
||||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||||
}
|
}
|
||||||
}, {
|
|
||||||
path: 'serveractivity.html',
|
|
||||||
pageProps: {
|
|
||||||
controller: 'dashboard/serveractivity',
|
|
||||||
view: 'dashboard/serveractivity.html'
|
|
||||||
}
|
|
||||||
}, {
|
}, {
|
||||||
path: 'apikeys.html',
|
path: 'apikeys.html',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
|
|
|
@ -21,6 +21,15 @@ const theme = createTheme({
|
||||||
fontFamily: '"Noto Sans", sans-serif',
|
fontFamily: '"Noto Sans", sans-serif',
|
||||||
button: {
|
button: {
|
||||||
textTransform: 'none'
|
textTransform: 'none'
|
||||||
|
},
|
||||||
|
h1: {
|
||||||
|
fontSize: '1.8rem'
|
||||||
|
},
|
||||||
|
h2: {
|
||||||
|
fontSize: '1.5rem'
|
||||||
|
},
|
||||||
|
h3: {
|
||||||
|
fontSize: '1.17rem'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
|
|
|
@ -51,6 +51,9 @@ const StableApp = () => (
|
||||||
|
|
||||||
{/* Suppress warnings for unhandled routes */}
|
{/* Suppress warnings for unhandled routes */}
|
||||||
<Route path='*' element={null} />
|
<Route path='*' element={null} />
|
||||||
|
|
||||||
|
{/* Redirects for old paths */}
|
||||||
|
<Route path='/serveractivity.html' element={<Navigate replace to='/dashboard/activity' />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
|
|
|
@ -164,7 +164,7 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [
|
||||||
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
view: 'dashboard/scheduledtasks/scheduledtasks.html'
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
path: 'serveractivity.html',
|
path: 'dashboard/activity',
|
||||||
pageProps: {
|
pageProps: {
|
||||||
controller: 'dashboard/serveractivity',
|
controller: 'dashboard/serveractivity',
|
||||||
view: 'dashboard/serveractivity.html'
|
view: 'dashboard/serveractivity.html'
|
||||||
|
|
32
src/components/UserAvatar.tsx
Normal file
32
src/components/UserAvatar.tsx
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import React, { FC } from 'react';
|
||||||
|
import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto';
|
||||||
|
import Avatar from '@mui/material/Avatar';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
|
import { useApi } from 'hooks/useApi';
|
||||||
|
|
||||||
|
interface UserAvatarProps {
|
||||||
|
user?: UserDto
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserAvatar: FC<UserAvatarProps> = ({ user }) => {
|
||||||
|
const { api } = useApi();
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return user ? (
|
||||||
|
<Avatar
|
||||||
|
alt={user.Name ?? undefined}
|
||||||
|
src={
|
||||||
|
api && user.Id && user.PrimaryImageTag ?
|
||||||
|
`${api.basePath}/Users/${user.Id}/Images/Primary?tag=${user.PrimaryImageTag}` :
|
||||||
|
undefined
|
||||||
|
}
|
||||||
|
sx={{
|
||||||
|
bgcolor: theme.palette.primary.dark,
|
||||||
|
color: 'inherit'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserAvatar;
|
|
@ -44,7 +44,7 @@
|
||||||
|
|
||||||
<div class="dashboardColumn dashboardColumn-2-40 dashboardColumn-3-27">
|
<div class="dashboardColumn dashboardColumn-2-40 dashboardColumn-3-27">
|
||||||
<div class="dashboardSection">
|
<div class="dashboardSection">
|
||||||
<a is="emby-linkbutton" href="#/serveractivity.html?useractivity=true" class="button-flat sectionTitleTextButton">
|
<a is="emby-linkbutton" href="#/dashboard/activity?useractivity=true" class="button-flat sectionTitleTextButton">
|
||||||
<h3>${HeaderActivity}</h3>
|
<h3>${HeaderActivity}</h3>
|
||||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -61,7 +61,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboardSection serverActivitySection hide activityContainer">
|
<div class="dashboardSection serverActivitySection hide activityContainer">
|
||||||
<a is="emby-linkbutton" href="#/serveractivity.html?useractivity=false" class="button-flat sectionTitleTextButton">
|
<a is="emby-linkbutton" href="#/dashboard/activity?useractivity=false" class="button-flat sectionTitleTextButton">
|
||||||
<h3>${Alerts}</h3>
|
<h3>${Alerts}</h3>
|
||||||
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
<span class="material-icons chevron_right" aria-hidden="true"></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -466,7 +466,7 @@ function createToolsMenuList(pluginItems) {
|
||||||
});
|
});
|
||||||
links.push({
|
links.push({
|
||||||
name: globalize.translate('HeaderActivity'),
|
name: globalize.translate('HeaderActivity'),
|
||||||
href: '#/serveractivity.html',
|
href: '#/dashboard/activity',
|
||||||
pageIds: ['serverActivityPage'],
|
pageIds: ['serverActivityPage'],
|
||||||
icon: 'assessment'
|
icon: 'assessment'
|
||||||
});
|
});
|
||||||
|
|
|
@ -603,6 +603,7 @@
|
||||||
"LabelCustomDeviceDisplayNameHelp": "Supply a custom display name or leave empty to use the name reported by the device.",
|
"LabelCustomDeviceDisplayNameHelp": "Supply a custom display name or leave empty to use the name reported by the device.",
|
||||||
"LabelCustomRating": "Custom rating",
|
"LabelCustomRating": "Custom rating",
|
||||||
"LabelDashboardTheme": "Server Dashboard theme",
|
"LabelDashboardTheme": "Server Dashboard theme",
|
||||||
|
"LabelDate": "Date",
|
||||||
"LabelDateAdded": "Date added",
|
"LabelDateAdded": "Date added",
|
||||||
"LabelDateAddedBehavior": "Date added behavior for new content",
|
"LabelDateAddedBehavior": "Date added behavior for new content",
|
||||||
"LabelDateAddedBehaviorHelp": "If a metadata value is present, it will always be used before either of these options.",
|
"LabelDateAddedBehaviorHelp": "If a metadata value is present, it will always be used before either of these options.",
|
||||||
|
@ -725,11 +726,13 @@
|
||||||
"LabelKodiMetadataUserHelp": "Save watch data to NFO files for other applications to use.",
|
"LabelKodiMetadataUserHelp": "Save watch data to NFO files for other applications to use.",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanNetworks": "LAN networks",
|
"LabelLanNetworks": "LAN networks",
|
||||||
|
"LabelLevel": "Level",
|
||||||
"LabelLibraryPageSize": "Library page size",
|
"LabelLibraryPageSize": "Library page size",
|
||||||
"LabelLibraryPageSizeHelp": "Set the amount of items to show on a library page. Set to 0 in order to disable paging.",
|
"LabelLibraryPageSizeHelp": "Set the amount of items to show on a library page. Set to 0 in order to disable paging.",
|
||||||
"LabelMaxDaysForNextUp": "Max days in 'Next Up'",
|
"LabelMaxDaysForNextUp": "Max days in 'Next Up'",
|
||||||
"LabelMaxDaysForNextUpHelp": "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.",
|
"LabelMaxDaysForNextUpHelp": "Set the maximum amount of days a show should stay in the 'Next Up' list without watching it.",
|
||||||
"LabelMaxVideoResolution": "Maximum Allowed Video Transcoding Resolution",
|
"LabelMaxVideoResolution": "Maximum Allowed Video Transcoding Resolution",
|
||||||
|
"LabelMediaDetails": "Media details",
|
||||||
"LabelLineup": "Lineup",
|
"LabelLineup": "Lineup",
|
||||||
"LabelLocalCustomCss": "Custom CSS code for styling which applies to this client only. You may want to disable server custom CSS code.",
|
"LabelLocalCustomCss": "Custom CSS code for styling which applies to this client only. You may want to disable server custom CSS code.",
|
||||||
"LabelLocalHttpServerPortNumber": "Local HTTP port number",
|
"LabelLocalHttpServerPortNumber": "Local HTTP port number",
|
||||||
|
@ -931,6 +934,7 @@
|
||||||
"LabelSyncPlaySettingsSpeedToSyncHelp": "Sync correction method that consist in speeding up the playback. Sync Correction must be enabled.",
|
"LabelSyncPlaySettingsSpeedToSyncHelp": "Sync correction method that consist in speeding up the playback. Sync Correction must be enabled.",
|
||||||
"LabelSyncPlaySettingsSkipToSync": "Enable SkipToSync",
|
"LabelSyncPlaySettingsSkipToSync": "Enable SkipToSync",
|
||||||
"LabelSyncPlaySettingsSkipToSyncHelp": "Sync correction method that consist in seeking to the estimated position. Sync Correction must be enabled.",
|
"LabelSyncPlaySettingsSkipToSyncHelp": "Sync correction method that consist in seeking to the estimated position. Sync Correction must be enabled.",
|
||||||
|
"LabelSystem": "System",
|
||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTagline": "Tagline",
|
"LabelTagline": "Tagline",
|
||||||
"LabelTextBackgroundColor": "Text background color",
|
"LabelTextBackgroundColor": "Text background color",
|
||||||
|
@ -1009,6 +1013,13 @@
|
||||||
"LiveBroadcasts": "Live broadcasts",
|
"LiveBroadcasts": "Live broadcasts",
|
||||||
"LiveTV": "Live TV",
|
"LiveTV": "Live TV",
|
||||||
"Localization": "Localization",
|
"Localization": "Localization",
|
||||||
|
"LogLevel.Trace": "Trace",
|
||||||
|
"LogLevel.Debug": "Debug",
|
||||||
|
"LogLevel.Information": "Information",
|
||||||
|
"LogLevel.Warning": "Warning",
|
||||||
|
"LogLevel.Error": "Error",
|
||||||
|
"LogLevel.Critical": "Critical",
|
||||||
|
"LogLevel.None": "None",
|
||||||
"Logo": "Logo",
|
"Logo": "Logo",
|
||||||
"Lyricist": "Lyricist",
|
"Lyricist": "Lyricist",
|
||||||
"ManageLibrary": "Manage library",
|
"ManageLibrary": "Manage library",
|
||||||
|
|
|
@ -40,7 +40,8 @@ const config = {
|
||||||
'@mui/private-theming': '@mui/private-theming/legacy',
|
'@mui/private-theming': '@mui/private-theming/legacy',
|
||||||
'@mui/styled-engine': '@mui/styled-engine/legacy',
|
'@mui/styled-engine': '@mui/styled-engine/legacy',
|
||||||
'@mui/system': '@mui/system/legacy',
|
'@mui/system': '@mui/system/legacy',
|
||||||
'@mui/utils': '@mui/utils/legacy'
|
'@mui/utils': '@mui/utils/legacy',
|
||||||
|
'@mui/x-data-grid': '@mui/x-data-grid/legacy'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue