mirror of
https://github.com/fallenbagel/jellyseerr.git
synced 2026-01-01 12:18:35 -05: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
@@ -1,6 +1,7 @@
|
||||
import { PlayIcon, StopIcon, TrashIcon } from '@heroicons/react/outline';
|
||||
import { PencilIcon } from '@heroicons/react/solid';
|
||||
import axios from 'axios';
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
defineMessages,
|
||||
FormattedRelativeTime,
|
||||
@@ -10,14 +11,17 @@ import {
|
||||
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',
|
||||
@@ -51,12 +55,21 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
|
||||
'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: string;
|
||||
id: JobId;
|
||||
name: string;
|
||||
type: 'process' | 'command';
|
||||
interval: 'short' | 'long' | 'fixed';
|
||||
nextExecutionTime: string;
|
||||
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) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
@@ -118,6 +141,42 @@ const SettingsJobs: React.FC = () => {
|
||||
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 (
|
||||
<>
|
||||
<PageTitle
|
||||
@@ -126,6 +185,82 @@ const SettingsJobs: React.FC = () => {
|
||||
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">
|
||||
<h3 className="heading">{intl.formatMessage(messages.jobs)}</h3>
|
||||
<p className="description">
|
||||
@@ -179,6 +314,18 @@ const SettingsJobs: React.FC = () => {
|
||||
</div>
|
||||
</Table.TD>
|
||||
<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 ? (
|
||||
<Button buttonType="danger" onClick={() => cancelJob(job)}>
|
||||
<StopIcon />
|
||||
|
||||
Reference in New Issue
Block a user