mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(logs): add copy to clipboard button to logs page
includes various other improvements to the logs page
This commit is contained in:
@@ -28,6 +28,7 @@
|
|||||||
"bowser": "^2.11.0",
|
"bowser": "^2.11.0",
|
||||||
"connect-typeorm": "^1.1.4",
|
"connect-typeorm": "^1.1.4",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
|
"copy-to-clipboard": "^3.3.1",
|
||||||
"country-flag-icons": "^1.2.9",
|
"country-flag-icons": "^1.2.9",
|
||||||
"csurf": "^1.11.0",
|
"csurf": "^1.11.0",
|
||||||
"email-templates": "^8.0.3",
|
"email-templates": "^8.0.3",
|
||||||
|
@@ -5,6 +5,7 @@ export type LogMessage = {
|
|||||||
level: string;
|
level: string;
|
||||||
label: string;
|
label: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
data?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface LogsResultsResponse extends PaginatedResponse {
|
export interface LogsResultsResponse extends PaginatedResponse {
|
||||||
|
@@ -271,14 +271,18 @@ settingsRoutes.get(
|
|||||||
const timestamp = line.match(new RegExp(/^.{24}/)) || [];
|
const timestamp = line.match(new RegExp(/^.{24}/)) || [];
|
||||||
const level = line.match(new RegExp(/\s\[\w+\]/)) || [];
|
const level = line.match(new RegExp(/\s\[\w+\]/)) || [];
|
||||||
const label = line.match(new RegExp(/[^\s]\[\w+\s*\w*\]/)) || [];
|
const label = line.match(new RegExp(/[^\s]\[\w+\s*\w*\]/)) || [];
|
||||||
const message = line.match(new RegExp(/:\s.*/)) || [];
|
const message = line.match(new RegExp(/:\s([^{}]+)({.*})?/)) || [];
|
||||||
|
|
||||||
if (level.length && filter.includes(level[0].slice(2, -1))) {
|
if (level.length && filter.includes(level[0].slice(2, -1))) {
|
||||||
logs.push({
|
logs.push({
|
||||||
timestamp: timestamp[0],
|
timestamp: timestamp[0],
|
||||||
level: level.length ? level[0].slice(2, -1) : '',
|
level: level.length ? level[0].slice(2, -1) : '',
|
||||||
label: label.length ? label[0].slice(2, -1) : '',
|
label: label.length ? label[0].slice(2, -1) : '',
|
||||||
message: message.length ? message[0].slice(2) : '',
|
message: message.length && message[1] ? message[1] : '',
|
||||||
|
data:
|
||||||
|
message.length && message[2]
|
||||||
|
? JSON.parse(message[2])
|
||||||
|
: undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import copy from 'copy-to-clipboard';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import {
|
import {
|
||||||
LogMessage,
|
LogMessage,
|
||||||
@@ -13,6 +14,9 @@ import PageTitle from '../../Common/PageTitle';
|
|||||||
import Table from '../../Common/Table';
|
import Table from '../../Common/Table';
|
||||||
import globalMessages from '../../../i18n/globalMessages';
|
import globalMessages from '../../../i18n/globalMessages';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import Modal from '../../Common/Modal';
|
||||||
|
import Transition from '../../Transition';
|
||||||
|
import { useToasts } from 'react-toast-notifications';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
logs: 'Logs',
|
logs: 'Logs',
|
||||||
@@ -35,6 +39,10 @@ const messages = defineMessages({
|
|||||||
previous: 'Previous',
|
previous: 'Previous',
|
||||||
pauseLogs: 'Pause',
|
pauseLogs: 'Pause',
|
||||||
resumeLogs: 'Resume',
|
resumeLogs: 'Resume',
|
||||||
|
viewDetails: 'View Details',
|
||||||
|
copyToClipboard: 'Copy to Clipboard',
|
||||||
|
logDetails: 'Log Details',
|
||||||
|
extraData: 'Extra Data',
|
||||||
});
|
});
|
||||||
|
|
||||||
type Filter = 'debug' | 'info' | 'warn' | 'error';
|
type Filter = 'debug' | 'info' | 'warn' | 'error';
|
||||||
@@ -42,9 +50,11 @@ type Filter = 'debug' | 'info' | 'warn' | 'error';
|
|||||||
const SettingsLogs: React.FC = () => {
|
const SettingsLogs: React.FC = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const { addToast } = useToasts();
|
||||||
const [currentFilter, setCurrentFilter] = useState<Filter>('debug');
|
const [currentFilter, setCurrentFilter] = useState<Filter>('debug');
|
||||||
const [currentPageSize, setCurrentPageSize] = useState(25);
|
const [currentPageSize, setCurrentPageSize] = useState(25);
|
||||||
const [refreshInterval, setRefreshInterval] = useState(5000);
|
const [refreshInterval, setRefreshInterval] = useState(5000);
|
||||||
|
const [activeLog, setActiveLog] = useState<LogMessage | null>(null);
|
||||||
|
|
||||||
const page = router.query.page ? Number(router.query.page) : 1;
|
const page = router.query.page ? Number(router.query.page) : 1;
|
||||||
const pageIndex = page - 1;
|
const pageIndex = page - 1;
|
||||||
@@ -86,6 +96,18 @@ const SettingsLogs: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}, [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('Copied log message to clipboard.', {
|
||||||
|
appearance: 'success',
|
||||||
|
autoDismiss: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
if (!data && !error) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
@@ -105,6 +127,101 @@ const SettingsLogs: React.FC = () => {
|
|||||||
intl.formatMessage(globalMessages.settings),
|
intl.formatMessage(globalMessages.settings),
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<Transition
|
||||||
|
enter="opacity-0 transition duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="opacity-100 transition duration-300"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
appear
|
||||||
|
show={!!activeLog}
|
||||||
|
>
|
||||||
|
<Modal
|
||||||
|
title={intl.formatMessage(messages.logDetails)}
|
||||||
|
onCancel={() => setActiveLog(null)}
|
||||||
|
cancelText={intl.formatMessage(globalMessages.close)}
|
||||||
|
onOk={() => (activeLog ? copyLogString(activeLog) : undefined)}
|
||||||
|
okText={intl.formatMessage(messages.copyToClipboard)}
|
||||||
|
okButtonType="primary"
|
||||||
|
>
|
||||||
|
{activeLog && (
|
||||||
|
<>
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="text-label">
|
||||||
|
{intl.formatMessage(messages.time)}
|
||||||
|
</div>
|
||||||
|
<div className="mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
|
||||||
|
<div className="flex items-center max-w-lg">
|
||||||
|
{intl.formatDate(activeLog.timestamp, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: 'numeric',
|
||||||
|
minute: 'numeric',
|
||||||
|
second: 'numeric',
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="text-label">
|
||||||
|
{intl.formatMessage(messages.level)}
|
||||||
|
</div>
|
||||||
|
<div className="mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
|
||||||
|
<div className="flex items-center max-w-lg">
|
||||||
|
<Badge
|
||||||
|
badgeType={
|
||||||
|
activeLog.level === 'error'
|
||||||
|
? 'danger'
|
||||||
|
: activeLog.level === 'warn'
|
||||||
|
? 'warning'
|
||||||
|
: activeLog.level === 'info'
|
||||||
|
? 'success'
|
||||||
|
: 'default'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{activeLog.level.toUpperCase()}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="text-label">
|
||||||
|
{intl.formatMessage(messages.label)}
|
||||||
|
</div>
|
||||||
|
<div className="mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
|
||||||
|
<div className="flex items-center max-w-lg">
|
||||||
|
{activeLog.label}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="text-label">
|
||||||
|
{intl.formatMessage(messages.message)}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
|
||||||
|
<div className="flex items-center max-w-lg">
|
||||||
|
{activeLog.message}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{activeLog.data && (
|
||||||
|
<div className="form-row">
|
||||||
|
<div className="text-label">
|
||||||
|
{intl.formatMessage(messages.extraData)}
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 mb-1 text-sm font-medium leading-5 text-gray-400 sm:mt-2">
|
||||||
|
<code className="block w-full px-6 py-4 overflow-auto whitespace-pre bg-gray-800 ring-1 ring-gray-700 max-h-64">
|
||||||
|
{JSON.stringify(activeLog.data, null, ' ')}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
|
</Transition>
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<h3 className="heading">{intl.formatMessage(messages.logs)}</h3>
|
<h3 className="heading">{intl.formatMessage(messages.logs)}</h3>
|
||||||
<p className="description">
|
<p className="description">
|
||||||
@@ -118,13 +235,44 @@ const SettingsLogs: React.FC = () => {
|
|||||||
<div className="flex flex-row flex-grow mt-2 sm:flex-grow-0 sm:justify-end">
|
<div className="flex flex-row flex-grow mt-2 sm:flex-grow-0 sm:justify-end">
|
||||||
<div className="flex flex-row justify-between flex-1 mb-2 sm:mb-0 sm:flex-none">
|
<div className="flex flex-row justify-between flex-1 mb-2 sm:mb-0 sm:flex-none">
|
||||||
<Button
|
<Button
|
||||||
className="flex-grow w-full mr-2 sm:w-24"
|
className="flex-grow w-full mr-2"
|
||||||
buttonType={refreshInterval ? 'default' : 'primary'}
|
buttonType={refreshInterval ? 'default' : 'primary'}
|
||||||
onClick={() => toggleLogs()}
|
onClick={() => toggleLogs()}
|
||||||
>
|
>
|
||||||
{intl.formatMessage(
|
<span>
|
||||||
refreshInterval ? messages.pauseLogs : messages.resumeLogs
|
{refreshInterval ? (
|
||||||
)}
|
<svg
|
||||||
|
className="w-5 h-5 mr-1"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zM7 8a1 1 0 012 0v4a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 mr-1"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{intl.formatMessage(
|
||||||
|
refreshInterval ? messages.pauseLogs : messages.resumeLogs
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 mb-2 sm:mb-0 sm:flex-none">
|
<div className="flex flex-1 mb-2 sm:mb-0 sm:flex-none">
|
||||||
@@ -174,6 +322,7 @@ const SettingsLogs: React.FC = () => {
|
|||||||
<Table.TH>{intl.formatMessage(messages.level)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.level)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.label)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.label)}</Table.TH>
|
||||||
<Table.TH>{intl.formatMessage(messages.message)}</Table.TH>
|
<Table.TH>{intl.formatMessage(messages.message)}</Table.TH>
|
||||||
|
<Table.TH></Table.TH>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<Table.TBody>
|
<Table.TBody>
|
||||||
@@ -207,6 +356,31 @@ const SettingsLogs: React.FC = () => {
|
|||||||
</Table.TD>
|
</Table.TD>
|
||||||
<Table.TD className="text-gray-300">{row.label}</Table.TD>
|
<Table.TD className="text-gray-300">{row.label}</Table.TD>
|
||||||
<Table.TD className="text-gray-300">{row.message}</Table.TD>
|
<Table.TD className="text-gray-300">{row.message}</Table.TD>
|
||||||
|
<Table.TD className="flex items-center justify-end">
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
buttonSize="sm"
|
||||||
|
onClick={() => setActiveLog(row)}
|
||||||
|
className="mr-2"
|
||||||
|
>
|
||||||
|
{intl.formatMessage(messages.viewDetails)}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
buttonType="primary"
|
||||||
|
buttonSize="sm"
|
||||||
|
onClick={() => copyLogString(row)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 text-white"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M8 2a1 1 0 000 2h2a1 1 0 100-2H8z" />
|
||||||
|
<path d="M3 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v6h-4.586l1.293-1.293a1 1 0 00-1.414-1.414l-3 3a1 1 0 000 1.414l3 3a1 1 0 001.414-1.414L10.414 13H15v3a2 2 0 01-2 2H5a2 2 0 01-2-2V5zM15 11h2a1 1 0 110 2h-2v-2z" />
|
||||||
|
</svg>
|
||||||
|
</Button>
|
||||||
|
</Table.TD>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@@ -451,12 +451,15 @@
|
|||||||
"components.Settings.SettingsJobsCache.runnow": "Run Now",
|
"components.Settings.SettingsJobsCache.runnow": "Run Now",
|
||||||
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan",
|
"components.Settings.SettingsJobsCache.sonarr-scan": "Sonarr Scan",
|
||||||
"components.Settings.SettingsJobsCache.unknownJob": "Unknown Job",
|
"components.Settings.SettingsJobsCache.unknownJob": "Unknown Job",
|
||||||
|
"components.Settings.SettingsLogs.copyToClipboard": "Copy to Clipboard",
|
||||||
|
"components.Settings.SettingsLogs.extraData": "Extra Data",
|
||||||
"components.Settings.SettingsLogs.filterDebug": "Debug",
|
"components.Settings.SettingsLogs.filterDebug": "Debug",
|
||||||
"components.Settings.SettingsLogs.filterError": "Error",
|
"components.Settings.SettingsLogs.filterError": "Error",
|
||||||
"components.Settings.SettingsLogs.filterInfo": "Info",
|
"components.Settings.SettingsLogs.filterInfo": "Info",
|
||||||
"components.Settings.SettingsLogs.filterWarn": "Warning",
|
"components.Settings.SettingsLogs.filterWarn": "Warning",
|
||||||
"components.Settings.SettingsLogs.label": "Label",
|
"components.Settings.SettingsLogs.label": "Label",
|
||||||
"components.Settings.SettingsLogs.level": "Severity",
|
"components.Settings.SettingsLogs.level": "Severity",
|
||||||
|
"components.Settings.SettingsLogs.logDetails": "Log Details",
|
||||||
"components.Settings.SettingsLogs.logs": "Logs",
|
"components.Settings.SettingsLogs.logs": "Logs",
|
||||||
"components.Settings.SettingsLogs.logsDescription": "You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/overseerr.log</code>.",
|
"components.Settings.SettingsLogs.logsDescription": "You can also view these logs directly via <code>stdout</code>, or in <code>{configDir}/logs/overseerr.log</code>.",
|
||||||
"components.Settings.SettingsLogs.message": "Message",
|
"components.Settings.SettingsLogs.message": "Message",
|
||||||
@@ -469,6 +472,7 @@
|
|||||||
"components.Settings.SettingsLogs.showall": "Show All Logs",
|
"components.Settings.SettingsLogs.showall": "Show All Logs",
|
||||||
"components.Settings.SettingsLogs.showingresults": "Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results",
|
"components.Settings.SettingsLogs.showingresults": "Showing <strong>{from}</strong> to <strong>{to}</strong> of <strong>{total}</strong> results",
|
||||||
"components.Settings.SettingsLogs.time": "Timestamp",
|
"components.Settings.SettingsLogs.time": "Timestamp",
|
||||||
|
"components.Settings.SettingsLogs.viewDetails": "View Details",
|
||||||
"components.Settings.SettingsUsers.defaultPermissions": "Default User Permissions",
|
"components.Settings.SettingsUsers.defaultPermissions": "Default User Permissions",
|
||||||
"components.Settings.SettingsUsers.localLogin": "Enable Local User Sign-In",
|
"components.Settings.SettingsUsers.localLogin": "Enable Local User Sign-In",
|
||||||
"components.Settings.SettingsUsers.save": "Save Changes",
|
"components.Settings.SettingsUsers.save": "Save Changes",
|
||||||
|
Reference in New Issue
Block a user