feat(cache): add cache table and flush cache option to settings

also increases tmdb cache times to about 6 hours (12 hours for detail requests)
This commit is contained in:
sct
2021-01-31 13:11:12 +00:00
parent 3c5ae360fd
commit 996bd9f14e
12 changed files with 363 additions and 178 deletions

View File

@@ -7,18 +7,7 @@ import type {
ServiceCommonServerWithDetails,
} from '../../../../server/interfaces/api/serviceInterfaces';
import { defineMessages, useIntl } from 'react-intl';
const formatBytes = (bytes: number, decimals = 2) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};
import { formatBytes } from '../../../utils/numberHelpers';
const messages = defineMessages({
advancedoptions: 'Advanced Options',

View File

@@ -1,118 +0,0 @@
import React from 'react';
import useSWR from 'swr';
import LoadingSpinner from '../Common/LoadingSpinner';
import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
import Button from '../Common/Button';
import Table from '../Common/Table';
import Spinner from '../../assets/spinner.svg';
import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import Badge from '../Common/Badge';
const messages = defineMessages({
jobname: 'Job Name',
jobtype: 'Type',
nextexecution: 'Next Execution',
runnow: 'Run Now',
canceljob: 'Cancel Job',
jobstarted: '{jobname} started.',
jobcancelled: '{jobname} cancelled.',
});
interface Job {
id: string;
name: string;
type: 'process' | 'command';
nextExecutionTime: string;
running: boolean;
}
const SettingsJobs: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR<Job[]>('/api/v1/settings/jobs', {
refreshInterval: 5000,
});
if (!data && !error) {
return <LoadingSpinner />;
}
const runJob = async (job: Job) => {
await axios.get(`/api/v1/settings/jobs/${job.id}/run`);
addToast(
intl.formatMessage(messages.jobstarted, {
jobname: job.name,
}),
{
appearance: 'success',
autoDismiss: true,
}
);
revalidate();
};
const cancelJob = async (job: Job) => {
await axios.get(`/api/v1/settings/jobs/${job.id}/cancel`);
addToast(intl.formatMessage(messages.jobcancelled, { jobname: job.name }), {
appearance: 'error',
autoDismiss: true,
});
revalidate();
};
return (
<Table>
<thead>
<Table.TH>{intl.formatMessage(messages.jobname)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.jobtype)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.nextexecution)}</Table.TH>
<Table.TH></Table.TH>
</thead>
<Table.TBody>
{data?.map((job) => (
<tr key={`job-list-${job.id}`}>
<Table.TD>
<div className="flex items-center text-sm leading-5 text-white">
{job.running && <Spinner className="w-5 h-5 mr-2" />}
<span>{job.name}</span>
</div>
</Table.TD>
<Table.TD>
<Badge
badgeType={job.type === 'process' ? 'primary' : 'warning'}
className="uppercase"
>
{job.type}
</Badge>
</Table.TD>
<Table.TD>
<div className="text-sm leading-5 text-white">
<FormattedRelativeTime
value={Math.floor(
(new Date(job.nextExecutionTime).getTime() - Date.now()) /
1000
)}
updateIntervalInSeconds={1}
/>
</div>
</Table.TD>
<Table.TD alignText="right">
{job.running ? (
<Button buttonType="danger" onClick={() => cancelJob(job)}>
{intl.formatMessage(messages.canceljob)}
</Button>
) : (
<Button buttonType="primary" onClick={() => runJob(job)}>
{intl.formatMessage(messages.runnow)}
</Button>
)}
</Table.TD>
</tr>
))}
</Table.TBody>
</Table>
);
};
export default SettingsJobs;

View File

@@ -0,0 +1,198 @@
import React from 'react';
import useSWR from 'swr';
import LoadingSpinner from '../../Common/LoadingSpinner';
import { FormattedRelativeTime, defineMessages, useIntl } from 'react-intl';
import Button from '../../Common/Button';
import Table from '../../Common/Table';
import Spinner from '../../../assets/spinner.svg';
import axios from 'axios';
import { useToasts } from 'react-toast-notifications';
import Badge from '../../Common/Badge';
import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
import { formatBytes } from '../../../utils/numberHelpers';
const messages = defineMessages({
jobs: 'Jobs',
jobsDescription:
'Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.',
jobname: 'Job Name',
jobtype: 'Type',
nextexecution: 'Next Execution',
runnow: 'Run Now',
canceljob: 'Cancel Job',
jobstarted: '{jobname} started.',
jobcancelled: '{jobname} cancelled.',
cache: 'Cache',
cacheDescription:
'Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.',
cacheflushed: '{cachename} cache flushed.',
cachename: 'Cache Name',
cachehits: 'Hits',
cachemisses: 'Misses',
cachekeys: 'Total Keys',
cacheksize: 'Key Size',
cachevsize: 'Value Size',
flushcache: 'Flush Cache',
});
interface Job {
id: string;
name: string;
type: 'process' | 'command';
nextExecutionTime: string;
running: boolean;
}
const SettingsJobs: React.FC = () => {
const intl = useIntl();
const { addToast } = useToasts();
const { data, error, revalidate } = useSWR<Job[]>('/api/v1/settings/jobs', {
refreshInterval: 5000,
});
const { data: cacheData, revalidate: cacheRevalidate } = useSWR<CacheItem[]>(
'/api/v1/settings/cache',
{
refreshInterval: 10000,
}
);
if (!data && !error) {
return <LoadingSpinner />;
}
const runJob = async (job: Job) => {
await axios.get(`/api/v1/settings/jobs/${job.id}/run`);
addToast(
intl.formatMessage(messages.jobstarted, {
jobname: job.name,
}),
{
appearance: 'success',
autoDismiss: true,
}
);
revalidate();
};
const cancelJob = async (job: Job) => {
await axios.get(`/api/v1/settings/jobs/${job.id}/cancel`);
addToast(intl.formatMessage(messages.jobcancelled, { jobname: job.name }), {
appearance: 'error',
autoDismiss: true,
});
revalidate();
};
const flushCache = async (cache: CacheItem) => {
await axios.get(`/api/v1/settings/cache/${cache.id}/flush`);
addToast(
intl.formatMessage(messages.cacheflushed, { cachename: cache.name }),
{
appearance: 'success',
autoDismiss: true,
}
);
cacheRevalidate();
};
return (
<>
<div className="mb-4">
<h3 className="text-lg font-medium leading-6 text-gray-200">
{intl.formatMessage(messages.jobs)}
</h3>
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
{intl.formatMessage(messages.jobsDescription)}
</p>
</div>
<Table>
<thead>
<Table.TH>{intl.formatMessage(messages.jobname)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.jobtype)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.nextexecution)}</Table.TH>
<Table.TH></Table.TH>
</thead>
<Table.TBody>
{data?.map((job) => (
<tr key={`job-list-${job.id}`}>
<Table.TD>
<div className="flex items-center text-sm leading-5 text-white">
{job.running && <Spinner className="w-5 h-5 mr-2" />}
<span>{job.name}</span>
</div>
</Table.TD>
<Table.TD>
<Badge
badgeType={job.type === 'process' ? 'primary' : 'warning'}
className="uppercase"
>
{job.type}
</Badge>
</Table.TD>
<Table.TD>
<div className="text-sm leading-5 text-white">
<FormattedRelativeTime
value={Math.floor(
(new Date(job.nextExecutionTime).getTime() - Date.now()) /
1000
)}
updateIntervalInSeconds={1}
/>
</div>
</Table.TD>
<Table.TD alignText="right">
{job.running ? (
<Button buttonType="danger" onClick={() => cancelJob(job)}>
{intl.formatMessage(messages.canceljob)}
</Button>
) : (
<Button buttonType="primary" onClick={() => runJob(job)}>
{intl.formatMessage(messages.runnow)}
</Button>
)}
</Table.TD>
</tr>
))}
</Table.TBody>
</Table>
<div className="my-4">
<h3 className="text-lg font-medium leading-6 text-gray-200">
{intl.formatMessage(messages.cache)}
</h3>
<p className="max-w-2xl mt-1 text-sm leading-5 text-gray-500">
{intl.formatMessage(messages.cacheDescription)}
</p>
</div>
<Table>
<thead>
<Table.TH>{intl.formatMessage(messages.cachename)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.cachehits)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.cachemisses)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.cachekeys)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.cacheksize)}</Table.TH>
<Table.TH>{intl.formatMessage(messages.cachevsize)}</Table.TH>
<Table.TH></Table.TH>
</thead>
<Table.TBody>
{cacheData?.map((cache) => (
<tr key={`cache-list-${cache.id}`}>
<Table.TD>{cache.name}</Table.TD>
<Table.TD>{cache.stats.hits}</Table.TD>
<Table.TD>{cache.stats.misses}</Table.TD>
<Table.TD>{cache.stats.keys}</Table.TD>
<Table.TD>{formatBytes(cache.stats.ksize)}</Table.TD>
<Table.TD>{formatBytes(cache.stats.vsize)}</Table.TD>
<Table.TD alignText="right">
<Button buttonType="danger" onClick={() => flushCache(cache)}>
{intl.formatMessage(messages.flushcache)}
</Button>
</Table.TD>
</tr>
))}
</Table.TBody>
</Table>
</>
);
};
export default SettingsJobs;

View File

@@ -9,7 +9,7 @@ const messages = defineMessages({
menuServices: 'Services',
menuNotifications: 'Notifications',
menuLogs: 'Logs',
menuJobs: 'Jobs',
menuJobs: 'Jobs & Cache',
menuAbout: 'About',
});
@@ -106,7 +106,7 @@ const SettingsLayout: React.FC = ({ children }) => {
)?.route
}
aria-label="Selected tab"
className="bg-gray-800 text-white mt-1 rounded-md form-select block w-full pl-3 pr-10 py-2 text-base leading-6 border-gray-700 focus:outline-none focus:ring-blue focus:border-blue-300 sm:text-sm sm:leading-5 transition ease-in-out duration-150"
className="block w-full py-2 pl-3 pr-10 mt-1 text-base leading-6 text-white transition duration-150 ease-in-out bg-gray-800 border-gray-700 rounded-md form-select focus:outline-none focus:ring-blue focus:border-blue-300 sm:text-sm sm:leading-5"
>
{settingsRoutes.map((route, index) => (
<SettingsLink
@@ -122,7 +122,7 @@ const SettingsLayout: React.FC = ({ children }) => {
</div>
<div className="hidden sm:block">
<div className="border-b border-gray-600">
<nav className="-mb-px flex">
<nav className="flex -mb-px">
{settingsRoutes.map((route, index) => (
<SettingsLink
route={route.route}

View File

@@ -345,6 +345,25 @@
"components.Settings.SettingsAbout.totalmedia": "Total Media",
"components.Settings.SettingsAbout.totalrequests": "Total Requests",
"components.Settings.SettingsAbout.version": "Version",
"components.Settings.SettingsJobsCache.cache": "Cache",
"components.Settings.SettingsJobsCache.cacheDescription": "Overseerr caches requests to external API endpoints to optimize performance and avoid making unnecessary API calls.",
"components.Settings.SettingsJobsCache.cacheflushed": "{cachename} cache flushed.",
"components.Settings.SettingsJobsCache.cachehits": "Hits",
"components.Settings.SettingsJobsCache.cachekeys": "Total Keys",
"components.Settings.SettingsJobsCache.cacheksize": "Key Size",
"components.Settings.SettingsJobsCache.cachemisses": "Misses",
"components.Settings.SettingsJobsCache.cachename": "Cache Name",
"components.Settings.SettingsJobsCache.cachevsize": "Value Size",
"components.Settings.SettingsJobsCache.canceljob": "Cancel Job",
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} cancelled.",
"components.Settings.SettingsJobsCache.jobname": "Job Name",
"components.Settings.SettingsJobsCache.jobs": "Jobs",
"components.Settings.SettingsJobsCache.jobsDescription": "Overseerr performs certain maintenance tasks as regularly-scheduled jobs, but they can also be manually triggered below. Manually running a job will not alter its schedule.",
"components.Settings.SettingsJobsCache.jobstarted": "{jobname} started.",
"components.Settings.SettingsJobsCache.jobtype": "Type",
"components.Settings.SettingsJobsCache.nextexecution": "Next Execution",
"components.Settings.SettingsJobsCache.runnow": "Run Now",
"components.Settings.SonarrModal.add": "Add Server",
"components.Settings.SonarrModal.animequalityprofile": "Anime Quality Profile",
"components.Settings.SonarrModal.animerootfolder": "Anime Root Folder",
@@ -393,7 +412,6 @@
"components.Settings.apikey": "API Key",
"components.Settings.applicationurl": "Application URL",
"components.Settings.autoapprovedrequests": "Send Notifications for Auto-Approved Requests",
"components.Settings.canceljob": "Cancel Job",
"components.Settings.cancelscan": "Cancel Scan",
"components.Settings.copied": "Copied API key to clipboard.",
"components.Settings.csrfProtection": "Enable CSRF Protection",
@@ -410,22 +428,17 @@
"components.Settings.generalsettingsDescription": "Configure global and default settings for Overseerr.",
"components.Settings.hideAvailable": "Hide Available Media",
"components.Settings.hostname": "Hostname/IP",
"components.Settings.jobcancelled": "{jobname} cancelled.",
"components.Settings.jobname": "Job Name",
"components.Settings.jobstarted": "{jobname} started.",
"components.Settings.jobtype": "Type",
"components.Settings.librariesRemaining": "Libraries Remaining: {count}",
"components.Settings.manualscan": "Manual Library Scan",
"components.Settings.manualscanDescription": "Normally, this will only be run once every 24 hours. Overseerr will check your Plex server's recently added more aggressively. If this is your first time configuring Plex, a one-time full manual library scan is recommended!",
"components.Settings.menuAbout": "About",
"components.Settings.menuGeneralSettings": "General Settings",
"components.Settings.menuJobs": "Jobs",
"components.Settings.menuJobs": "Jobs & Cache",
"components.Settings.menuLogs": "Logs",
"components.Settings.menuNotifications": "Notifications",
"components.Settings.menuPlexSettings": "Plex",
"components.Settings.menuServices": "Services",
"components.Settings.ms": "ms",
"components.Settings.nextexecution": "Next Execution",
"components.Settings.nodefault": "No default server selected!",
"components.Settings.nodefaultdescription": "At least one server must be marked as default before any requests will make it to your services.",
"components.Settings.notificationAgentSettingsDescription": "Choose the types of notifications to send, and which notification agents to use.",
@@ -442,7 +455,6 @@
"components.Settings.port": "Port",
"components.Settings.radarrSettingsDescription": "Set up your Radarr connection below. You can have multiple, but only two active as defaults at any time (one for standard HD, and one for 4K). Administrators can override the server is used for new requests.",
"components.Settings.radarrsettings": "Radarr Settings",
"components.Settings.runnow": "Run Now",
"components.Settings.save": "Save Changes",
"components.Settings.saving": "Saving…",
"components.Settings.serverConnected": "connected",

View File

@@ -1,7 +1,7 @@
import React from 'react';
import type { NextPage } from 'next';
import SettingsLayout from '../../components/Settings/SettingsLayout';
import SettingsJobs from '../../components/Settings/SettingsJobs';
import SettingsJobs from '../../components/Settings/SettingsJobsCache';
import { Permission } from '../../hooks/useUser';
import useRouteGuard from '../../hooks/useRouteGuard';

View File

@@ -0,0 +1,11 @@
export const formatBytes = (bytes: number, decimals = 2): string => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
};