mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(jobs): allow modifying job schedules (#1440)
* feat(jobs): backend implementation * feat(jobs): initial frontend implementation * feat(jobs): store job settings as Record * feat(jobs): use heroicons/react instead of inline svgs * feat(jobs): use presets instead of cron expressions * feat(jobs): ran `yarn i18n:extract` * feat(jobs): suggested changes - use job ids in settings - add intervalDuration to jobs to allow choosing only minutes or hours for the job schedule - move job schedule defaults to settings.json - better TS types for jobs in settings cache component - make suggested changes to wording - plural form for label when job schedule can be defined in minutes - add fixed job interval duration - add predefined interval choices for minutes and hours - add new schema for job to overseerr api * feat(jobs): required change for CI to not fail * feat(jobs): suggested changes * fix(jobs): revert offending type refactor
This commit is contained in:

committed by
GitHub

parent
5683f55ebf
commit
82614ca441
@@ -1278,6 +1278,27 @@ components:
|
|||||||
allowSelfSigned:
|
allowSelfSigned:
|
||||||
type: boolean
|
type: boolean
|
||||||
example: false
|
example: false
|
||||||
|
Job:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
example: job-name
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
enum: [process, command]
|
||||||
|
interval:
|
||||||
|
type: string
|
||||||
|
enum: [short, long, fixed]
|
||||||
|
name:
|
||||||
|
type: string
|
||||||
|
example: A Job Name
|
||||||
|
nextExecutionTime:
|
||||||
|
type: string
|
||||||
|
example: '2020-09-02T05:02:23.000Z'
|
||||||
|
running:
|
||||||
|
type: boolean
|
||||||
|
example: false
|
||||||
PersonDetail:
|
PersonDetail:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
@@ -2214,23 +2235,7 @@ paths:
|
|||||||
schema:
|
schema:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: object
|
$ref: '#/components/schemas/Job'
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
example: job-name
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
example: A Job Name
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
enum: [process, command]
|
|
||||||
nextExecutionTime:
|
|
||||||
type: string
|
|
||||||
example: '2020-09-02T05:02:23.000Z'
|
|
||||||
running:
|
|
||||||
type: boolean
|
|
||||||
example: false
|
|
||||||
/settings/jobs/{jobId}/run:
|
/settings/jobs/{jobId}/run:
|
||||||
post:
|
post:
|
||||||
summary: Invoke a specific job
|
summary: Invoke a specific job
|
||||||
@@ -2249,23 +2254,7 @@ paths:
|
|||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
$ref: '#/components/schemas/Job'
|
||||||
properties:
|
|
||||||
id:
|
|
||||||
type: string
|
|
||||||
example: job-name
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
enum: [process, command]
|
|
||||||
name:
|
|
||||||
type: string
|
|
||||||
example: A Job Name
|
|
||||||
nextExecutionTime:
|
|
||||||
type: string
|
|
||||||
example: '2020-09-02T05:02:23.000Z'
|
|
||||||
running:
|
|
||||||
type: boolean
|
|
||||||
example: false
|
|
||||||
/settings/jobs/{jobId}/cancel:
|
/settings/jobs/{jobId}/cancel:
|
||||||
post:
|
post:
|
||||||
summary: Cancel a specific job
|
summary: Cancel a specific job
|
||||||
@@ -2281,26 +2270,39 @@ paths:
|
|||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: Canceled job returned
|
description: Canceled job returned
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/Job'
|
||||||
|
/settings/jobs/{jobId}/schedule:
|
||||||
|
post:
|
||||||
|
summary: Modify job schedule
|
||||||
|
description: Re-registers the job with the schedule specified. Will return the job in JSON format.
|
||||||
|
tags:
|
||||||
|
- settings
|
||||||
|
parameters:
|
||||||
|
- in: path
|
||||||
|
name: jobId
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
requestBody:
|
||||||
|
required: true
|
||||||
content:
|
content:
|
||||||
application/json:
|
application/json:
|
||||||
schema:
|
schema:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
id:
|
schedule:
|
||||||
type: string
|
type: string
|
||||||
example: job-name
|
example: '0 */5 * * * *'
|
||||||
type:
|
responses:
|
||||||
type: string
|
'200':
|
||||||
enum: [process, command]
|
description: Rescheduled job
|
||||||
name:
|
content:
|
||||||
type: string
|
application/json:
|
||||||
example: A Job Name
|
schema:
|
||||||
nextExecutionTime:
|
$ref: '#/components/schemas/Job'
|
||||||
type: string
|
|
||||||
example: '2020-09-02T05:02:23.000Z'
|
|
||||||
running:
|
|
||||||
type: boolean
|
|
||||||
example: false
|
|
||||||
/settings/cache:
|
/settings/cache:
|
||||||
get:
|
get:
|
||||||
summary: Get a list of active caches
|
summary: Get a list of active caches
|
||||||
@@ -2398,7 +2400,7 @@ paths:
|
|||||||
example: Server ready on port 5055
|
example: Server ready on port 5055
|
||||||
timestamp:
|
timestamp:
|
||||||
type: string
|
type: string
|
||||||
example: 2020-12-15T16:20:00.069Z
|
example: '2020-12-15T16:20:00.069Z'
|
||||||
/settings/notifications/email:
|
/settings/notifications/email:
|
||||||
get:
|
get:
|
||||||
summary: Get email notification settings
|
summary: Get email notification settings
|
||||||
|
@@ -1,15 +1,17 @@
|
|||||||
import schedule from 'node-schedule';
|
import schedule from 'node-schedule';
|
||||||
import logger from '../logger';
|
|
||||||
import downloadTracker from '../lib/downloadtracker';
|
import downloadTracker from '../lib/downloadtracker';
|
||||||
import { plexFullScanner, plexRecentScanner } from '../lib/scanners/plex';
|
import { plexFullScanner, plexRecentScanner } from '../lib/scanners/plex';
|
||||||
import { radarrScanner } from '../lib/scanners/radarr';
|
import { radarrScanner } from '../lib/scanners/radarr';
|
||||||
import { sonarrScanner } from '../lib/scanners/sonarr';
|
import { sonarrScanner } from '../lib/scanners/sonarr';
|
||||||
|
import { getSettings, JobId } from '../lib/settings';
|
||||||
|
import logger from '../logger';
|
||||||
|
|
||||||
interface ScheduledJob {
|
interface ScheduledJob {
|
||||||
id: string;
|
id: JobId;
|
||||||
job: schedule.Job;
|
job: schedule.Job;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'process' | 'command';
|
type: 'process' | 'command';
|
||||||
|
interval: 'short' | 'long' | 'fixed';
|
||||||
running?: () => boolean;
|
running?: () => boolean;
|
||||||
cancelFn?: () => void;
|
cancelFn?: () => void;
|
||||||
}
|
}
|
||||||
@@ -17,12 +19,15 @@ interface ScheduledJob {
|
|||||||
export const scheduledJobs: ScheduledJob[] = [];
|
export const scheduledJobs: ScheduledJob[] = [];
|
||||||
|
|
||||||
export const startJobs = (): void => {
|
export const startJobs = (): void => {
|
||||||
|
const jobs = getSettings().jobs;
|
||||||
|
|
||||||
// Run recently added plex scan every 5 minutes
|
// Run recently added plex scan every 5 minutes
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'plex-recently-added-scan',
|
id: 'plex-recently-added-scan',
|
||||||
name: 'Plex Recently Added Scan',
|
name: 'Plex Recently Added Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
job: schedule.scheduleJob('0 */5 * * * *', () => {
|
interval: 'short',
|
||||||
|
job: schedule.scheduleJob(jobs['plex-recently-added-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Plex Recently Added Scan', {
|
logger.info('Starting scheduled job: Plex Recently Added Scan', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
@@ -37,7 +42,8 @@ export const startJobs = (): void => {
|
|||||||
id: 'plex-full-scan',
|
id: 'plex-full-scan',
|
||||||
name: 'Plex Full Library Scan',
|
name: 'Plex Full Library Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
job: schedule.scheduleJob('0 0 3 * * *', () => {
|
interval: 'long',
|
||||||
|
job: schedule.scheduleJob(jobs['plex-full-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Plex Full Library Scan', {
|
logger.info('Starting scheduled job: Plex Full Library Scan', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
@@ -52,7 +58,8 @@ export const startJobs = (): void => {
|
|||||||
id: 'radarr-scan',
|
id: 'radarr-scan',
|
||||||
name: 'Radarr Scan',
|
name: 'Radarr Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
job: schedule.scheduleJob('0 0 4 * * *', () => {
|
interval: 'long',
|
||||||
|
job: schedule.scheduleJob(jobs['radarr-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
|
logger.info('Starting scheduled job: Radarr Scan', { label: 'Jobs' });
|
||||||
radarrScanner.run();
|
radarrScanner.run();
|
||||||
}),
|
}),
|
||||||
@@ -65,7 +72,8 @@ export const startJobs = (): void => {
|
|||||||
id: 'sonarr-scan',
|
id: 'sonarr-scan',
|
||||||
name: 'Sonarr Scan',
|
name: 'Sonarr Scan',
|
||||||
type: 'process',
|
type: 'process',
|
||||||
job: schedule.scheduleJob('0 30 4 * * *', () => {
|
interval: 'long',
|
||||||
|
job: schedule.scheduleJob(jobs['sonarr-scan'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
|
logger.info('Starting scheduled job: Sonarr Scan', { label: 'Jobs' });
|
||||||
sonarrScanner.run();
|
sonarrScanner.run();
|
||||||
}),
|
}),
|
||||||
@@ -73,23 +81,27 @@ export const startJobs = (): void => {
|
|||||||
cancelFn: () => sonarrScanner.cancel(),
|
cancelFn: () => sonarrScanner.cancel(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Run download sync
|
// Run download sync every minute
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'download-sync',
|
id: 'download-sync',
|
||||||
name: 'Download Sync',
|
name: 'Download Sync',
|
||||||
type: 'command',
|
type: 'command',
|
||||||
job: schedule.scheduleJob('0 * * * * *', () => {
|
interval: 'fixed',
|
||||||
logger.debug('Starting scheduled job: Download Sync', { label: 'Jobs' });
|
job: schedule.scheduleJob(jobs['download-sync'].schedule, () => {
|
||||||
|
logger.debug('Starting scheduled job: Download Sync', {
|
||||||
|
label: 'Jobs',
|
||||||
|
});
|
||||||
downloadTracker.updateDownloads();
|
downloadTracker.updateDownloads();
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reset download sync
|
// Reset download sync everyday at 01:00 am
|
||||||
scheduledJobs.push({
|
scheduledJobs.push({
|
||||||
id: 'download-sync-reset',
|
id: 'download-sync-reset',
|
||||||
name: 'Download Sync Reset',
|
name: 'Download Sync Reset',
|
||||||
type: 'command',
|
type: 'command',
|
||||||
job: schedule.scheduleJob('0 0 1 * * *', () => {
|
interval: 'long',
|
||||||
|
job: schedule.scheduleJob(jobs['download-sync-reset'].schedule, () => {
|
||||||
logger.info('Starting scheduled job: Download Sync Reset', {
|
logger.info('Starting scheduled job: Download Sync Reset', {
|
||||||
label: 'Jobs',
|
label: 'Jobs',
|
||||||
});
|
});
|
||||||
|
@@ -215,6 +215,18 @@ interface NotificationSettings {
|
|||||||
agents: NotificationAgents;
|
agents: NotificationAgents;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface JobSettings {
|
||||||
|
schedule: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type JobId =
|
||||||
|
| 'plex-recently-added-scan'
|
||||||
|
| 'plex-full-scan'
|
||||||
|
| 'radarr-scan'
|
||||||
|
| 'sonarr-scan'
|
||||||
|
| 'download-sync'
|
||||||
|
| 'download-sync-reset';
|
||||||
|
|
||||||
interface AllSettings {
|
interface AllSettings {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
vapidPublic: string;
|
vapidPublic: string;
|
||||||
@@ -225,6 +237,7 @@ interface AllSettings {
|
|||||||
sonarr: SonarrSettings[];
|
sonarr: SonarrSettings[];
|
||||||
public: PublicSettings;
|
public: PublicSettings;
|
||||||
notifications: NotificationSettings;
|
notifications: NotificationSettings;
|
||||||
|
jobs: Record<JobId, JobSettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
const SETTINGS_PATH = process.env.CONFIG_DIRECTORY
|
||||||
@@ -346,6 +359,26 @@ class Settings {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
jobs: {
|
||||||
|
'plex-recently-added-scan': {
|
||||||
|
schedule: '0 */5 * * * *',
|
||||||
|
},
|
||||||
|
'plex-full-scan': {
|
||||||
|
schedule: '0 0 3 * * *',
|
||||||
|
},
|
||||||
|
'radarr-scan': {
|
||||||
|
schedule: '0 0 4 * * *',
|
||||||
|
},
|
||||||
|
'sonarr-scan': {
|
||||||
|
schedule: '0 30 4 * * *',
|
||||||
|
},
|
||||||
|
'download-sync': {
|
||||||
|
schedule: '0 * * * * *',
|
||||||
|
},
|
||||||
|
'download-sync-reset': {
|
||||||
|
schedule: '0 0 1 * * *',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
if (initialSettings) {
|
if (initialSettings) {
|
||||||
this.data = merge(this.data, initialSettings);
|
this.data = merge(this.data, initialSettings);
|
||||||
@@ -428,6 +461,14 @@ class Settings {
|
|||||||
this.data.notifications = data;
|
this.data.notifications = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get jobs(): Record<JobId, JobSettings> {
|
||||||
|
return this.data.jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
set jobs(data: Record<JobId, JobSettings>) {
|
||||||
|
this.data.jobs = data;
|
||||||
|
}
|
||||||
|
|
||||||
get clientId(): string {
|
get clientId(): string {
|
||||||
if (!this.data.clientId) {
|
if (!this.data.clientId) {
|
||||||
this.data.clientId = randomUUID();
|
this.data.clientId = randomUUID();
|
||||||
|
@@ -2,6 +2,7 @@ import { Router } from 'express';
|
|||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { merge, omit } from 'lodash';
|
import { merge, omit } from 'lodash';
|
||||||
|
import { rescheduleJob } from 'node-schedule';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { getRepository } from 'typeorm';
|
import { getRepository } from 'typeorm';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
@@ -49,7 +50,7 @@ settingsRoutes.get('/main', (req, res, next) => {
|
|||||||
const settings = getSettings();
|
const settings = getSettings();
|
||||||
|
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return next({ status: 500, message: 'User missing from request' });
|
return next({ status: 400, message: 'User missing from request' });
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
res.status(200).json(filteredMainSettings(req.user, settings.main));
|
||||||
@@ -310,6 +311,7 @@ settingsRoutes.get('/jobs', (_req, res) => {
|
|||||||
id: job.id,
|
id: job.id,
|
||||||
name: job.name,
|
name: job.name,
|
||||||
type: job.type,
|
type: job.type,
|
||||||
|
interval: job.interval,
|
||||||
nextExecutionTime: job.job.nextInvocation(),
|
nextExecutionTime: job.job.nextInvocation(),
|
||||||
running: job.running ? job.running() : false,
|
running: job.running ? job.running() : false,
|
||||||
}))
|
}))
|
||||||
@@ -329,6 +331,7 @@ settingsRoutes.post<{ jobId: string }>('/jobs/:jobId/run', (req, res, next) => {
|
|||||||
id: scheduledJob.id,
|
id: scheduledJob.id,
|
||||||
name: scheduledJob.name,
|
name: scheduledJob.name,
|
||||||
type: scheduledJob.type,
|
type: scheduledJob.type,
|
||||||
|
interval: scheduledJob.interval,
|
||||||
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||||
});
|
});
|
||||||
@@ -353,12 +356,45 @@ settingsRoutes.post<{ jobId: string }>(
|
|||||||
id: scheduledJob.id,
|
id: scheduledJob.id,
|
||||||
name: scheduledJob.name,
|
name: scheduledJob.name,
|
||||||
type: scheduledJob.type,
|
type: scheduledJob.type,
|
||||||
|
interval: scheduledJob.interval,
|
||||||
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||||
running: scheduledJob.running ? scheduledJob.running() : false,
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
settingsRoutes.post<{ jobId: string }>(
|
||||||
|
'/jobs/:jobId/schedule',
|
||||||
|
(req, res, next) => {
|
||||||
|
const scheduledJob = scheduledJobs.find(
|
||||||
|
(job) => job.id === req.params.jobId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!scheduledJob) {
|
||||||
|
return next({ status: 404, message: 'Job not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = rescheduleJob(scheduledJob.job, req.body.schedule);
|
||||||
|
const settings = getSettings();
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
settings.jobs[scheduledJob.id].schedule = req.body.schedule;
|
||||||
|
settings.save();
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
id: scheduledJob.id,
|
||||||
|
name: scheduledJob.name,
|
||||||
|
type: scheduledJob.type,
|
||||||
|
interval: scheduledJob.interval,
|
||||||
|
nextExecutionTime: scheduledJob.job.nextInvocation(),
|
||||||
|
running: scheduledJob.running ? scheduledJob.running() : false,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return next({ status: 400, message: 'Invalid job schedule' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
settingsRoutes.get('/cache', (req, res) => {
|
settingsRoutes.get('/cache', (req, res) => {
|
||||||
const caches = cacheManager.getAllCaches();
|
const caches = cacheManager.getAllCaches();
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
|
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
|
||||||
|
import { PencilIcon } from '@heroicons/react/solid';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import {
|
import {
|
||||||
defineMessages,
|
defineMessages,
|
||||||
FormattedRelativeTime,
|
FormattedRelativeTime,
|
||||||
@@ -10,14 +11,17 @@ import {
|
|||||||
import { useToasts } from 'react-toast-notifications';
|
import { useToasts } from 'react-toast-notifications';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
|
import { CacheItem } from '../../../../server/interfaces/api/settingsInterfaces';
|
||||||
|
import { JobId } from '../../../../server/lib/settings';
|
||||||
import Spinner from '../../../assets/spinner.svg';
|
import Spinner from '../../../assets/spinner.svg';
|
||||||
import globalMessages from '../../../i18n/globalMessages';
|
import globalMessages from '../../../i18n/globalMessages';
|
||||||
import { formatBytes } from '../../../utils/numberHelpers';
|
import { formatBytes } from '../../../utils/numberHelpers';
|
||||||
import Badge from '../../Common/Badge';
|
import Badge from '../../Common/Badge';
|
||||||
import Button from '../../Common/Button';
|
import Button from '../../Common/Button';
|
||||||
import LoadingSpinner from '../../Common/LoadingSpinner';
|
import LoadingSpinner from '../../Common/LoadingSpinner';
|
||||||
|
import Modal from '../../Common/Modal';
|
||||||
import PageTitle from '../../Common/PageTitle';
|
import PageTitle from '../../Common/PageTitle';
|
||||||
import Table from '../../Common/Table';
|
import Table from '../../Common/Table';
|
||||||
|
import Transition from '../../Transition';
|
||||||
|
|
||||||
const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||||
jobsandcache: 'Jobs & Cache',
|
jobsandcache: 'Jobs & Cache',
|
||||||
@@ -51,12 +55,21 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
|||||||
'sonarr-scan': 'Sonarr Scan',
|
'sonarr-scan': 'Sonarr Scan',
|
||||||
'download-sync': 'Download Sync',
|
'download-sync': 'Download Sync',
|
||||||
'download-sync-reset': 'Download Sync Reset',
|
'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 {
|
interface Job {
|
||||||
id: string;
|
id: JobId;
|
||||||
name: string;
|
name: string;
|
||||||
type: 'process' | 'command';
|
type: 'process' | 'command';
|
||||||
|
interval: 'short' | 'long' | 'fixed';
|
||||||
nextExecutionTime: string;
|
nextExecutionTime: string;
|
||||||
running: boolean;
|
running: boolean;
|
||||||
}
|
}
|
||||||
@@ -74,6 +87,16 @@ const SettingsJobs: React.FC = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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) {
|
if (!data && !error) {
|
||||||
return <LoadingSpinner />;
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
@@ -118,6 +141,42 @@ const SettingsJobs: React.FC = () => {
|
|||||||
cacheRevalidate();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageTitle
|
<PageTitle
|
||||||
@@ -126,6 +185,82 @@ const SettingsJobs: 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"
|
||||||
|
show={jobEditModal.isOpen}
|
||||||
|
>
|
||||||
|
<Modal
|
||||||
|
title={intl.formatMessage(messages.editJobSchedule)}
|
||||||
|
okText={
|
||||||
|
isSaving
|
||||||
|
? intl.formatMessage(globalMessages.saving)
|
||||||
|
: intl.formatMessage(globalMessages.save)
|
||||||
|
}
|
||||||
|
iconSvg={<PencilIcon />}
|
||||||
|
onCancel={() => setJobEditModal({ isOpen: false })}
|
||||||
|
okDisabled={isSaving}
|
||||||
|
onOk={() => scheduleJob()}
|
||||||
|
>
|
||||||
|
<div className="section">
|
||||||
|
<form>
|
||||||
|
<div className="pb-6 form-row">
|
||||||
|
<label htmlFor="jobSchedule" className="text-label">
|
||||||
|
{intl.formatMessage(messages.editJobSchedulePrompt)}
|
||||||
|
</label>
|
||||||
|
<div className="form-input">
|
||||||
|
{jobEditModal.job?.interval === 'short' ? (
|
||||||
|
<select
|
||||||
|
name="jobScheduleMinutes"
|
||||||
|
className="inline"
|
||||||
|
value={jobScheduleMinutes}
|
||||||
|
onChange={(e) =>
|
||||||
|
setJobScheduleMinutes(Number(e.target.value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 60].map((v) => (
|
||||||
|
<option value={v} key={`jobScheduleMinutes-${v}`}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
messages.editJobScheduleSelectorMinutes,
|
||||||
|
{
|
||||||
|
jobScheduleMinutes: v,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
) : (
|
||||||
|
<select
|
||||||
|
name="jobScheduleHours"
|
||||||
|
className="inline"
|
||||||
|
value={jobScheduleHours}
|
||||||
|
onChange={(e) =>
|
||||||
|
setJobScheduleHours(Number(e.target.value))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{[1, 2, 3, 4, 6, 8, 12, 24, 48, 72].map((v) => (
|
||||||
|
<option value={v} key={`jobScheduleHours-${v}`}>
|
||||||
|
{intl.formatMessage(
|
||||||
|
messages.editJobScheduleSelectorHours,
|
||||||
|
{
|
||||||
|
jobScheduleHours: v,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="heading">{intl.formatMessage(messages.jobs)}</h3>
|
<h3 className="heading">{intl.formatMessage(messages.jobs)}</h3>
|
||||||
<p className="description">
|
<p className="description">
|
||||||
@@ -179,6 +314,18 @@ const SettingsJobs: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</Table.TD>
|
</Table.TD>
|
||||||
<Table.TD alignText="right">
|
<Table.TD alignText="right">
|
||||||
|
{job.interval !== 'fixed' && (
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
buttonType="warning"
|
||||||
|
onClick={() =>
|
||||||
|
setJobEditModal({ isOpen: true, job: job })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PencilIcon />
|
||||||
|
{intl.formatMessage(globalMessages.edit)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{job.running ? (
|
{job.running ? (
|
||||||
<Button buttonType="danger" onClick={() => cancelJob(job)}>
|
<Button buttonType="danger" onClick={() => cancelJob(job)}>
|
||||||
<StopIcon />
|
<StopIcon />
|
||||||
|
@@ -462,7 +462,13 @@
|
|||||||
"components.Settings.SettingsJobsCache.command": "Command",
|
"components.Settings.SettingsJobsCache.command": "Command",
|
||||||
"components.Settings.SettingsJobsCache.download-sync": "Download Sync",
|
"components.Settings.SettingsJobsCache.download-sync": "Download Sync",
|
||||||
"components.Settings.SettingsJobsCache.download-sync-reset": "Download Sync Reset",
|
"components.Settings.SettingsJobsCache.download-sync-reset": "Download Sync Reset",
|
||||||
|
"components.Settings.SettingsJobsCache.editJobSchedule": "Modify Job",
|
||||||
|
"components.Settings.SettingsJobsCache.editJobSchedulePrompt": "Frequency",
|
||||||
|
"components.Settings.SettingsJobsCache.editJobScheduleSelectorHours": "Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}",
|
||||||
|
"components.Settings.SettingsJobsCache.editJobScheduleSelectorMinutes": "Every {jobScheduleMinutes, plural, one {minute} other {{jobScheduleMinutes} minutes}}",
|
||||||
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
|
"components.Settings.SettingsJobsCache.flushcache": "Flush Cache",
|
||||||
|
"components.Settings.SettingsJobsCache.jobScheduleEditFailed": "Something went wrong while saving the job.",
|
||||||
|
"components.Settings.SettingsJobsCache.jobScheduleEditSaved": "Job edited successfully!",
|
||||||
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
|
"components.Settings.SettingsJobsCache.jobcancelled": "{jobname} canceled.",
|
||||||
"components.Settings.SettingsJobsCache.jobname": "Job Name",
|
"components.Settings.SettingsJobsCache.jobname": "Job Name",
|
||||||
"components.Settings.SettingsJobsCache.jobs": "Jobs",
|
"components.Settings.SettingsJobsCache.jobs": "Jobs",
|
||||||
|
Reference in New Issue
Block a user