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} ),