import Badge from '@app/components/Common/Badge'; import Button from '@app/components/Common/Button'; import LoadingSpinner from '@app/components/Common/LoadingSpinner'; import Modal from '@app/components/Common/Modal'; import PageTitle from '@app/components/Common/PageTitle'; import Table from '@app/components/Common/Table'; import Tooltip from '@app/components/Common/Tooltip'; import useDebouncedState from '@app/hooks/useDebouncedState'; import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams'; import globalMessages from '@app/i18n/globalMessages'; import Error from '@app/pages/_error'; import { Transition } from '@headlessui/react'; import { ChevronLeftIcon, ChevronRightIcon, ClipboardDocumentIcon, DocumentMagnifyingGlassIcon, FunnelIcon, MagnifyingGlassIcon, PauseIcon, PlayIcon, } from '@heroicons/react/24/solid'; import type { LogMessage, LogsResultsResponse, } from '@server/interfaces/api/settingsInterfaces'; import copy from 'copy-to-clipboard'; import { useRouter } from 'next/router'; import { Fragment, useEffect, useState } from 'react'; import { defineMessages, useIntl } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; const messages = defineMessages({ logs: 'Logs', logsDescription: 'You can also view these logs directly via stdout, or in {appDataPath}/logs/overseerr.log.', time: 'Timestamp', level: 'Severity', label: 'Label', message: 'Message', filterDebug: 'Debug', filterInfo: 'Info', filterWarn: 'Warning', filterError: 'Error', showall: 'Show All Logs', pauseLogs: 'Pause', resumeLogs: 'Resume', copyToClipboard: 'Copy to Clipboard', logDetails: 'Log Details', extraData: 'Additional Data', copiedLogMessage: 'Copied log message to clipboard.', viewdetails: 'View Details', }); type Filter = 'debug' | 'info' | 'warn' | 'error'; const SettingsLogs = () => { const router = useRouter(); const intl = useIntl(); const { addToast } = useToasts(); const [currentFilter, setCurrentFilter] = useState('debug'); const [currentPageSize, setCurrentPageSize] = useState(25); const [searchFilter, debouncedSearchFilter, setSearchFilter] = useDebouncedState(''); const [refreshInterval, setRefreshInterval] = useState(5000); const [activeLog, setActiveLog] = useState<{ isOpen: boolean; log?: LogMessage; }>({ isOpen: false }); const page = router.query.page ? Number(router.query.page) : 1; const pageIndex = page - 1; const updateQueryParams = useUpdateQueryParams({ page: page.toString() }); const toggleLogs = () => { setRefreshInterval(refreshInterval === 5000 ? 0 : 5000); }; const { data, error } = useSWR( `/api/v1/settings/logs?take=${currentPageSize}&skip=${ pageIndex * currentPageSize }&filter=${currentFilter}${ debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : '' }`, { refreshInterval: refreshInterval, revalidateOnFocus: false, } ); const { data: appData } = useSWR('/api/v1/status/appdata'); useEffect(() => { const filterString = window.localStorage.getItem('logs-display-settings'); if (filterString) { const filterSettings = JSON.parse(filterString); setCurrentFilter(filterSettings.currentFilter); setCurrentPageSize(filterSettings.currentPageSize); } }, []); useEffect(() => { window.localStorage.setItem( 'logs-display-settings', JSON.stringify({ currentFilter, currentPageSize, }) ); }, [currentFilter, currentPageSize]); const copyLogString = (log: LogMessage): void => { copy( `${log.timestamp} [${log.level}]${log.label ? `[${log.label}]` : ''}: ${ log.message }${log.data ? `${JSON.stringify(log.data)}` : ''}` ); addToast(intl.formatMessage(messages.copiedLogMessage), { appearance: 'success', autoDismiss: true, }); }; // check if there's no data and no errors in the table // so as to show a spinner inside the table and not refresh the whole component if (!data && error) { return ; } const hasNextPage = data?.pageInfo.pages ?? 0 > pageIndex + 1; const hasPrevPage = pageIndex > 0; return ( <> setActiveLog({ log: activeLog.log, isOpen: false })} cancelText={intl.formatMessage(globalMessages.close)} onOk={() => activeLog.log ? copyLogString(activeLog.log) : undefined } okText={intl.formatMessage(messages.copyToClipboard)} okButtonType="primary" > {activeLog && ( <>
{intl.formatMessage(messages.time)}
{intl.formatDate(activeLog.log?.timestamp, { year: 'numeric', month: 'short', day: '2-digit', hour: 'numeric', minute: 'numeric', second: 'numeric', })}
{intl.formatMessage(messages.level)}
{activeLog.log?.level.toUpperCase()}
{intl.formatMessage(messages.label)}
{activeLog.log?.label}
{intl.formatMessage(messages.message)}
{activeLog.log?.message}
{activeLog.log?.data && (
{intl.formatMessage(messages.extraData)}
{JSON.stringify(activeLog.log?.data, null, ' ')}
)} )}

{intl.formatMessage(messages.logs)}

{intl.formatMessage(messages.logsDescription, { code: (msg: React.ReactNode) => ( {msg} ), appDataPath: appData ? appData.appDataPath : '/app/config', })}

setSearchFilter(e.target.value as string)} />
{intl.formatMessage(messages.time)}{intl.formatMessage(messages.level)}{intl.formatMessage(messages.label)}{intl.formatMessage(messages.message)} {!data ? ( ) : ( data.results.map((row: LogMessage, index: number) => { return ( {intl.formatDate(row.timestamp, { year: 'numeric', month: 'short', day: '2-digit', hour: 'numeric', minute: 'numeric', second: 'numeric', })} {row.level.toUpperCase()} {row.label ?? ''} {row.message} {row.data && ( )} ); }) )} {data?.results.length === 0 && (
{intl.formatMessage(globalMessages.noresults)} {currentFilter !== 'debug' && (
)}
)}
); }; export default SettingsLogs;