diff --git a/src/apps/dashboard/features/activity/api/useLogEntries.ts b/src/apps/dashboard/features/activity/api/useLogEntries.ts index b0f1be5233..ba956d5171 100644 --- a/src/apps/dashboard/features/activity/api/useLogEntries.ts +++ b/src/apps/dashboard/features/activity/api/useLogEntries.ts @@ -23,12 +23,12 @@ const fetchLogEntries = async ( return response.data; }; -export const useLogEntires = ( +export const useLogEntries = ( requestParams: ActivityLogApiGetLogEntriesRequest ) => { const { api } = useApi(); return useQuery({ - queryKey: ['LogEntries', requestParams], + queryKey: ['ActivityLogEntries', requestParams], queryFn: ({ signal }) => fetchLogEntries(api, requestParams, { signal }), enabled: !!api diff --git a/src/apps/dashboard/features/logs/api/useServerLogs.ts b/src/apps/dashboard/features/logs/api/useServerLogs.ts new file mode 100644 index 0000000000..fa00dc9afa --- /dev/null +++ b/src/apps/dashboard/features/logs/api/useServerLogs.ts @@ -0,0 +1,26 @@ +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 fetchServerLogs = async (api?: Api, options?: AxiosRequestConfig) => { + if (!api) { + console.error('[useServerLogs] No API instance available'); + return; + } + + const response = await getSystemApi(api).getServerLogs(options); + + return response.data; +}; + +export const useServerLogs = () => { + const { api } = useApi(); + + return useQuery({ + queryKey: [ 'ServerLogs' ], + queryFn: ({ signal }) => fetchServerLogs(api, { signal }), + enabled: !!api + }); +}; diff --git a/src/apps/dashboard/features/logs/components/LogItemList.tsx b/src/apps/dashboard/features/logs/components/LogItemList.tsx new file mode 100644 index 0000000000..3b72120faa --- /dev/null +++ b/src/apps/dashboard/features/logs/components/LogItemList.tsx @@ -0,0 +1,56 @@ +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'; + +type LogItemProps = { + logs: LogFile[]; +}; + +const LogItemList: FunctionComponent = ({ logs }: LogItemProps) => { + const { api } = useApi(); + + // TODO: Use getUri from TS SDK once available. + const getLogFileUrl = (logFile: LogFile) => { + if (!api) return ''; + + let url = api.basePath + '/System/Logs/Log'; + + url += '?name=' + encodeURIComponent(String(logFile.Name)); + url += '&api_key=' + encodeURIComponent(api.accessToken); + + return url; + }; + + const getDate = (logFile: LogFile) => { + const date = datetime.parseISO8601Date(logFile.DateModified, true); + return datetime.toLocaleDateString(date) + ' ' + datetime.getDisplayTime(date); + }; + + return ( + + {logs.map(log => { + return ( + + + + + + + ); + })} + + ); +}; + +export default LogItemList; diff --git a/src/apps/dashboard/routes/_asyncRoutes.ts b/src/apps/dashboard/routes/_asyncRoutes.ts index c2e1e4a8d2..bd7e264318 100644 --- a/src/apps/dashboard/routes/_asyncRoutes.ts +++ b/src/apps/dashboard/routes/_asyncRoutes.ts @@ -3,6 +3,7 @@ import { AsyncRouteType, type AsyncRoute } from 'components/router/AsyncRoute'; export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [ { path: 'activity', type: AsyncRouteType.Dashboard }, { path: 'branding', type: AsyncRouteType.Dashboard }, + { path: 'logs', type: AsyncRouteType.Dashboard }, { path: 'playback/trickplay', type: AsyncRouteType.Dashboard }, { path: 'plugins/:pluginId', page: 'plugins/plugin', type: AsyncRouteType.Dashboard }, { path: 'users', type: AsyncRouteType.Dashboard }, diff --git a/src/apps/dashboard/routes/_legacyRoutes.ts b/src/apps/dashboard/routes/_legacyRoutes.ts index ae82c7e5f7..64911dc20a 100644 --- a/src/apps/dashboard/routes/_legacyRoutes.ts +++ b/src/apps/dashboard/routes/_legacyRoutes.ts @@ -49,12 +49,6 @@ export const LEGACY_ADMIN_ROUTES: LegacyRoute[] = [ controller: 'dashboard/encodingsettings', view: 'dashboard/encodingsettings.html' } - }, { - path: 'logs', - pageProps: { - controller: 'dashboard/logs', - view: 'dashboard/logs.html' - } }, { path: 'libraries/metadata', pageProps: { diff --git a/src/apps/dashboard/routes/activity.tsx b/src/apps/dashboard/routes/activity/index.tsx similarity index 98% rename from src/apps/dashboard/routes/activity.tsx rename to src/apps/dashboard/routes/activity/index.tsx index 666d08441c..5b0e328777 100644 --- a/src/apps/dashboard/routes/activity.tsx +++ b/src/apps/dashboard/routes/activity/index.tsx @@ -9,7 +9,7 @@ import Typography from '@mui/material/Typography'; import { type MRT_ColumnDef, MaterialReactTable, useMaterialReactTable } from 'material-react-table'; import { useSearchParams } from 'react-router-dom'; -import { useLogEntires } from 'apps/dashboard/features/activity/api/useLogEntries'; +import { useLogEntries } from 'apps/dashboard/features/activity/api/useLogEntries'; import ActionsCell from 'apps/dashboard/features/activity/components/ActionsCell'; import LogLevelCell from 'apps/dashboard/features/activity/components/LogLevelCell'; import OverviewCell from 'apps/dashboard/features/activity/components/OverviewCell'; @@ -87,7 +87,7 @@ const Activity = () => { hasUserId: activityView !== ActivityView.All ? activityView === ActivityView.User : undefined }), [activityView, pagination.pageIndex, pagination.pageSize]); - const { data: logEntries, isLoading: isLogEntriesLoading } = useLogEntires(activityParams); + const { data: logEntries, isLoading: isLogEntriesLoading } = useLogEntries(activityParams); const isLoading = isUsersLoading || isLogEntriesLoading; diff --git a/src/apps/dashboard/routes/branding/index.tsx b/src/apps/dashboard/routes/branding/index.tsx index 9958f13803..7859610a5c 100644 --- a/src/apps/dashboard/routes/branding/index.tsx +++ b/src/apps/dashboard/routes/branding/index.tsx @@ -17,10 +17,7 @@ import Page from 'components/Page'; import ServerConnections from 'components/ServerConnections'; import globalize from 'lib/globalize'; import { queryClient } from 'utils/query/queryClient'; - -interface ActionData { - isSaved: boolean -} +import { ActionData } from 'types/actionData'; const BRANDING_CONFIG_KEY = 'branding'; const BrandingOption = { diff --git a/src/apps/dashboard/routes/logs/index.tsx b/src/apps/dashboard/routes/logs/index.tsx new file mode 100644 index 0000000000..bafab8f763 --- /dev/null +++ b/src/apps/dashboard/routes/logs/index.tsx @@ -0,0 +1,139 @@ +import React, { ChangeEvent, useCallback, useEffect, useState } from 'react'; +import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; +import Loading from 'components/loading/LoadingComponent'; +import Page from 'components/Page'; +import globalize from 'lib/globalize'; +import Alert from '@mui/material/Alert'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Stack from '@mui/material/Stack'; +import Switch from '@mui/material/Switch'; +import TextField from '@mui/material/TextField'; +import Typography from '@mui/material/Typography'; +import { type ActionFunctionArgs, Form, useActionData } from 'react-router-dom'; +import ServerConnections from 'components/ServerConnections'; +import { useServerLogs } from 'apps/dashboard/features/logs/api/useServerLogs'; +import { useConfiguration } from 'hooks/useConfiguration'; +import type { ServerConfiguration } from '@jellyfin/sdk/lib/generated-client/models/server-configuration'; +import { ActionData } from 'types/actionData'; +import LogItemList from 'apps/dashboard/features/logs/components/LogItemList'; + +export const action = async ({ request }: ActionFunctionArgs) => { + const api = ServerConnections.getCurrentApi(); + if (!api) throw new Error('No Api instance available'); + + const formData = await request.formData(); + const { data: config } = await getConfigurationApi(api).getConfiguration(); + + const enableWarningMessage = formData.get('EnableWarningMessage'); + config.EnableSlowResponseWarning = enableWarningMessage === 'on'; + + const responseTime = formData.get('SlowResponseTime'); + if (responseTime) { + config.SlowResponseThresholdMs = parseInt(responseTime.toString(), 10); + } + + await getConfigurationApi(api) + .updateConfiguration({ serverConfiguration: config }); + + return { + isSaved: true + }; +}; + +const Logs = () => { + const actionData = useActionData() as ActionData | undefined; + const [ isSubmitting, setIsSubmitting ] = useState(false); + + const { isPending: isLogEntriesPending, data: logs } = useServerLogs(); + const { isPending: isConfigurationPending, data: defaultConfiguration } = useConfiguration(); + const [ loading, setLoading ] = useState(true); + const [ configuration, setConfiguration ] = useState( {} ); + + useEffect(() => { + if (!isConfigurationPending && defaultConfiguration) { + setConfiguration(defaultConfiguration); + setLoading(false); + } + }, [isConfigurationPending, defaultConfiguration]); + + const setLogWarningMessage = useCallback((_: ChangeEvent, checked: boolean) => { + setConfiguration({ + ...configuration, + EnableSlowResponseWarning: checked + }); + }, [configuration]); + + const onResponseTimeChange = useCallback((event: ChangeEvent) => { + setConfiguration({ + ...configuration, + SlowResponseThresholdMs: parseInt(event.target.value, 10) + }); + }, [configuration]); + + const onSubmit = useCallback(() => { + setIsSubmitting(true); + }, []); + + if (isLogEntriesPending || isConfigurationPending || loading || !logs) { + return ; + } + + return ( + + +
+ + + {globalize.translate('TabLogs')} + + + {isSubmitting && actionData?.isSaved && ( + + {globalize.translate('SettingsSaved')} + + )} + + + } + label={globalize.translate('LabelSlowResponseEnabled')} + /> + + + + + +
+ + + +
+
+ ); +}; + +export default Logs; diff --git a/src/controllers/dashboard/logs.html b/src/controllers/dashboard/logs.html deleted file mode 100644 index a23d7750f4..0000000000 --- a/src/controllers/dashboard/logs.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
-
-
-
-
-

${TabLogs}

-
-
- -
-
- -
-
- -
-
-
-
- -
-
-
-
-
-
-
diff --git a/src/controllers/dashboard/logs.js b/src/controllers/dashboard/logs.js deleted file mode 100644 index 110d4be815..0000000000 --- a/src/controllers/dashboard/logs.js +++ /dev/null @@ -1,63 +0,0 @@ -import datetime from '../../scripts/datetime'; -import loading from '../../components/loading/loading'; -import globalize from '../../lib/globalize'; -import '../../elements/emby-button/emby-button'; -import '../../components/listview/listview.scss'; -import '../../styles/flexstyles.scss'; -import Dashboard from '../../utils/dashboard'; -import alert from '../../components/alert'; - -function onSubmit(event) { - event.preventDefault(); - loading.show(); - const form = this; - ApiClient.getServerConfiguration().then(function (config) { - config.EnableSlowResponseWarning = form.querySelector('#chkSlowResponseWarning').checked; - config.SlowResponseThresholdMs = form.querySelector('#txtSlowResponseWarning').value; - ApiClient.updateServerConfiguration(config).then(function() { - Dashboard.processServerConfigurationUpdateResult(); - }, function () { - alert(globalize.translate('ErrorDefault')); - Dashboard.processServerConfigurationUpdateResult(); - }); - }); - return false; -} - -export default function(view) { - view.querySelector('.logsForm').addEventListener('submit', onSubmit); - view.addEventListener('viewbeforeshow', function() { - loading.show(); - const apiClient = ApiClient; - apiClient.getJSON(apiClient.getUrl('System/Logs')).then(function(logs) { - let html = ''; - html += '
'; - html += logs.map(function(log) { - let logUrl = apiClient.getUrl('System/Logs/Log', { - name: log.Name - }); - logUrl += '&api_key=' + apiClient.accessToken(); - let logHtml = ''; - logHtml += ''; - logHtml += '
'; - logHtml += "

" + log.Name + '

'; - const date = datetime.parseISO8601Date(log.DateModified, true); - let text = datetime.toLocaleDateString(date); - text += ' ' + datetime.getDisplayTime(date); - logHtml += '
' + text + '
'; - logHtml += '
'; - logHtml += '
'; - return logHtml; - }).join(''); - html += '
'; - view.querySelector('.serverLogs').innerHTML = html; - }); - - apiClient.getServerConfiguration().then(function (config) { - view.querySelector('#chkSlowResponseWarning').checked = config.EnableSlowResponseWarning; - view.querySelector('#txtSlowResponseWarning').value = config.SlowResponseThresholdMs; - }); - - loading.hide(); - }); -} diff --git a/src/hooks/useConfiguration.ts b/src/hooks/useConfiguration.ts new file mode 100644 index 0000000000..2d5dbd5a2a --- /dev/null +++ b/src/hooks/useConfiguration.ts @@ -0,0 +1,26 @@ +import { Api } from '@jellyfin/sdk'; +import { getConfigurationApi } from '@jellyfin/sdk/lib/utils/api/configuration-api'; +import { useQuery } from '@tanstack/react-query'; +import { useApi } from 'hooks/useApi'; +import type { AxiosRequestConfig } from 'axios'; + +export const fetchConfiguration = async (api?: Api, options?: AxiosRequestConfig) => { + if (!api) { + console.error('[useLogOptions] No API instance available'); + return; + } + + const response = await getConfigurationApi(api).getConfiguration(options); + + return response.data; +}; + +export const useConfiguration = () => { + const { api } = useApi(); + + return useQuery({ + queryKey: ['Configuration'], + queryFn: ({ signal }) => fetchConfiguration(api, { signal }), + enabled: !!api + }); +}; diff --git a/src/types/actionData.ts b/src/types/actionData.ts new file mode 100644 index 0000000000..5524d64f54 --- /dev/null +++ b/src/types/actionData.ts @@ -0,0 +1,3 @@ +export interface ActionData { + isSaved: boolean; +}