From 3bad9f03782a271eeaf64b579e3b7d4dd35d0906 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sun, 4 Jun 2023 02:34:40 -0400 Subject: [PATCH 1/7] Rewrite activity log page in dashboard --- package-lock.json | 48 +++++ package.json | 1 + .../components/AppToolbar/UserMenuButton.tsx | 19 +- .../experimental/routes/asyncRoutes/admin.ts | 1 + src/apps/stable/routes/asyncRoutes/admin.ts | 1 + src/apps/stable/routes/dashboard/activity.tsx | 175 ++++++++++++++++++ src/components/UserAvatar.tsx | 34 ++++ src/strings/en-us.json | 9 + 8 files changed, 272 insertions(+), 16 deletions(-) create mode 100644 src/apps/stable/routes/dashboard/activity.tsx create mode 100644 src/components/UserAvatar.tsx diff --git a/package-lock.json b/package-lock.json index 64c7e6147c..60da03f2df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@loadable/component": "5.15.3", "@mui/icons-material": "5.11.16", "@mui/material": "5.13.3", + "@mui/x-data-grid": "6.6.0", "blurhash": "2.0.5", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "classnames": "2.3.2", @@ -3329,6 +3330,31 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "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": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -14521,6 +14547,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "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": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", @@ -22526,6 +22557,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": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -30822,6 +30865,11 @@ "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "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": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", diff --git a/package.json b/package.json index 0a3d6f7e62..a9f2750834 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "@loadable/component": "5.15.3", "@mui/icons-material": "5.11.16", "@mui/material": "5.13.3", + "@mui/x-data-grid": "6.6.0", "blurhash": "2.0.5", "classlist.js": "https://github.com/eligrey/classList.js/archive/1.2.20180112.tar.gz", "classnames": "2.3.2", diff --git a/src/apps/experimental/components/AppToolbar/UserMenuButton.tsx b/src/apps/experimental/components/AppToolbar/UserMenuButton.tsx index 85db4d1f29..89f18669e9 100644 --- a/src/apps/experimental/components/AppToolbar/UserMenuButton.tsx +++ b/src/apps/experimental/components/AppToolbar/UserMenuButton.tsx @@ -1,6 +1,4 @@ -import Avatar from '@mui/material/Avatar'; import IconButton from '@mui/material/IconButton'; -import { useTheme } from '@mui/material/styles'; import Tooltip from '@mui/material/Tooltip'; import React, { useCallback, useState } from 'react'; @@ -8,10 +6,10 @@ import { useApi } from 'hooks/useApi'; import globalize from 'scripts/globalize'; import AppUserMenu, { ID } from './menus/AppUserMenu'; +import UserAvatar from 'components/UserAvatar'; const UserMenuButton = () => { - const theme = useTheme(); - const { api, user } = useApi(); + const { user } = useApi(); const [ userMenuAnchorEl, setUserMenuAnchorEl ] = useState(null); const isUserMenuOpen = Boolean(userMenuAnchorEl); @@ -37,18 +35,7 @@ const UserMenuButton = () => { color='inherit' sx={{ padding: 0 }} > - + diff --git a/src/apps/experimental/routes/asyncRoutes/admin.ts b/src/apps/experimental/routes/asyncRoutes/admin.ts index 72bcc6f32b..b06c8cac8c 100644 --- a/src/apps/experimental/routes/asyncRoutes/admin.ts +++ b/src/apps/experimental/routes/asyncRoutes/admin.ts @@ -1,6 +1,7 @@ import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ + { path: 'dashboard/activity', page: 'dashboard/activity' }, { path: 'notificationsettings.html', page: 'dashboard/notifications' }, { path: 'usernew.html', page: 'user/usernew' }, { path: 'userprofiles.html', page: 'user/userprofiles' }, diff --git a/src/apps/stable/routes/asyncRoutes/admin.ts b/src/apps/stable/routes/asyncRoutes/admin.ts index 72bcc6f32b..b06c8cac8c 100644 --- a/src/apps/stable/routes/asyncRoutes/admin.ts +++ b/src/apps/stable/routes/asyncRoutes/admin.ts @@ -1,6 +1,7 @@ import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ + { path: 'dashboard/activity', page: 'dashboard/activity' }, { path: 'notificationsettings.html', page: 'dashboard/notifications' }, { path: 'usernew.html', page: 'user/usernew' }, { path: 'userprofiles.html', page: 'user/userprofiles' }, diff --git a/src/apps/stable/routes/dashboard/activity.tsx b/src/apps/stable/routes/dashboard/activity.tsx new file mode 100644 index 0000000000..8d8bea9789 --- /dev/null +++ b/src/apps/stable/routes/dashboard/activity.tsx @@ -0,0 +1,175 @@ +import React, { 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 { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level'; +import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; +import Chip from '@mui/material/Chip'; +import { DataGrid, type GridColDef } from '@mui/x-data-grid'; + +import Page from 'components/Page'; +import UserAvatar from 'components/UserAvatar'; +import { useApi } from 'hooks/useApi'; +import globalize from 'scripts/globalize'; + +const DEFAULT_PAGE_SIZE = 25; + +const getRowId = (row: ActivityLogEntry) => row.Id ?? -1; + +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 ( + + ); +}; + +const Activity = () => { + const { api } = useApi(); + + const columns: GridColDef[] = [ + { + field: 'Date', + headerName: globalize.translate('LabelDate'), + width: 180, + type: 'dateTime', + valueGetter: ({ row }) => new Date(row.Date) + }, + { + field: 'Severity', + headerName: globalize.translate('LabelLevel'), + width: 110, + renderCell: ({ row }) => ( + row.Severity ? ( + + ) : undefined + ) + }, + { + field: 'User', + headerName: globalize.translate('LabelUser'), + width: 60, + valueGetter: ({ row }) => users[row.UserId]?.Name, + renderCell: ({ row }) => ( + + ) + }, + { + field: 'Name', + headerName: globalize.translate('LabelName'), + width: 200 + }, + { + field: 'Overview', + headerName: globalize.translate('LabelOverview'), + width: 200, + valueGetter: ({ row }) => row.Overview ?? row.ShortOverview + }, + { + field: 'Type', + headerName: globalize.translate('LabelType'), + width: 150 + } + ]; + + const [ isLoading, setIsLoading ] = useState(true); + const [paginationModel, setPaginationModel] = useState({ + page: 0, + pageSize: DEFAULT_PAGE_SIZE + }); + const [ rowCount, setRowCount ] = useState(0); + const [ rows, setRows ] = useState([]); + const [ users, setUsers ] = useState>({}); + + useEffect(() => { + if (api) { + const fetchUsers = async () => { + const { data } = await getUserApi(api).getUsers(); + const usersById: Record = {}; + 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 { data } = await getActivityLogApi(api) + .getLogEntries({ + startIndex: paginationModel.page * paginationModel.pageSize, + limit: paginationModel.pageSize + }); + + setRowCount(data.TotalRecordCount ?? 0); + setRows(data.Items ?? []); + setIsLoading(false); + }; + + fetchActivity() + .catch(err => { + console.error('[activity] failed to fetch activity log entries', err); + }); + } + }, [ api, paginationModel ]); + + return ( + +
+

{globalize.translate('HeaderActivity')}

+ +
+
+ ); +}; + +export default Activity; diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx new file mode 100644 index 0000000000..0f17802805 --- /dev/null +++ b/src/components/UserAvatar.tsx @@ -0,0 +1,34 @@ +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 + showTitle?: boolean +} + +const UserAvatar: FC = ({ user, showTitle = false }) => { + const { api } = useApi(); + const theme = useTheme(); + + return user ? ( + + ) : null; +}; + +export default UserAvatar; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 5cc2db0ca6..c425050958 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -602,6 +602,7 @@ "LabelCustomDeviceDisplayNameHelp": "Supply a custom display name or leave empty to use the name reported by the device.", "LabelCustomRating": "Custom rating", "LabelDashboardTheme": "Server Dashboard theme", + "LabelDate": "Date", "LabelDateAdded": "Date added", "LabelDateAddedBehavior": "Date added behavior for new content", "LabelDateAddedBehaviorHelp": "If a metadata value is present, it will always be used before either of these options.", @@ -723,6 +724,7 @@ "LabelKodiMetadataUserHelp": "Save watch data to NFO files for other applications to use.", "LabelLanguage": "Language", "LabelLanNetworks": "LAN networks", + "LabelLevel": "Level", "LabelLibraryPageSize": "Library page size", "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'", @@ -1006,6 +1008,13 @@ "LiveBroadcasts": "Live broadcasts", "LiveTV": "Live TV", "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", "Lyricist": "Lyricist", "ManageLibrary": "Manage library", From e3f605ef2d8a6ba468c1a972a8d117fb2d33fdfc Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Thu, 8 Jun 2023 10:46:01 -0400 Subject: [PATCH 2/7] Use legacy build of mui data grid --- webpack.common.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webpack.common.js b/webpack.common.js index e1448db74f..85d9a63d95 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -40,7 +40,8 @@ const config = { '@mui/private-theming': '@mui/private-theming/legacy', '@mui/styled-engine': '@mui/styled-engine/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: [ From 3038ddb6ff03aae74b5df7de8e988701979c35b8 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Fri, 9 Jun 2023 01:20:34 -0400 Subject: [PATCH 3/7] Add user/system event filtering --- src/apps/experimental/theme.ts | 9 ++ src/apps/stable/routes/dashboard/activity.tsx | 92 +++++++++++++++++-- src/strings/en-us.json | 1 + 3 files changed, 94 insertions(+), 8 deletions(-) diff --git a/src/apps/experimental/theme.ts b/src/apps/experimental/theme.ts index 7def6bd4b7..d3f84366be 100644 --- a/src/apps/experimental/theme.ts +++ b/src/apps/experimental/theme.ts @@ -21,6 +21,15 @@ const theme = createTheme({ fontFamily: '"Noto Sans", sans-serif', button: { textTransform: 'none' + }, + h1: { + fontSize: '1.8rem' + }, + h2: { + fontSize: '1.5rem' + }, + h3: { + fontSize: '1.17rem' } }, components: { diff --git a/src/apps/stable/routes/dashboard/activity.tsx b/src/apps/stable/routes/dashboard/activity.tsx index 8d8bea9789..297a47aecf 100644 --- a/src/apps/stable/routes/dashboard/activity.tsx +++ b/src/apps/stable/routes/dashboard/activity.tsx @@ -1,18 +1,37 @@ -import React, { useEffect, useState } from 'react'; +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 { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level'; import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; +import Box from '@mui/material/Box'; import Chip from '@mui/material/Chip'; +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 { useSearchParams } from 'react-router-dom'; import Page from 'components/Page'; import UserAvatar from 'components/UserAvatar'; import { useApi } from 'hooks/useApi'; import globalize from 'scripts/globalize'; +import { toBoolean } from 'utils/string'; 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; @@ -45,6 +64,7 @@ const LogLevelChip = ({ level }: { level: LogLevel }) => { const Activity = () => { const { api } = useApi(); + const [ searchParams, setSearchParams ] = useSearchParams(); const columns: GridColDef[] = [ { @@ -94,8 +114,10 @@ const Activity = () => { } ]; + const [ activityView, setActivityView ] = useState( + getActivityView(searchParams.get(VIEW_PARAM))); const [ isLoading, setIsLoading ] = useState(true); - const [paginationModel, setPaginationModel] = useState({ + const [ paginationModel, setPaginationModel ] = useState({ page: 0, pageSize: DEFAULT_PAGE_SIZE }); @@ -103,6 +125,12 @@ const Activity = () => { const [ rows, setRows ] = useState([]); const [ users, setUsers ] = useState>({}); + const onViewChange = useCallback((_e, newView: ActivityView | null) => { + if (newView !== null) { + setActivityView(newView); + } + }, []); + useEffect(() => { if (api) { const fetchUsers = async () => { @@ -127,11 +155,20 @@ const Activity = () => { 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({ - startIndex: paginationModel.page * paginationModel.pageSize, - limit: paginationModel.pageSize - }); + .getLogEntries(params); setRowCount(data.TotalRecordCount ?? 0); setRows(data.Items ?? []); @@ -143,7 +180,19 @@ const Activity = () => { console.error('[activity] failed to fetch activity log entries', err); }); } - }, [ api, paginationModel ]); + }, [ 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 ( { className='mainAnimatedPage type-interior' >
-

{globalize.translate('HeaderActivity')}

+ + + + {globalize.translate('HeaderActivity')} + + + + + {globalize.translate('All')} + + + {globalize.translate('LabelUser')} + + + {globalize.translate('LabelSystem')} + + + Date: Fri, 9 Jun 2023 03:01:27 -0400 Subject: [PATCH 4/7] Update paths and stable app view --- src/apps/experimental/App.tsx | 3 +++ .../components/drawers/dashboard/DevicesDrawerSection.tsx | 2 +- src/apps/experimental/routes/legacyRoutes/admin.ts | 6 ------ src/apps/stable/App.tsx | 3 +++ src/apps/stable/routes/asyncRoutes/admin.ts | 1 - src/apps/stable/routes/legacyRoutes/admin.ts | 2 +- src/controllers/dashboard/dashboard.html | 4 ++-- src/scripts/libraryMenu.js | 2 +- 8 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx index 0013ef0430..0c0778b74e 100644 --- a/src/apps/experimental/App.tsx +++ b/src/apps/experimental/App.tsx @@ -36,6 +36,9 @@ const ExperimentalApp = () => { {LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)} + + {/* Redirects for old paths */} + } /> ); diff --git a/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx b/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx index ae1e7d4b85..cb3cbf33ce 100644 --- a/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx +++ b/src/apps/experimental/components/drawers/dashboard/DevicesDrawerSection.tsx @@ -40,7 +40,7 @@ const DevicesDrawerSection = () => { - + diff --git a/src/apps/experimental/routes/legacyRoutes/admin.ts b/src/apps/experimental/routes/legacyRoutes/admin.ts index e2037ddb11..b53f86ce87 100644 --- a/src/apps/experimental/routes/legacyRoutes/admin.ts +++ b/src/apps/experimental/routes/legacyRoutes/admin.ts @@ -163,12 +163,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'dashboard/scheduledtasks/scheduledtasks', view: 'dashboard/scheduledtasks/scheduledtasks.html' } - }, { - path: 'serveractivity.html', - pageProps: { - controller: 'dashboard/serveractivity', - view: 'dashboard/serveractivity.html' - } }, { path: 'apikeys.html', pageProps: { diff --git a/src/apps/stable/App.tsx b/src/apps/stable/App.tsx index bbaec1b922..73c73f5881 100644 --- a/src/apps/stable/App.tsx +++ b/src/apps/stable/App.tsx @@ -51,6 +51,9 @@ const StableApp = () => ( {/* Suppress warnings for unhandled routes */} + + {/* Redirects for old paths */} + } /> ); diff --git a/src/apps/stable/routes/asyncRoutes/admin.ts b/src/apps/stable/routes/asyncRoutes/admin.ts index b06c8cac8c..72bcc6f32b 100644 --- a/src/apps/stable/routes/asyncRoutes/admin.ts +++ b/src/apps/stable/routes/asyncRoutes/admin.ts @@ -1,7 +1,6 @@ import { AsyncRoute } from '../../../../components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ - { path: 'dashboard/activity', page: 'dashboard/activity' }, { path: 'notificationsettings.html', page: 'dashboard/notifications' }, { path: 'usernew.html', page: 'user/usernew' }, { path: 'userprofiles.html', page: 'user/userprofiles' }, diff --git a/src/apps/stable/routes/legacyRoutes/admin.ts b/src/apps/stable/routes/legacyRoutes/admin.ts index e2037ddb11..92aaef8041 100644 --- a/src/apps/stable/routes/legacyRoutes/admin.ts +++ b/src/apps/stable/routes/legacyRoutes/admin.ts @@ -164,7 +164,7 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ view: 'dashboard/scheduledtasks/scheduledtasks.html' } }, { - path: 'serveractivity.html', + path: 'dashboard/activity', pageProps: { controller: 'dashboard/serveractivity', view: 'dashboard/serveractivity.html' diff --git a/src/controllers/dashboard/dashboard.html b/src/controllers/dashboard/dashboard.html index 7fd60f7695..a76cf11971 100644 --- a/src/controllers/dashboard/dashboard.html +++ b/src/controllers/dashboard/dashboard.html @@ -44,7 +44,7 @@
- +

${HeaderActivity}

@@ -61,7 +61,7 @@
- +

${Alerts}

diff --git a/src/scripts/libraryMenu.js b/src/scripts/libraryMenu.js index ae59829aa5..c49a8336d3 100644 --- a/src/scripts/libraryMenu.js +++ b/src/scripts/libraryMenu.js @@ -466,7 +466,7 @@ function createToolsMenuList(pluginItems) { }); links.push({ name: globalize.translate('HeaderActivity'), - href: '#/serveractivity.html', + href: '#/dashboard/activity', pageIds: ['serverActivityPage'], icon: 'assessment' }); From 6bdd480943e47918feba0f0163b394e1240ce873 Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Sat, 10 Jun 2023 02:31:48 -0400 Subject: [PATCH 5/7] Update date and overview columns --- src/apps/stable/routes/dashboard/activity.tsx | 175 +++++++++++++----- 1 file changed, 126 insertions(+), 49 deletions(-) diff --git a/src/apps/stable/routes/dashboard/activity.tsx b/src/apps/stable/routes/dashboard/activity.tsx index 297a47aecf..159096aeea 100644 --- a/src/apps/stable/routes/dashboard/activity.tsx +++ b/src/apps/stable/routes/dashboard/activity.tsx @@ -1,13 +1,17 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { FC, 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 { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level'; import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; +import Info from '@mui/icons-material/Info'; import Box from '@mui/material/Box'; import Chip from '@mui/material/Chip'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import IconButton from '@mui/material/IconButton'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; +import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import { DataGrid, type GridColDef } from '@mui/x-data-grid'; import { useSearchParams } from 'react-router-dom'; @@ -15,6 +19,7 @@ import { 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'; @@ -62,58 +67,65 @@ const LogLevelChip = ({ level }: { level: LogLevel }) => { ); }; +const OverviewCell: FC = ({ 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 ( + + + {displayValue} + + {ShortOverview && Overview && ( + + + + + + + + )} + + ); +}; + const Activity = () => { const { api } = useApi(); const [ searchParams, setSearchParams ] = useSearchParams(); - const columns: GridColDef[] = [ - { - field: 'Date', - headerName: globalize.translate('LabelDate'), - width: 180, - type: 'dateTime', - valueGetter: ({ row }) => new Date(row.Date) - }, - { - field: 'Severity', - headerName: globalize.translate('LabelLevel'), - width: 110, - renderCell: ({ row }) => ( - row.Severity ? ( - - ) : undefined - ) - }, - { - field: 'User', - headerName: globalize.translate('LabelUser'), - width: 60, - valueGetter: ({ row }) => users[row.UserId]?.Name, - renderCell: ({ row }) => ( - - ) - }, - { - field: 'Name', - headerName: globalize.translate('LabelName'), - width: 200 - }, - { - field: 'Overview', - headerName: globalize.translate('LabelOverview'), - width: 200, - valueGetter: ({ row }) => row.Overview ?? row.ShortOverview - }, - { - field: 'Type', - headerName: globalize.translate('LabelType'), - width: 150 - } - ]; - const [ activityView, setActivityView ] = useState( getActivityView(searchParams.get(VIEW_PARAM))); const [ isLoading, setIsLoading ] = useState(true); @@ -125,6 +137,70 @@ const Activity = () => { const [ rows, setRows ] = useState([]); const [ users, setUsers ] = useState>({}); + const userColDef: GridColDef[] = activityView !== ActivityView.System ? [ + { + field: 'User', + headerName: globalize.translate('LabelUser'), + width: 60, + valueGetter: ({ row }) => users[row.UserId]?.Name, + renderCell: ({ row }) => ( + + ) + } + ] : []; + + 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 ? ( + + ) : undefined + ) + }, + ...userColDef, + { + field: 'Name', + headerName: globalize.translate('LabelName'), + width: 200 + }, + { + field: 'Overview', + headerName: globalize.translate('LabelOverview'), + width: 220, + valueGetter: ({ row }) => row.ShortOverview ?? row.Overview, + renderCell: ({ row }) => ( + + ) + }, + { + field: 'Type', + headerName: globalize.translate('LabelType'), + width: 150 + } + ]; + const onViewChange = useCallback((_e, newView: ActivityView | null) => { if (newView !== null) { setActivityView(newView); @@ -175,6 +251,7 @@ const Activity = () => { setIsLoading(false); }; + setIsLoading(true); fetchActivity() .catch(err => { console.error('[activity] failed to fetch activity log entries', err); From 97f703f7d795d33496302f0ef7af1d691facf7fa Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 13 Jun 2023 17:15:20 -0400 Subject: [PATCH 6/7] Move activity log to experimental routes --- src/apps/experimental/routes/asyncRoutes/admin.ts | 4 ++-- .../{stable => experimental}/routes/dashboard/activity.tsx | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename src/apps/{stable => experimental}/routes/dashboard/activity.tsx (100%) diff --git a/src/apps/experimental/routes/asyncRoutes/admin.ts b/src/apps/experimental/routes/asyncRoutes/admin.ts index b06c8cac8c..7e8c0eca16 100644 --- a/src/apps/experimental/routes/asyncRoutes/admin.ts +++ b/src/apps/experimental/routes/asyncRoutes/admin.ts @@ -1,7 +1,7 @@ -import { AsyncRoute } from '../../../../components/router/AsyncRoute'; +import { AsyncRoute, AsyncRouteType } from 'components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ - { path: 'dashboard/activity', page: 'dashboard/activity' }, + { path: 'dashboard/activity', page: 'dashboard/activity', type: AsyncRouteType.Experimental }, { path: 'notificationsettings.html', page: 'dashboard/notifications' }, { path: 'usernew.html', page: 'user/usernew' }, { path: 'userprofiles.html', page: 'user/userprofiles' }, diff --git a/src/apps/stable/routes/dashboard/activity.tsx b/src/apps/experimental/routes/dashboard/activity.tsx similarity index 100% rename from src/apps/stable/routes/dashboard/activity.tsx rename to src/apps/experimental/routes/dashboard/activity.tsx From 834e36b5a3cb9fbdd1d23e25bdefeb518f59e01c Mon Sep 17 00:00:00 2001 From: Bill Thornton Date: Tue, 27 Jun 2023 01:31:50 -0400 Subject: [PATCH 7/7] Move distinct components to individual files --- .../components/GridActionsCellLink.tsx | 17 +++ .../components/activityTable/LogLevelChip.tsx | 34 +++++ .../components/activityTable/OverviewCell.tsx | 64 +++++++++ .../routes/dashboard/activity.tsx | 135 ++++++------------ src/components/UserAvatar.tsx | 4 +- src/strings/en-us.json | 1 + 6 files changed, 157 insertions(+), 98 deletions(-) create mode 100644 src/apps/experimental/components/GridActionsCellLink.tsx create mode 100644 src/apps/experimental/components/activityTable/LogLevelChip.tsx create mode 100644 src/apps/experimental/components/activityTable/OverviewCell.tsx diff --git a/src/apps/experimental/components/GridActionsCellLink.tsx b/src/apps/experimental/components/GridActionsCellLink.tsx new file mode 100644 index 0000000000..faf7619c9e --- /dev/null +++ b/src/apps/experimental/components/GridActionsCellLink.tsx @@ -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; + +/** + * 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) => ( + + + +); + +export default GridActionsCellLink; diff --git a/src/apps/experimental/components/activityTable/LogLevelChip.tsx b/src/apps/experimental/components/activityTable/LogLevelChip.tsx new file mode 100644 index 0000000000..8053f770c1 --- /dev/null +++ b/src/apps/experimental/components/activityTable/LogLevelChip.tsx @@ -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 ( + + ); +}; + +export default LogLevelChip; diff --git a/src/apps/experimental/components/activityTable/OverviewCell.tsx b/src/apps/experimental/components/activityTable/OverviewCell.tsx new file mode 100644 index 0000000000..69702de5f6 --- /dev/null +++ b/src/apps/experimental/components/activityTable/OverviewCell.tsx @@ -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 = ({ 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 ( + + + {displayValue} + + {ShortOverview && Overview && ( + + + + + + + + )} + + ); +}; + +export default OverviewCell; diff --git a/src/apps/experimental/routes/dashboard/activity.tsx b/src/apps/experimental/routes/dashboard/activity.tsx index 159096aeea..f007e104d3 100644 --- a/src/apps/experimental/routes/dashboard/activity.tsx +++ b/src/apps/experimental/routes/dashboard/activity.tsx @@ -1,20 +1,16 @@ -import React, { FC, useCallback, useEffect, useState } from 'react'; +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 { LogLevel } from '@jellyfin/sdk/lib/generated-client/models/log-level'; import type { UserDto } from '@jellyfin/sdk/lib/generated-client/models/user-dto'; -import Info from '@mui/icons-material/Info'; +import PermMedia from '@mui/icons-material/PermMedia'; import Box from '@mui/material/Box'; -import Chip from '@mui/material/Chip'; -import ClickAwayListener from '@mui/material/ClickAwayListener'; import IconButton from '@mui/material/IconButton'; import ToggleButton from '@mui/material/ToggleButton'; import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'; -import Tooltip from '@mui/material/Tooltip'; import Typography from '@mui/material/Typography'; import { DataGrid, type GridColDef } from '@mui/x-data-grid'; -import { useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams } from 'react-router-dom'; import Page from 'components/Page'; import UserAvatar from 'components/UserAvatar'; @@ -23,6 +19,10 @@ import { parseISO8601Date, toLocaleDateString, toLocaleTimeString } from 'script 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'; @@ -40,88 +40,6 @@ const getActivityView = (param: string | null) => { const getRowId = (row: ActivityLogEntry) => row.Id ?? -1; -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 ( - - ); -}; - -const OverviewCell: FC = ({ 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 ( - - - {displayValue} - - {ShortOverview && Overview && ( - - - - - - - - )} - - ); -}; - const Activity = () => { const { api } = useApi(); const [ searchParams, setSearchParams ] = useSearchParams(); @@ -144,10 +62,16 @@ const Activity = () => { width: 60, valueGetter: ({ row }) => users[row.UserId]?.Name, renderCell: ({ row }) => ( - + + + ) } ] : []; @@ -188,7 +112,7 @@ const Activity = () => { { field: 'Overview', headerName: globalize.translate('LabelOverview'), - width: 220, + width: 200, valueGetter: ({ row }) => row.ShortOverview ?? row.Overview, renderCell: ({ row }) => ( @@ -197,7 +121,28 @@ const Activity = () => { { field: 'Type', headerName: globalize.translate('LabelType'), - width: 150 + width: 120 + }, + { + field: 'actions', + type: 'actions', + getActions: ({ row }) => { + const actions = []; + + if (row.ItemId) { + actions.push( + } + label={globalize.translate('LabelMediaDetails')} + title={globalize.translate('LabelMediaDetails')} + to={`/details?id=${row.ItemId}`} + /> + ); + } + + return actions; + } } ]; diff --git a/src/components/UserAvatar.tsx b/src/components/UserAvatar.tsx index 0f17802805..3a2790e835 100644 --- a/src/components/UserAvatar.tsx +++ b/src/components/UserAvatar.tsx @@ -7,17 +7,15 @@ import { useApi } from 'hooks/useApi'; interface UserAvatarProps { user?: UserDto - showTitle?: boolean } -const UserAvatar: FC = ({ user, showTitle = false }) => { +const UserAvatar: FC = ({ user }) => { const { api } = useApi(); const theme = useTheme(); return user ? (