Add underlying data changes necessary for new task & schedule features

This commit is contained in:
Dane Everitt 2021-05-01 10:44:40 -07:00
parent cf1ac04e39
commit 92cd659db3
No known key found for this signature in database
GPG Key ID: EEA66103B3D71F53
13 changed files with 201 additions and 107 deletions

View File

@ -16,7 +16,6 @@ use Pterodactyl\Services\Schedules\ProcessScheduleService;
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer; use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest;
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest;
@ -81,6 +80,7 @@ class ScheduleController extends ClientApiController
'cron_hour' => $request->input('hour'), 'cron_hour' => $request->input('hour'),
'cron_minute' => $request->input('minute'), 'cron_minute' => $request->input('minute'),
'is_active' => (bool) $request->input('is_active'), 'is_active' => (bool) $request->input('is_active'),
'only_when_online' => (bool) $request->input('only_when_online'),
'next_run_at' => $this->getNextRunAt($request), 'next_run_at' => $this->getNextRunAt($request),
]); ]);
@ -128,6 +128,7 @@ class ScheduleController extends ClientApiController
'cron_hour' => $request->input('hour'), 'cron_hour' => $request->input('hour'),
'cron_minute' => $request->input('minute'), 'cron_minute' => $request->input('minute'),
'is_active' => $active, 'is_active' => $active,
'only_when_online' => (bool) $request->input('only_when_online'),
'next_run_at' => $this->getNextRunAt($request), 'next_run_at' => $this->getNextRunAt($request),
]; ];

View File

@ -59,6 +59,7 @@ class ScheduleTaskController extends ClientApiController
'action' => $request->input('action'), 'action' => $request->input('action'),
'payload' => $request->input('payload') ?? '', 'payload' => $request->input('payload') ?? '',
'time_offset' => $request->input('time_offset'), 'time_offset' => $request->input('time_offset'),
'continue_on_failure' => (bool) $request->input('continue_on_failure'),
]); ]);
return $this->fractal->item($task) return $this->fractal->item($task)
@ -84,6 +85,7 @@ class ScheduleTaskController extends ClientApiController
'action' => $request->input('action'), 'action' => $request->input('action'),
'payload' => $request->input('payload') ?? '', 'payload' => $request->input('payload') ?? '',
'time_offset' => $request->input('time_offset'), 'time_offset' => $request->input('time_offset'),
'continue_on_failure' => (bool) $request->input('continue_on_failure'),
]); ]);
return $this->fractal->item($task->refresh()) return $this->fractal->item($task->refresh())

View File

@ -7,6 +7,7 @@ use Pterodactyl\Jobs\Job;
use Carbon\CarbonImmutable; use Carbon\CarbonImmutable;
use Pterodactyl\Models\Task; use Pterodactyl\Models\Task;
use InvalidArgumentException; use InvalidArgumentException;
use Illuminate\Http\Response;
use Pterodactyl\Models\Schedule; use Pterodactyl\Models\Schedule;
use Illuminate\Queue\SerializesModels; use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\InteractsWithQueue;
@ -15,6 +16,7 @@ use Illuminate\Foundation\Bus\DispatchesJobs;
use Pterodactyl\Services\Backups\InitiateBackupService; use Pterodactyl\Services\Backups\InitiateBackupService;
use Pterodactyl\Repositories\Wings\DaemonPowerRepository; use Pterodactyl\Repositories\Wings\DaemonPowerRepository;
use Pterodactyl\Repositories\Wings\DaemonCommandRepository; use Pterodactyl\Repositories\Wings\DaemonCommandRepository;
use Pterodactyl\Exceptions\Http\Connection\DaemonConnectionException;
class RunTaskJob extends Job implements ShouldQueue class RunTaskJob extends Job implements ShouldQueue
{ {
@ -62,19 +64,33 @@ class RunTaskJob extends Job implements ShouldQueue
$server = $this->task->server; $server = $this->task->server;
// Perform the provided task against the daemon. // Perform the provided task against the daemon.
try {
switch ($this->task->action) { switch ($this->task->action) {
case 'power': case Task::ACTION_POWER:
$powerRepository->setServer($server)->send($this->task->payload); $powerRepository->setServer($server)->send($this->task->payload);
break; break;
case 'command': case Task::ACTION_COMMAND:
$commandRepository->setServer($server)->send($this->task->payload); $commandRepository->setServer($server)->send($this->task->payload);
break; break;
case 'backup': case Task::ACTION_BACKUP:
$backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true); $backupService->setIgnoredFiles(explode(PHP_EOL, $this->task->payload))->handle($server, null, true);
break; break;
default: default:
throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.'); throw new InvalidArgumentException('Cannot run a task that points to a non-existent action.');
} }
} catch (Exception $exception) {
if ($exception instanceof DaemonConnectionException) {
// If the task "failed" because the server is offline and it was sending a command or
// executing a power action (which shouldn't happen?) then just stop trying to process
// the schedule, but don't actually log the failure.
if ($this->task->action === Task::ACTION_POWER || $this->task->action === Task::ACTION_COMMAND) {
// Do the thing
if ($exception->getStatusCode() === Response::HTTP_CONFLICT) {
}
}
}
throw $exception;
}
$this->markTaskNotQueued(); $this->markTaskNotQueued();
$this->queueNextTask(); $this->queueNextTask();

View File

@ -18,6 +18,7 @@ use Pterodactyl\Contracts\Extensions\HashidsInterface;
* @property string $cron_minute * @property string $cron_minute
* @property bool $is_active * @property bool $is_active
* @property bool $is_processing * @property bool $is_processing
* @property bool $only_when_online
* @property \Carbon\Carbon|null $last_run_at * @property \Carbon\Carbon|null $last_run_at
* @property \Carbon\Carbon|null $next_run_at * @property \Carbon\Carbon|null $next_run_at
* @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $created_at
@ -63,6 +64,7 @@ class Schedule extends Model
'cron_minute', 'cron_minute',
'is_active', 'is_active',
'is_processing', 'is_processing',
'only_when_online',
'last_run_at', 'last_run_at',
'next_run_at', 'next_run_at',
]; ];
@ -75,6 +77,7 @@ class Schedule extends Model
'server_id' => 'integer', 'server_id' => 'integer',
'is_active' => 'boolean', 'is_active' => 'boolean',
'is_processing' => 'boolean', 'is_processing' => 'boolean',
'only_when_online' => 'boolean',
]; ];
/** /**
@ -99,6 +102,7 @@ class Schedule extends Model
'cron_minute' => '*', 'cron_minute' => '*',
'is_active' => true, 'is_active' => true,
'is_processing' => false, 'is_processing' => false,
'only_when_online' => false,
]; ];
/** /**
@ -114,6 +118,7 @@ class Schedule extends Model
'cron_minute' => 'required|string', 'cron_minute' => 'required|string',
'is_active' => 'boolean', 'is_active' => 'boolean',
'is_processing' => 'boolean', 'is_processing' => 'boolean',
'only_when_online' => 'boolean',
'last_run_at' => 'nullable|date', 'last_run_at' => 'nullable|date',
'next_run_at' => 'nullable|date', 'next_run_at' => 'nullable|date',
]; ];
@ -122,6 +127,7 @@ class Schedule extends Model
* Returns the schedule's execution crontab entry as a string. * Returns the schedule's execution crontab entry as a string.
* *
* @return \Carbon\CarbonImmutable * @return \Carbon\CarbonImmutable
*
* @throws \Exception * @throws \Exception
*/ */
public function getNextRunDate() public function getNextRunDate()

View File

@ -14,6 +14,7 @@ use Pterodactyl\Contracts\Extensions\HashidsInterface;
* @property string $payload * @property string $payload
* @property int $time_offset * @property int $time_offset
* @property bool $is_queued * @property bool $is_queued
* @property bool $continue_on_failure
* @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $created_at
* @property \Carbon\Carbon $updated_at * @property \Carbon\Carbon $updated_at
* @property string $hashid * @property string $hashid
@ -30,6 +31,13 @@ class Task extends Model
*/ */
public const RESOURCE_NAME = 'schedule_task'; public const RESOURCE_NAME = 'schedule_task';
/**
* The default actions that can exist for a task in Pterodactyl.
*/
public const ACTION_POWER = 'power';
public const ACTION_COMMAND = 'command';
public const ACTION_BACKUP = 'backup';
/** /**
* The table associated with the model. * The table associated with the model.
* *
@ -56,6 +64,7 @@ class Task extends Model
'payload', 'payload',
'time_offset', 'time_offset',
'is_queued', 'is_queued',
'continue_on_failure',
]; ];
/** /**
@ -69,6 +78,7 @@ class Task extends Model
'sequence_id' => 'integer', 'sequence_id' => 'integer',
'time_offset' => 'integer', 'time_offset' => 'integer',
'is_queued' => 'boolean', 'is_queued' => 'boolean',
'continue_on_failure' => 'boolean',
]; ];
/** /**
@ -79,6 +89,7 @@ class Task extends Model
protected $attributes = [ protected $attributes = [
'time_offset' => 0, 'time_offset' => 0,
'is_queued' => false, 'is_queued' => false,
'continue_on_failure' => false,
]; ];
/** /**
@ -91,6 +102,7 @@ class Task extends Model
'payload' => 'required_unless:action,backup|string', 'payload' => 'required_unless:action,backup|string',
'time_offset' => 'required|numeric|between:0,900', 'time_offset' => 'required|numeric|between:0,900',
'is_queued' => 'boolean', 'is_queued' => 'boolean',
'continue_on_failure' => 'boolean',
]; ];
/** /**

View File

@ -45,6 +45,7 @@ class ScheduleTransformer extends BaseClientTransformer
], ],
'is_active' => $model->is_active, 'is_active' => $model->is_active,
'is_processing' => $model->is_processing, 'is_processing' => $model->is_processing,
'only_when_online' => $model->only_when_online,
'last_run_at' => $model->last_run_at ? $model->last_run_at->toIso8601String() : null, 'last_run_at' => $model->last_run_at ? $model->last_run_at->toIso8601String() : null,
'next_run_at' => $model->next_run_at ? $model->next_run_at->toIso8601String() : null, 'next_run_at' => $model->next_run_at ? $model->next_run_at->toIso8601String() : null,
'created_at' => $model->created_at->toIso8601String(), 'created_at' => $model->created_at->toIso8601String(),

View File

@ -28,6 +28,7 @@ class TaskTransformer extends BaseClientTransformer
'payload' => $model->payload, 'payload' => $model->payload,
'time_offset' => $model->time_offset, 'time_offset' => $model->time_offset,
'is_queued' => $model->is_queued, 'is_queued' => $model->is_queued,
'continue_on_failure' => $model->continue_on_failure,
'created_at' => $model->created_at->toIso8601String(), 'created_at' => $model->created_at->toIso8601String(),
'updated_at' => $model->updated_at->toIso8601String(), 'updated_at' => $model->updated_at->toIso8601String(),
]; ];

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddContinueOnFailureOptionToTasks extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tasks', function (Blueprint $table) {
$table->unsignedTinyInteger('continue_on_failure')->after('is_queued')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tasks', function (Blueprint $table) {
$table->dropColumn('continue_on_failure');
});
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddOnlyRunWhenServerOnlineOptionToSchedules extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('schedules', function (Blueprint $table) {
$table->unsignedTinyInteger('only_when_online')->after('is_processing')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('schedules', function (Blueprint $table) {
$table->dropColumn('only_when_online');
});
}
}

View File

@ -1,20 +1,19 @@
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules'; import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
import http from '@/api/http'; import http from '@/api/http';
type Data = Pick<Schedule, 'cron' | 'name' | 'isActive'> & { id?: number } type Data = Pick<Schedule, 'cron' | 'name' | 'onlyWhenOnline' | 'isActive'> & { id?: number }
export default (uuid: string, schedule: Data): Promise<Schedule> => { export default async (uuid: string, schedule: Data): Promise<Schedule> => {
return new Promise((resolve, reject) => { const { data } = await http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, {
http.post(`/api/client/servers/${uuid}/schedules${schedule.id ? `/${schedule.id}` : ''}`, {
is_active: schedule.isActive, is_active: schedule.isActive,
only_when_online: schedule.onlyWhenOnline,
name: schedule.name, name: schedule.name,
minute: schedule.cron.minute, minute: schedule.cron.minute,
hour: schedule.cron.hour, hour: schedule.cron.hour,
day_of_month: schedule.cron.dayOfMonth, day_of_month: schedule.cron.dayOfMonth,
month: schedule.cron.month, month: schedule.cron.month,
day_of_week: schedule.cron.dayOfWeek, day_of_week: schedule.cron.dayOfWeek,
})
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
.catch(reject);
}); });
return rawDataToServerSchedule(data.attributes);
}; };

View File

@ -12,6 +12,7 @@ export interface Schedule {
}; };
isActive: boolean; isActive: boolean;
isProcessing: boolean; isProcessing: boolean;
onlyWhenOnline: boolean;
lastRunAt: Date | null; lastRunAt: Date | null;
nextRunAt: Date | null; nextRunAt: Date | null;
createdAt: Date; createdAt: Date;
@ -27,6 +28,7 @@ export interface Task {
payload: string; payload: string;
timeOffset: number; timeOffset: number;
isQueued: boolean; isQueued: boolean;
continueOnFailure: boolean;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
} }
@ -38,6 +40,7 @@ export const rawDataToServerTask = (data: any): Task => ({
payload: data.payload, payload: data.payload,
timeOffset: data.time_offset, timeOffset: data.time_offset,
isQueued: data.is_queued, isQueued: data.is_queued,
continueOnFailure: data.continue_on_failure,
createdAt: new Date(data.created_at), createdAt: new Date(data.created_at),
updatedAt: new Date(data.updated_at), updatedAt: new Date(data.updated_at),
}); });
@ -54,6 +57,7 @@ export const rawDataToServerSchedule = (data: any): Schedule => ({
}, },
isActive: data.is_active, isActive: data.is_active,
isProcessing: data.is_processing, isProcessing: data.is_processing,
onlyWhenOnline: data.only_when_online,
lastRunAt: data.last_run_at ? new Date(data.last_run_at) : null, lastRunAt: data.last_run_at ? new Date(data.last_run_at) : null,
nextRunAt: data.next_run_at ? new Date(data.next_run_at) : null, nextRunAt: data.next_run_at ? new Date(data.next_run_at) : null,
createdAt: new Date(data.created_at), createdAt: new Date(data.created_at),
@ -62,14 +66,12 @@ export const rawDataToServerSchedule = (data: any): Schedule => ({
tasks: (data.relationships?.tasks?.data || []).map((row: any) => rawDataToServerTask(row.attributes)), tasks: (data.relationships?.tasks?.data || []).map((row: any) => rawDataToServerTask(row.attributes)),
}); });
export default (uuid: string): Promise<Schedule[]> => { export default async (uuid: string): Promise<Schedule[]> => {
return new Promise((resolve, reject) => { const { data } = await http.get(`/api/client/servers/${uuid}/schedules`, {
http.get(`/api/client/servers/${uuid}/schedules`, {
params: { params: {
include: [ 'tasks' ], include: [ 'tasks' ],
}, },
})
.then(({ data }) => resolve((data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes))))
.catch(reject);
}); });
return (data.data || []).map((row: any) => rawDataToServerSchedule(row.attributes));
}; };

View File

@ -17,7 +17,7 @@ const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, la
<FormikField innerRef={ref} name={name} validate={validate}> <FormikField innerRef={ref} name={name} validate={validate}>
{ {
({ field, form: { errors, touched } }: FieldProps) => ( ({ field, form: { errors, touched } }: FieldProps) => (
<> <div>
{label && {label &&
<Label htmlFor={id} isLight={light}>{label}</Label> <Label htmlFor={id} isLight={light}>{label}</Label>
} }
@ -35,7 +35,7 @@ const Field = forwardRef<HTMLInputElement, Props>(({ id, name, light = false, la
: :
description ? <p className={'input-help'}>{description}</p> : null description ? <p className={'input-help'}>{description}</p> : null
} }
</> </div>
) )
} }
</FormikField> </FormikField>

View File

@ -1,8 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect } from 'react';
import { Schedule } from '@/api/server/schedules/getServerSchedules'; import { Schedule } from '@/api/server/schedules/getServerSchedules';
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import Field from '@/components/elements/Field'; import Field from '@/components/elements/Field';
import { Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Form, Formik, FormikHelpers } from 'formik';
import FormikSwitch from '@/components/elements/FormikSwitch'; import FormikSwitch from '@/components/elements/FormikSwitch';
import createOrUpdateSchedule from '@/api/server/schedules/createOrUpdateSchedule'; import createOrUpdateSchedule from '@/api/server/schedules/createOrUpdateSchedule';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
@ -11,10 +10,12 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import ModalContext from '@/context/ModalContext';
import asModal from '@/hoc/asModal';
type Props = { interface Props {
schedule?: Schedule; schedule?: Schedule;
} & RequiredModalProps; }
interface Values { interface Values {
name: string; name: string;
@ -24,70 +25,21 @@ interface Values {
hour: string; hour: string;
minute: string; minute: string;
enabled: boolean; enabled: boolean;
onlyWhenOnline: boolean;
} }
const EditScheduleModal = ({ schedule, ...props }: Omit<Props, 'onScheduleUpdated'>) => { const EditScheduleModal = ({ schedule }: Props) => {
const { isSubmitting } = useFormikContext();
return (
<Modal {...props} showSpinnerOverlay={isSubmitting}>
<h3 css={tw`text-2xl mb-6`}>{schedule ? 'Edit schedule' : 'Create new schedule'}</h3>
<FlashMessageRender byKey={'schedule:edit'} css={tw`mb-6`}/>
<Form>
<Field
name={'name'}
label={'Schedule name'}
description={'A human readable identifer for this schedule.'}
/>
<div css={tw`grid grid-cols-2 sm:grid-cols-5 gap-4 mt-6`}>
<div>
<Field name={'minute'} label={'Minute'}/>
</div>
<div>
<Field name={'hour'} label={'Hour'}/>
</div>
<div>
<Field name={'dayOfMonth'} label={'Day of month'}/>
</div>
<div>
<Field name={'month'} label={'Month'}/>
</div>
<div>
<Field name={'dayOfWeek'} label={'Day of week'}/>
</div>
</div>
<p css={tw`text-neutral-400 text-xs mt-2`}>
The schedule system supports the use of Cronjob syntax when defining when tasks should begin
running. Use the fields above to specify when these tasks should begin running.
</p>
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
<FormikSwitch
name={'enabled'}
description={'If disabled, this schedule and it\'s associated tasks will not run.'}
label={'Enabled'}
/>
</div>
<div css={tw`mt-6 text-right`}>
<Button css={tw`w-full sm:w-auto`} type={'submit'} disabled={isSubmitting}>
{schedule ? 'Save changes' : 'Create schedule'}
</Button>
</div>
</Form>
</Modal>
);
};
export default ({ schedule, visible, ...props }: Props) => {
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const { addError, clearFlashes } = useFlash(); const { addError, clearFlashes } = useFlash();
const [ modalVisible, setModalVisible ] = useState(visible); const { dismiss } = useContext(ModalContext);
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule); const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
useEffect(() => { useEffect(() => {
setModalVisible(visible); return () => {
clearFlashes('schedule:edit'); clearFlashes('schedule:edit');
}, [ visible ]); };
}, []);
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => { const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('schedule:edit'); clearFlashes('schedule:edit');
@ -101,12 +53,13 @@ export default ({ schedule, visible, ...props }: Props) => {
month: values.month, month: values.month,
dayOfMonth: values.dayOfMonth, dayOfMonth: values.dayOfMonth,
}, },
onlyWhenOnline: values.onlyWhenOnline,
isActive: values.enabled, isActive: values.enabled,
}) })
.then(schedule => { .then(schedule => {
setSubmitting(false); setSubmitting(false);
appendSchedule(schedule); appendSchedule(schedule);
setModalVisible(false); dismiss();
}) })
.catch(error => { .catch(error => {
console.error(error); console.error(error);
@ -128,13 +81,50 @@ export default ({ schedule, visible, ...props }: Props) => {
dayOfWeek: schedule?.cron.dayOfWeek || '*', dayOfWeek: schedule?.cron.dayOfWeek || '*',
enabled: schedule ? schedule.isActive : true, enabled: schedule ? schedule.isActive : true,
} as Values} } as Values}
validationSchema={null}
> >
<EditScheduleModal {({ isSubmitting }) => (
visible={modalVisible} <Form>
schedule={schedule} <h3 css={tw`text-2xl mb-6`}>{schedule ? 'Edit schedule' : 'Create new schedule'}</h3>
{...props} <FlashMessageRender byKey={'schedule:edit'} css={tw`mb-6`}/>
<Field
name={'name'}
label={'Schedule name'}
description={'A human readable identifer for this schedule.'}
/> />
<div css={tw`grid grid-cols-2 sm:grid-cols-5 gap-4 mt-6`}>
<Field name={'minute'} label={'Minute'}/>
<Field name={'hour'} label={'Hour'}/>
<Field name={'dayOfMonth'} label={'Day of month'}/>
<Field name={'month'} label={'Month'}/>
<Field name={'dayOfWeek'} label={'Day of week'}/>
</div>
<p css={tw`text-neutral-400 text-xs mt-2`}>
The schedule system supports the use of Cronjob syntax when defining when tasks should begin
running. Use the fields above to specify when these tasks should begin running.
</p>
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
<FormikSwitch
name={'only_when_online'}
description={'If disabled this schedule will always run, regardless of the server\'s current power state.'}
label={'Only When Server Is Online'}
/>
</div>
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
<FormikSwitch
name={'enabled'}
description={'If disabled this schedule and it\'s associated tasks will not run.'}
label={'Schedule Enabled'}
/>
</div>
<div css={tw`mt-6 text-right`}>
<Button css={tw`w-full sm:w-auto`} type={'submit'} disabled={isSubmitting}>
{schedule ? 'Save changes' : 'Create schedule'}
</Button>
</div>
</Form>
)}
</Formik> </Formik>
); );
}; };
export default asModal<Props>()(EditScheduleModal);