From 30141f76e025763bf79fd3c8fb344d45519d5d8d Mon Sep 17 00:00:00 2001
From: Danshil Kokil Mungur
Date: Mon, 12 Sep 2022 06:21:16 +0400
Subject: [PATCH] feat(logs): add search filter (#2505)
* feat(logs): add search filter
* refactor(logs): move loading spinner inside log viewer
Inputting text in the search bar on the logs page would refresh the page
losing focus on the search bar.
This moves the loading spinner inside the log viewer, so that it is not
as disruptive as it would
* fix(logs): escape string for search filter
* chore: rebase
* fix(logs): suggested changes
---
overseerr-api.yml | 6 +
server/routes/settings/index.ts | 33 ++-
.../Settings/SettingsLogs/index.tsx | 219 ++++++++++--------
3 files changed, 159 insertions(+), 99 deletions(-)
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}
),