Skip to content

Commit 30141f7

Browse files
authored
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
1 parent 87825a0 commit 30141f7

File tree

3 files changed

+159
-99
lines changed

3 files changed

+159
-99
lines changed

overseerr-api.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2539,6 +2539,12 @@ paths:
25392539
nullable: true
25402540
enum: [debug, info, warn, error]
25412541
default: debug
2542+
- in: query
2543+
name: search
2544+
schema:
2545+
type: string
2546+
nullable: true
2547+
example: plex
25422548
responses:
25432549
'200':
25442550
description: Server log returned

server/routes/settings/index.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { getAppVersion } from '@server/utils/appVersion';
2525
import { Router } from 'express';
2626
import rateLimit from 'express-rate-limit';
2727
import fs from 'fs';
28-
import { merge, omit, set, sortBy } from 'lodash';
28+
import { escapeRegExp, merge, omit, set, sortBy } from 'lodash';
2929
import { rescheduleJob } from 'node-schedule';
3030
import path from 'path';
3131
import semver from 'semver';
@@ -344,6 +344,8 @@ settingsRoutes.get(
344344
(req, res, next) => {
345345
const pageSize = req.query.take ? Number(req.query.take) : 25;
346346
const skip = req.query.skip ? Number(req.query.skip) : 0;
347+
const search = (req.query.search as string) ?? '';
348+
const searchRegexp = new RegExp(escapeRegExp(search), 'i');
347349

348350
let filter: string[] = [];
349351
switch (req.query.filter) {
@@ -375,6 +377,22 @@ settingsRoutes.get(
375377
'data',
376378
];
377379

380+
const deepValueStrings = (obj: Record<string, unknown>): string[] => {
381+
const values = [];
382+
383+
for (const val of Object.values(obj)) {
384+
if (typeof val === 'string') {
385+
values.push(val);
386+
} else if (typeof val === 'number') {
387+
values.push(val.toString());
388+
} else if (val !== null && typeof val === 'object') {
389+
values.push(...deepValueStrings(val as Record<string, unknown>));
390+
}
391+
}
392+
393+
return values;
394+
};
395+
378396
try {
379397
fs.readFileSync(logFile, 'utf-8')
380398
.split('\n')
@@ -399,6 +417,19 @@ settingsRoutes.get(
399417
});
400418
}
401419

420+
if (req.query.search) {
421+
if (
422+
// label and data are sometimes undefined
423+
!searchRegexp.test(logMessage.label ?? '') &&
424+
!searchRegexp.test(logMessage.message) &&
425+
!deepValueStrings(logMessage.data ?? {}).some((val) =>
426+
searchRegexp.test(val)
427+
)
428+
) {
429+
return;
430+
}
431+
}
432+
402433
logs.push(logMessage);
403434
});
404435

src/components/Settings/SettingsLogs/index.tsx

Lines changed: 121 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Modal from '@app/components/Common/Modal';
55
import PageTitle from '@app/components/Common/PageTitle';
66
import Table from '@app/components/Common/Table';
77
import Tooltip from '@app/components/Common/Tooltip';
8+
import useDebouncedState from '@app/hooks/useDebouncedState';
89
import { useUpdateQueryParams } from '@app/hooks/useUpdateQueryParams';
910
import globalMessages from '@app/i18n/globalMessages';
1011
import Error from '@app/pages/_error';
@@ -17,6 +18,7 @@ import {
1718
FilterIcon,
1819
PauseIcon,
1920
PlayIcon,
21+
SearchIcon,
2022
} from '@heroicons/react/solid';
2123
import type {
2224
LogMessage,
@@ -59,6 +61,8 @@ const SettingsLogs = () => {
5961
const { addToast } = useToasts();
6062
const [currentFilter, setCurrentFilter] = useState<Filter>('debug');
6163
const [currentPageSize, setCurrentPageSize] = useState(25);
64+
const [searchFilter, debouncedSearchFilter, setSearchFilter] =
65+
useDebouncedState('');
6266
const [refreshInterval, setRefreshInterval] = useState(5000);
6367
const [activeLog, setActiveLog] = useState<{
6468
isOpen: boolean;
@@ -76,7 +80,9 @@ const SettingsLogs = () => {
7680
const { data, error } = useSWR<LogsResultsResponse>(
7781
`/api/v1/settings/logs?take=${currentPageSize}&skip=${
7882
pageIndex * currentPageSize
79-
}&filter=${currentFilter}`,
83+
}&filter=${currentFilter}${
84+
debouncedSearchFilter ? `&search=${debouncedSearchFilter}` : ''
85+
}`,
8086
{
8187
refreshInterval: refreshInterval,
8288
revalidateOnFocus: false,
@@ -118,15 +124,13 @@ const SettingsLogs = () => {
118124
});
119125
};
120126

121-
if (!data && !error) {
122-
return <LoadingSpinner />;
123-
}
124-
125-
if (!data) {
127+
// check if there's no data and no errors in the table
128+
// so as to show a spinner inside the table and not refresh the whole component
129+
if (!data && error) {
126130
return <Error statusCode={500} />;
127131
}
128132

129-
const hasNextPage = data.pageInfo.pages > pageIndex + 1;
133+
const hasNextPage = data?.pageInfo.pages ?? 0 > pageIndex + 1;
130134
const hasPrevPage = pageIndex > 0;
131135

132136
return (
@@ -245,10 +249,21 @@ const SettingsLogs = () => {
245249
appDataPath: appData ? appData.appDataPath : '/app/config',
246250
})}
247251
</p>
248-
<div className="mt-2 flex flex-grow flex-row sm:flex-grow-0 sm:justify-end">
252+
<div className="mt-2 flex flex-grow flex-col sm:flex-grow-0 sm:flex-row sm:justify-end">
253+
<div className="mb-2 flex flex-grow sm:mb-0 sm:mr-2 md:flex-grow-0">
254+
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
255+
<SearchIcon className="h-6 w-6" />
256+
</span>
257+
<input
258+
type="text"
259+
className="rounded-r-only"
260+
value={searchFilter}
261+
onChange={(e) => setSearchFilter(e.target.value as string)}
262+
/>
263+
</div>
249264
<div className="mb-2 flex flex-1 flex-row justify-between sm:mb-0 sm:flex-none">
250265
<Button
251-
className="mr-2 w-full flex-grow"
266+
className="mr-2 flex flex-grow"
252267
buttonType={refreshInterval ? 'default' : 'primary'}
253268
onClick={() => toggleLogs()}
254269
>
@@ -259,34 +274,34 @@ const SettingsLogs = () => {
259274
)}
260275
</span>
261276
</Button>
262-
</div>
263-
<div className="mb-2 flex flex-1 sm:mb-0 sm:flex-none">
264-
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
265-
<FilterIcon className="h-6 w-6" />
266-
</span>
267-
<select
268-
id="filter"
269-
name="filter"
270-
onChange={(e) => {
271-
setCurrentFilter(e.target.value as Filter);
272-
router.push(router.pathname);
273-
}}
274-
value={currentFilter}
275-
className="rounded-r-only"
276-
>
277-
<option value="debug">
278-
{intl.formatMessage(messages.filterDebug)}
279-
</option>
280-
<option value="info">
281-
{intl.formatMessage(messages.filterInfo)}
282-
</option>
283-
<option value="warn">
284-
{intl.formatMessage(messages.filterWarn)}
285-
</option>
286-
<option value="error">
287-
{intl.formatMessage(messages.filterError)}
288-
</option>
289-
</select>
277+
<div className="flex flex-grow">
278+
<span className="inline-flex cursor-default items-center rounded-l-md border border-r-0 border-gray-500 bg-gray-800 px-3 text-sm text-gray-100">
279+
<FilterIcon className="h-6 w-6" />
280+
</span>
281+
<select
282+
id="filter"
283+
name="filter"
284+
onChange={(e) => {
285+
setCurrentFilter(e.target.value as Filter);
286+
router.push(router.pathname);
287+
}}
288+
value={currentFilter}
289+
className="rounded-r-only"
290+
>
291+
<option value="debug">
292+
{intl.formatMessage(messages.filterDebug)}
293+
</option>
294+
<option value="info">
295+
{intl.formatMessage(messages.filterInfo)}
296+
</option>
297+
<option value="warn">
298+
{intl.formatMessage(messages.filterWarn)}
299+
</option>
300+
<option value="error">
301+
{intl.formatMessage(messages.filterError)}
302+
</option>
303+
</select>
304+
</div>
290305
</div>
291306
</div>
292307
<Table>
@@ -300,73 +315,81 @@ const SettingsLogs = () => {
300315
</tr>
301316
</thead>
302317
<Table.TBody>
303-
{data.results.map((row: LogMessage, index: number) => {
304-
return (
305-
<tr key={`log-list-${index}`}>
306-
<Table.TD className="text-gray-300">
307-
{intl.formatDate(row.timestamp, {
308-
year: 'numeric',
309-
month: 'short',
310-
day: '2-digit',
311-
hour: 'numeric',
312-
minute: 'numeric',
313-
second: 'numeric',
314-
})}
315-
</Table.TD>
316-
<Table.TD className="text-gray-300">
317-
<Badge
318-
badgeType={
319-
row.level === 'error'
320-
? 'danger'
321-
: row.level === 'warn'
322-
? 'warning'
323-
: row.level === 'info'
324-
? 'success'
325-
: 'default'
326-
}
327-
>
328-
{row.level.toUpperCase()}
329-
</Badge>
330-
</Table.TD>
331-
<Table.TD className="text-gray-300">
332-
{row.label ?? ''}
333-
</Table.TD>
334-
<Table.TD className="text-gray-300">{row.message}</Table.TD>
335-
<Table.TD className="-m-1 flex flex-wrap items-center justify-end">
336-
{row.data && (
318+
{!data ? (
319+
<tr>
320+
<Table.TD colSpan={5} noPadding>
321+
<LoadingSpinner />
322+
</Table.TD>
323+
</tr>
324+
) : (
325+
data.results.map((row: LogMessage, index: number) => {
326+
return (
327+
<tr key={`log-list-${index}`}>
328+
<Table.TD className="text-gray-300">
329+
{intl.formatDate(row.timestamp, {
330+
year: 'numeric',
331+
month: 'short',
332+
day: '2-digit',
333+
hour: 'numeric',
334+
minute: 'numeric',
335+
second: 'numeric',
336+
})}
337+
</Table.TD>
338+
<Table.TD className="text-gray-300">
339+
<Badge
340+
badgeType={
341+
row.level === 'error'
342+
? 'danger'
343+
: row.level === 'warn'
344+
? 'warning'
345+
: row.level === 'info'
346+
? 'success'
347+
: 'default'
348+
}
349+
>
350+
{row.level.toUpperCase()}
351+
</Badge>
352+
</Table.TD>
353+
<Table.TD className="text-gray-300">
354+
{row.label ?? ''}
355+
</Table.TD>
356+
<Table.TD className="text-gray-300">{row.message}</Table.TD>
357+
<Table.TD className="-m-1 flex flex-wrap items-center justify-end">
358+
{row.data && (
359+
<Tooltip
360+
content={intl.formatMessage(messages.viewdetails)}
361+
>
362+
<Button
363+
buttonSize="sm"
364+
buttonType="primary"
365+
onClick={() =>
366+
setActiveLog({ log: row, isOpen: true })
367+
}
368+
className="m-1"
369+
>
370+
<DocumentSearchIcon className="icon-md" />
371+
</Button>
372+
</Tooltip>
373+
)}
337374
<Tooltip
338-
content={intl.formatMessage(messages.viewdetails)}
375+
content={intl.formatMessage(messages.copyToClipboard)}
339376
>
340377
<Button
341378
buttonType="primary"
342379
buttonSize="sm"
343-
onClick={() =>
344-
setActiveLog({ log: row, isOpen: true })
345-
}
380+
onClick={() => copyLogString(row)}
346381
className="m-1"
347382
>
348-
<DocumentSearchIcon className="icon-md" />
383+
<ClipboardCopyIcon className="icon-md" />
349384
</Button>
350385
</Tooltip>
351-
)}
352-
<Tooltip
353-
content={intl.formatMessage(messages.copyToClipboard)}
354-
>
355-
<Button
356-
buttonType="primary"
357-
buttonSize="sm"
358-
onClick={() => copyLogString(row)}
359-
className="m-1"
360-
>
361-
<ClipboardCopyIcon className="icon-md" />
362-
</Button>
363-
</Tooltip>
364-
</Table.TD>
365-
</tr>
366-
);
367-
})}
386+
</Table.TD>
387+
</tr>
388+
);
389+
})
390+
)}
368391

369-
{data.results.length === 0 && (
392+
{data?.results.length === 0 && (
370393
<tr className="relative h-24 p-2 text-white">
371394
<Table.TD colSpan={5} noPadding>
372395
<div className="flex w-screen flex-col items-center justify-center p-6 md:w-full">
@@ -396,15 +419,15 @@ const SettingsLogs = () => {
396419
>
397420
<div className="hidden lg:flex lg:flex-1">
398421
<p className="text-sm">
399-
{data.results.length > 0 &&
422+
{(data?.results.length ?? 0) > 0 &&
400423
intl.formatMessage(globalMessages.showingresults, {
401424
from: pageIndex * currentPageSize + 1,
402425
to:
403-
data.results.length < currentPageSize
426+
data?.results.length ?? 0 < currentPageSize
404427
? pageIndex * currentPageSize +
405-
data.results.length
428+
(data?.results.length ?? 0)
406429
: (pageIndex + 1) * currentPageSize,
407-
total: data.pageInfo.results,
430+
total: data?.pageInfo.results ?? 0,
408431
strong: (msg: React.ReactNode) => (
409432
<span className="font-medium">{msg}</span>
410433
),

0 commit comments

Comments
 (0)