diff --git a/src/apps/dashboard/features/logs/api/useServerLog.ts b/src/apps/dashboard/features/logs/api/useServerLog.ts new file mode 100644 index 0000000000..c3794bf182 --- /dev/null +++ b/src/apps/dashboard/features/logs/api/useServerLog.ts @@ -0,0 +1,35 @@ +import { Api } from '@jellyfin/sdk'; +import { getSystemApi } from '@jellyfin/sdk/lib/utils/api/system-api'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import type { AxiosRequestConfig } from 'axios'; + +const fetchServerLog = async ( + api?: Api, + name?: string, + options?: AxiosRequestConfig +) => { + if (!api) { + console.error('[useServerLog] No API instance available'); + return; + } + + if (!name) { + console.error('[useServerLog] Name is required'); + return; + } + + const response = await getSystemApi(api).getLogFile({ name }, options); + + // FIXME: TypeScript SDK thinks it is returning a File but in reality it is a string + return response.data as never as string; +}; +export const useServerLog = (name: string) => { + const { api } = useApi(); + + return useQuery({ + queryKey: ['ServerLog', name], + queryFn: ({ signal }) => fetchServerLog(api, name, { signal }), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/logs/components/LogItemList.tsx b/src/apps/dashboard/features/logs/components/LogItemList.tsx index a7f2aa69f6..eed1509726 100644 --- a/src/apps/dashboard/features/logs/components/LogItemList.tsx +++ b/src/apps/dashboard/features/logs/components/LogItemList.tsx @@ -2,28 +2,15 @@ import React, { FunctionComponent } from 'react'; import type { LogFile } from '@jellyfin/sdk/lib/generated-client/models/log-file'; import List from '@mui/material/List'; import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; import ListItemText from '@mui/material/ListItemText'; -import OpenInNewIcon from '@mui/icons-material/OpenInNew'; -import { useApi } from 'hooks/useApi'; import datetime from 'scripts/datetime'; +import ListItemLink from 'components/ListItemLink'; type LogItemProps = { logs: LogFile[]; }; const LogItemList: FunctionComponent = ({ logs }: LogItemProps) => { - const { api } = useApi(); - - const getLogFileUrl = (logFile: LogFile) => { - if (!api) return ''; - - return api.getUri('/System/Logs/Log', { - name: logFile.Name, - api_key: api.accessToken - }); - }; - const getDate = (logFile: LogFile) => { const date = datetime.parseISO8601Date(logFile.DateModified, true); return datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date); @@ -34,15 +21,14 @@ const LogItemList: FunctionComponent = ({ logs }: LogItemProps) => {logs.map(log => { return ( - + - - + ); })} diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index 19cf7ea1a5..c48c604b79 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -7,6 +7,7 @@ export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'devices', type: AppType.Dashboard }, { path: 'keys', type: AppType.Dashboard }, { path: 'logs', type: AppType.Dashboard }, + { path: 'logs/:file', page: 'logs/file', type: AppType.Dashboard }, { path: 'playback/resume', type: AppType.Dashboard }, { path: 'playback/streaming', type: AppType.Dashboard }, { path: 'playback/trickplay', type: AppType.Dashboard }, diff --git a/src/apps/dashboard/routes/logs/file.tsx b/src/apps/dashboard/routes/logs/file.tsx new file mode 100644 index 0000000000..25b7e492f1 --- /dev/null +++ b/src/apps/dashboard/routes/logs/file.tsx @@ -0,0 +1,112 @@ +import Loading from 'components/loading/LoadingComponent'; +import Page from 'components/Page'; +import React, { useCallback } from 'react'; +import { useParams } from 'react-router-dom'; +import { useServerLog } from 'apps/dashboard/features/logs/api/useServerLog'; +import { + Alert, + Box, + Button, + ButtonGroup, + Card, + CardContent, + Container, + Typography +} from '@mui/material'; +import { ContentCopy, FileDownload } from '@mui/icons-material'; +import globalize from 'lib/globalize'; + +export const Component = () => { + const { file: fileName } = useParams(); + const { + isError: error, + isPending: loading, + data: log, + refetch + } = useServerLog(fileName ?? ''); + + const retry = useCallback(() => refetch(), [refetch]); + + const copyToClipboard = useCallback(async () => { + if ('clipboard' in navigator && log) { + await navigator.clipboard.writeText(log); + } + }, [log]); + + const downloadFile = useCallback(() => { + if ('URL' in globalThis && log && fileName) { + const blob = new Blob([log], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + a.click(); + URL.revokeObjectURL(url); + } + }, [log, fileName]); + + return ( + + + + {fileName} + + {error && ( + + Retry + + } + > + {globalize.translate('LogLoadFailure')} + + )} + + {loading && } + + {!error && !loading && ( + <> + + + + + + + + +
{log}
+
+
+
+ + )} +
+
+
+ ); +}; + +Component.displayName = 'LogPage'; diff --git a/src/strings/en-us.json b/src/strings/en-us.json index 5377cbd88a..576de0abd8 100644 --- a/src/strings/en-us.json +++ b/src/strings/en-us.json @@ -1808,5 +1808,6 @@ "OptionExtractTrickplayImage": "Enable trickplay image extraction", "ExtractTrickplayImagesHelp": "Trickplay images are similar to chapter images, except they span the entire length of the content and are used to show a preview when scrubbing through videos.", "LabelExtractTrickplayDuringLibraryScan": "Extract trickplay images during the library scan", - "LabelExtractTrickplayDuringLibraryScanHelp": "Generate trickplay images when videos are imported during the library scan. Otherwise, they will be extracted during the trickplay images scheduled task. If generation is set to non-blocking this will not affect the time a library scan takes to complete." + "LabelExtractTrickplayDuringLibraryScanHelp": "Generate trickplay images when videos are imported during the library scan. Otherwise, they will be extracted during the trickplay images scheduled task. If generation is set to non-blocking this will not affect the time a library scan takes to complete.", + "LogLoadFailure": "Failed to load the log file. It may still be actively written to." }