diff --git a/overseerr-api.yml b/overseerr-api.yml
index 25a24667..164187de 100644
--- a/overseerr-api.yml
+++ b/overseerr-api.yml
@@ -2539,6 +2539,12 @@ paths:
nullable: true
enum: [debug, info, warn, error]
default: debug
+ - in: query
+ name: search
+ schema:
+ type: string
+ nullable: true
+ example: plex
responses:
'200':
description: Server log returned
diff --git a/server/routes/settings/index.ts b/server/routes/settings/index.ts
index de23d4b8..41d2c745 100644
--- a/server/routes/settings/index.ts
+++ b/server/routes/settings/index.ts
@@ -25,7 +25,7 @@ import { getAppVersion } from '@server/utils/appVersion';
import { Router } from 'express';
import rateLimit from 'express-rate-limit';
import fs from 'fs';
-import { merge, omit, set, sortBy } from 'lodash';
+import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
import { rescheduleJob } from 'node-schedule';
import path from 'path';
import semver from 'semver';
@@ -344,6 +344,8 @@ settingsRoutes.get(
(req, res, next) => {
const pageSize = req.query.take ? Number(req.query.take) : 25;
const skip = req.query.skip ? Number(req.query.skip) : 0;
+ const search = (req.query.search as string) ?? '';
+ const searchRegexp = new RegExp(escapeRegExp(search), 'i');
let filter: string[] = [];
switch (req.query.filter) {
@@ -375,6 +377,22 @@ settingsRoutes.get(
'data',
];
+ const deepValueStrings = (obj: Record): string[] => {
+ const values = [];
+
+ for (const val of Object.values(obj)) {
+ if (typeof val === 'string') {
+ values.push(val);
+ } else if (typeof val === 'number') {
+ values.push(val.toString());
+ } else if (val !== null && typeof val === 'object') {
+ values.push(...deepValueStrings(val as Record));
+ }
+ }
+
+ return values;
+ };
+
try {
fs.readFileSync(logFile, 'utf-8')
.split('\n')
@@ -399,6 +417,19 @@ settingsRoutes.get(
});
}
+ if (req.query.search) {
+ if (
+ // label and data are sometimes undefined
+ !searchRegexp.test(logMessage.label ?? '') &&
+ !searchRegexp.test(logMessage.message) &&
+ !deepValueStrings(logMessage.data ?? {}).some((val) =>
+ searchRegexp.test(val)
+ )
+ ) {
+ return;
+ }
+ }
+
logs.push(logMessage);
});
diff --git a/src/components/Settings/SettingsLogs/index.tsx b/src/components/Settings/SettingsLogs/index.tsx
index 3b370e24..fbf5d5e0 100644
--- a/src/components/Settings/SettingsLogs/index.tsx
+++ b/src/components/Settings/SettingsLogs/index.tsx
@@ -5,6 +5,7 @@ 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';
@@ -17,6 +18,7 @@ import {
FilterIcon,
PauseIcon,
PlayIcon,
+ SearchIcon,
} from '@heroicons/react/solid';
import type {
LogMessage,
@@ -59,6 +61,8 @@ const SettingsLogs = () => {
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;
@@ -76,7 +80,9 @@ const SettingsLogs = () => {
const { data, error } = useSWR(
`/api/v1/settings/logs?take=${currentPageSize}&skip=${
pageIndex * currentPageSize
- }&filter=${currentFilter}`,
+ }&filter=${currentFilter}${
+ debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''
+ }`,
{
refreshInterval: refreshInterval,
revalidateOnFocus: false,
@@ -118,15 +124,13 @@ const SettingsLogs = () => {
});
};
- if (!data && !error) {
- return ;
- }
-
- if (!data) {
+ // 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 > pageIndex + 1;
+ const hasNextPage = data?.pageInfo.pages ?? 0 > pageIndex + 1;
const hasPrevPage = pageIndex > 0;
return (
@@ -245,10 +249,21 @@ const SettingsLogs = () => {
appDataPath: appData ? appData.appDataPath : '/app/config',
})}
-
+
+
+
+
+
+ setSearchFilter(e.target.value as string)}
+ />
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -300,73 +315,81 @@ const SettingsLogs = () => {
- {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 ? (
+
+
+
+
+
+ ) : (
+ 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 && (
+ {data?.results.length === 0 && (
@@ -396,15 +419,15 @@ const SettingsLogs = () => {
>
- {data.results.length > 0 &&
+ {(data?.results.length ?? 0) > 0 &&
intl.formatMessage(globalMessages.showingresults, {
from: pageIndex * currentPageSize + 1,
to:
- data.results.length < currentPageSize
+ data?.results.length ?? 0 < currentPageSize
? pageIndex * currentPageSize +
- data.results.length
+ (data?.results.length ?? 0)
: (pageIndex + 1) * currentPageSize,
- total: data.pageInfo.results,
+ total: data?.pageInfo.results ?? 0,
strong: (msg: React.ReactNode) => (
{msg}
),