@ -5,6 +5,7 @@ import LoadingSpinner from '@app/components/Common/LoadingSpinner';
import Modal from '@app/components/Common/Modal' ;
import Modal from '@app/components/Common/Modal' ;
import PageTitle from '@app/components/Common/PageTitle' ;
import PageTitle from '@app/components/Common/PageTitle' ;
import Table from '@app/components/Common/Table' ;
import Table from '@app/components/Common/Table' ;
import useLocale from '@app/hooks/useLocale' ;
import globalMessages from '@app/i18n/globalMessages' ;
import globalMessages from '@app/i18n/globalMessages' ;
import { formatBytes } from '@app/utils/numberHelpers' ;
import { formatBytes } from '@app/utils/numberHelpers' ;
import { Transition } from '@headlessui/react' ;
import { Transition } from '@headlessui/react' ;
@ -13,7 +14,8 @@ import { PencilIcon } from '@heroicons/react/solid';
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces' ;
import type { CacheItem } from '@server/interfaces/api/settingsInterfaces' ;
import type { JobId } from '@server/lib/settings' ;
import type { JobId } from '@server/lib/settings' ;
import axios from 'axios' ;
import axios from 'axios' ;
import { Fragment , useState } from 'react' ;
import cronstrue from 'cronstrue/i18n' ;
import { Fragment , useReducer , useState } from 'react' ;
import type { MessageDescriptor } from 'react-intl' ;
import type { MessageDescriptor } from 'react-intl' ;
import { defineMessages , FormattedRelativeTime , useIntl } from 'react-intl' ;
import { defineMessages , FormattedRelativeTime , useIntl } from 'react-intl' ;
import { useToasts } from 'react-toast-notifications' ;
import { useToasts } from 'react-toast-notifications' ;
@ -55,7 +57,8 @@ const messages: { [messageName: string]: MessageDescriptor } = defineMessages({
editJobSchedule : 'Modify Job' ,
editJobSchedule : 'Modify Job' ,
jobScheduleEditSaved : 'Job edited successfully!' ,
jobScheduleEditSaved : 'Job edited successfully!' ,
jobScheduleEditFailed : 'Something went wrong while saving the job.' ,
jobScheduleEditFailed : 'Something went wrong while saving the job.' ,
editJobSchedulePrompt : 'Frequency' ,
editJobScheduleCurrent : 'Current Frequency' ,
editJobSchedulePrompt : 'New Frequency' ,
editJobScheduleSelectorHours :
editJobScheduleSelectorHours :
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}' ,
'Every {jobScheduleHours, plural, one {hour} other {{jobScheduleHours} hours}}' ,
editJobScheduleSelectorMinutes :
editJobScheduleSelectorMinutes :
@ -67,12 +70,56 @@ interface Job {
name : string ;
name : string ;
type : 'process' | 'command' ;
type : 'process' | 'command' ;
interval : 'short' | 'long' | 'fixed' ;
interval : 'short' | 'long' | 'fixed' ;
cronSchedule : string ;
nextExecutionTime : string ;
nextExecutionTime : string ;
running : boolean ;
running : boolean ;
}
}
type JobModalState = {
isOpen? : boolean ;
job? : Job ;
scheduleHours : number ;
scheduleMinutes : number ;
} ;
type JobModalAction =
| { type : 'set' ; hours? : number ; minutes? : number }
| {
type : 'close' ;
}
| { type : 'open' ; job? : Job } ;
const jobModalReducer = (
state : JobModalState ,
action : JobModalAction
) : JobModalState = > {
switch ( action . type ) {
case 'close' :
return {
. . . state ,
isOpen : false ,
} ;
case 'open' :
return {
isOpen : true ,
job : action.job ,
scheduleHours : 1 ,
scheduleMinutes : 5 ,
} ;
case 'set' :
return {
. . . state ,
scheduleHours : action.hours ? ? state . scheduleHours ,
scheduleMinutes : action.minutes ? ? state . scheduleMinutes ,
} ;
}
} ;
const SettingsJobs = ( ) = > {
const SettingsJobs = ( ) = > {
const intl = useIntl ( ) ;
const intl = useIntl ( ) ;
const { locale } = useLocale ( ) ;
const { addToast } = useToasts ( ) ;
const { addToast } = useToasts ( ) ;
const {
const {
data ,
data ,
@ -88,15 +135,12 @@ const SettingsJobs = () => {
}
}
) ;
) ;
const [ jobEditModal , setJobEditModal ] = useState < {
const [ jobModalState , dispatch ] = useReducer ( jobModalReducer , {
isOpen : boolean ;
job? : Job ;
} > ( {
isOpen : false ,
isOpen : false ,
scheduleHours : 1 ,
scheduleMinutes : 5 ,
} ) ;
} ) ;
const [ isSaving , setIsSaving ] = useState ( 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 / > ;
@ -146,10 +190,10 @@ const SettingsJobs = () => {
const jobScheduleCron = [ '0' , '0' , '*' , '*' , '*' , '*' ] ;
const jobScheduleCron = [ '0' , '0' , '*' , '*' , '*' , '*' ] ;
try {
try {
if ( job Edit Modal. job ? . interval === 'short' ) {
if ( job ModalState . job ? . interval === 'short' ) {
jobScheduleCron [ 1 ] = ` */ ${ job ScheduleMinutes} ` ;
jobScheduleCron [ 1 ] = ` */ ${ job Modal State. s cheduleMinutes} ` ;
} else if ( job Edit Modal. job ? . interval === 'long' ) {
} else if ( job ModalState . job ? . interval === 'long' ) {
jobScheduleCron [ 2 ] = ` */ ${ job ScheduleHours} ` ;
jobScheduleCron [ 2 ] = ` */ ${ job Modal State. s cheduleHours} ` ;
} else {
} else {
// jobs with interval: fixed should not be editable
// jobs with interval: fixed should not be editable
throw new Error ( ) ;
throw new Error ( ) ;
@ -157,16 +201,18 @@ const SettingsJobs = () => {
setIsSaving ( true ) ;
setIsSaving ( true ) ;
await axios . post (
await axios . post (
` /api/v1/settings/jobs/ ${ job Edit Modal. job ? . id } /schedule ` ,
` /api/v1/settings/jobs/ ${ job ModalState . job . id } /schedule ` ,
{
{
schedule : jobScheduleCron.join ( ' ' ) ,
schedule : jobScheduleCron.join ( ' ' ) ,
}
}
) ;
) ;
addToast ( intl . formatMessage ( messages . jobScheduleEditSaved ) , {
addToast ( intl . formatMessage ( messages . jobScheduleEditSaved ) , {
appearance : 'success' ,
appearance : 'success' ,
autoDismiss : true ,
autoDismiss : true ,
} ) ;
} ) ;
setJobEditModal ( { isOpen : false } ) ;
dispatch ( { type : 'close' } ) ;
revalidate ( ) ;
revalidate ( ) ;
} catch ( e ) {
} catch ( e ) {
addToast ( intl . formatMessage ( messages . jobScheduleEditFailed ) , {
addToast ( intl . formatMessage ( messages . jobScheduleEditFailed ) , {
@ -194,7 +240,7 @@ const SettingsJobs = () => {
leave = "opacity-100 transition duration-300"
leave = "opacity-100 transition duration-300"
leaveFrom = "opacity-100"
leaveFrom = "opacity-100"
leaveTo = "opacity-0"
leaveTo = "opacity-0"
show = { job Edit Modal. isOpen }
show = { job ModalState . isOpen }
>
>
< Modal
< Modal
title = { intl . formatMessage ( messages . editJobSchedule ) }
title = { intl . formatMessage ( messages . editJobSchedule ) }
@ -203,24 +249,43 @@ const SettingsJobs = () => {
? intl . formatMessage ( globalMessages . saving )
? intl . formatMessage ( globalMessages . saving )
: intl . formatMessage ( globalMessages . save )
: intl . formatMessage ( globalMessages . save )
}
}
onCancel = { ( ) = > setJobEditModal( { isOpen : false } ) }
onCancel = { ( ) = > dispatch( { type : 'close' } ) }
okDisabled = { isSaving }
okDisabled = { isSaving }
onOk = { ( ) = > scheduleJob ( ) }
onOk = { ( ) = > scheduleJob ( ) }
>
>
< div className = "section" >
< div className = "section" >
< form >
< form className = "mb-6" >
< div className = "form-row pb-6" >
< div className = "form-row" >
< label className = "text-label" >
{ intl . formatMessage ( messages . editJobScheduleCurrent ) }
< / label >
< div className = "form-input-area mt-2 mb-1" >
< div >
{ jobModalState . job &&
cronstrue . toString ( jobModalState . job . cronSchedule , {
locale ,
} ) }
< / div >
< div className = "text-sm text-gray-500" >
{ jobModalState . job ? . cronSchedule }
< / div >
< / div >
< / div >
< div className = "form-row" >
< label htmlFor = "jobSchedule" className = "text-label" >
< label htmlFor = "jobSchedule" className = "text-label" >
{ intl . formatMessage ( messages . editJobSchedulePrompt ) }
{ intl . formatMessage ( messages . editJobSchedulePrompt ) }
< / label >
< / label >
< div className = "form-input-area" >
< div className = "form-input-area" >
{ jobEditModal . job ? . interval === 'short' ? (
{ job ModalState . job ? . interval === 'short' ? (
< select
< select
name = "jobScheduleMinutes"
name = "jobScheduleMinutes"
className = "inline"
className = "inline"
value = { jobScheduleMinutes }
value = { job Modal State. s cheduleMinutes}
onChange = { ( e ) = >
onChange = { ( e ) = >
setJobScheduleMinutes ( Number ( e . target . value ) )
dispatch ( {
type : 'set' ,
minutes : Number ( e . target . value ) ,
} )
}
}
>
>
{ [ 5 , 10 , 15 , 20 , 30 , 60 ] . map ( ( v ) = > (
{ [ 5 , 10 , 15 , 20 , 30 , 60 ] . map ( ( v ) = > (
@ -238,9 +303,12 @@ const SettingsJobs = () => {
< select
< select
name = "jobScheduleHours"
name = "jobScheduleHours"
className = "inline"
className = "inline"
value = { job ScheduleHours}
value = { job Modal State. s cheduleHours}
onChange = { ( e ) = >
onChange = { ( e ) = >
setJobScheduleHours ( Number ( e . target . value ) )
dispatch ( {
type : 'set' ,
hours : Number ( e . target . value ) ,
} )
}
}
>
>
{ [ 1 , 2 , 3 , 4 , 6 , 8 , 12 , 24 , 48 , 72 ] . map ( ( v ) = > (
{ [ 1 , 2 , 3 , 4 , 6 , 8 , 12 , 24 , 48 , 72 ] . map ( ( v ) = > (
@ -319,9 +387,7 @@ const SettingsJobs = () => {
< Button
< Button
className = "mr-2"
className = "mr-2"
buttonType = "warning"
buttonType = "warning"
onClick = { ( ) = >
onClick = { ( ) = > dispatch ( { type : 'open' , job } ) }
setJobEditModal ( { isOpen : true , job : job } )
}
>
>
< PencilIcon / >
< PencilIcon / >
< span > { intl . formatMessage ( globalMessages . edit ) } < / span >
< span > { intl . formatMessage ( globalMessages . edit ) } < / span >