import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline'; import { PencilIcon } from '@heroicons/react/solid'; import axios from 'axios'; import React, { useState } from 'react'; import { defineMessages, FormattedRelativeTime, MessageDescriptor, useIntl, } from 'react-intl'; import { useToasts } from 'react-toast-notifications'; import useSWR from 'swr'; import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces'; import { JobId } from '../../../../server/lib/settings'; import Spinner from '../../../assets/spinner.svg'; import globalMessages from '../../../i18n/globalMessages'; import { formatBytes } from '../../../utils/numberHelpers'; import Badge from '../../Common/Badge'; import Button from '../../Common/Button'; import LoadingSpinner from '../../Common/LoadingSpinner'; import Modal from '../../Common/Modal'; import PageTitle from '../../Common/PageTitle'; import Table from '../../Common/Table'; import Transition from '../../Transition'; const messages: { [messageName: string]: MessageDescriptor } = defineMessages({ jobsandcache: 'Jobs & Cache', 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} canceled.', process: 'Process', command: 'Command', 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', unknownJob: 'Unknown Job', 'plex-recently-added-scan': 'Plex Recently Added Scan', 'plex-full-scan': 'Plex Full Library Scan', 'radarr-scan': 'Radarr Scan', 'sonarr-scan': 'Sonarr Scan', 'download-sync': 'Download Sync', 'download-sync-reset': 'Download Sync Reset', editJobSchedule: 'Modify Job', jobScheduleEditSaved: 'Job edited successfully!', jobScheduleEditFailed: 'Something went wrong while saving the job.', editJobSchedulePrompt: 'Frequency', editJobScheduleSelectorHours: 'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}', editJobScheduleSelectorMinutes: 'Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}', }); interface Job { id: JobId; name: string; type: 'process' | 'command'; interval: 'short' | 'long' | 'fixed'; nextExecutionTime: string; running: boolean; } const SettingsJobs: React.FC = () => { const intl = useIntl(); const { addToast } = useToasts(); const { data, error, mutate: revalidate, } = useSWR('/api/v1/settings/jobs', { refreshInterval: 5000, }); const { data: cacheData, mutate: cacheRevalidate } = useSWR( '/api/v1/settings/cache', { refreshInterval: 10000, } ); const [jobEditModal, setJobEditModal] = useState<{ isOpen: boolean; job?: Job; }>({ isOpen: false, }); const [isSaving, setIsSaving] = useState(false); const [jobScheduleMinutes, setJobScheduleMinutes] = useState(5); const [jobScheduleHours, setJobScheduleHours] = useState(1); if (!data && !error) { return ; } const runJob = async (job: Job) => { await axios.post(`/api/v1/settings/jobs/${job.id}/run`); addToast( intl.formatMessage(messages.jobstarted, { jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob), }), { appearance: 'success', autoDismiss: true, } ); revalidate(); }; const cancelJob = async (job: Job) => { await axios.post(`/api/v1/settings/jobs/${job.id}/cancel`); addToast( intl.formatMessage(messages.jobcancelled, { jobname: intl.formatMessage(messages[job.id] ?? messages.unknownJob), }), { appearance: 'error', autoDismiss: true, } ); revalidate(); }; const flushCache = async (cache: CacheItem) => { await axios.post(`/api/v1/settings/cache/${cache.id}/flush`); addToast( intl.formatMessage(messages.cacheflushed, { cachename: cache.name }), { appearance: 'success', autoDismiss: true, } ); cacheRevalidate(); }; const scheduleJob = async () => { const jobScheduleCron = ['0', '0', '*', '*', '*', '*']; try { if (jobEditModal.job?.interval === 'short') { jobScheduleCron[1] = `*/${jobScheduleMinutes}`; } else if (jobEditModal.job?.interval === 'long') { jobScheduleCron[2] = `*/${jobScheduleHours}`; } else { // jobs with interval: fixed should not be editable throw new Error(); } setIsSaving(true); await axios.post( `/api/v1/settings/jobs/${jobEditModal.job?.id}/schedule`, { schedule: jobScheduleCron.join(' '), } ); addToast(intl.formatMessage(messages.jobScheduleEditSaved), { appearance: 'success', autoDismiss: true, }); setJobEditModal({ isOpen: false }); revalidate(); } catch (e) { addToast(intl.formatMessage(messages.jobScheduleEditFailed), { appearance: 'error', autoDismiss: true, }); } finally { setIsSaving(false); } }; return ( <> } onCancel={() => setJobEditModal({ isOpen: false })} okDisabled={isSaving} onOk={() => scheduleJob()} >
{jobEditModal.job?.interval === 'short' ? ( ) : ( )}

{intl.formatMessage(messages.jobs)}

{intl.formatMessage(messages.jobsDescription)}

{intl.formatMessage(messages.jobname)}{intl.formatMessage(messages.jobtype)}{intl.formatMessage(messages.nextexecution)} {data?.map((job) => (
{intl.formatMessage( messages[job.id] ?? messages.unknownJob )} {job.running && }
{job.type === 'process' ? intl.formatMessage(messages.process) : intl.formatMessage(messages.command)}
{job.interval !== 'fixed' && ( )} {job.running ? ( ) : ( )} ))}

{intl.formatMessage(messages.cache)}

{intl.formatMessage(messages.cacheDescription)}

{intl.formatMessage(messages.cachename)}{intl.formatMessage(messages.cachehits)}{intl.formatMessage(messages.cachemisses)}{intl.formatMessage(messages.cachekeys)}{intl.formatMessage(messages.cacheksize)}{intl.formatMessage(messages.cachevsize)} {cacheData?.map((cache) => ( {cache.name}{intl.formatNumber(cache.stats.hits)}{intl.formatNumber(cache.stats.misses)}{intl.formatNumber(cache.stats.keys)}{formatBytes(cache.stats.ksize)}{formatBytes(cache.stats.vsize)} ))}
); }; export default SettingsJobs;