Update UI to support setting "Continue on Error" for tasks
This commit is contained in:
parent
92cd659db3
commit
ea057cb1cb
|
@ -5,15 +5,16 @@ interface Data {
|
||||||
action: string;
|
action: string;
|
||||||
payload: string;
|
payload: string;
|
||||||
timeOffset: string | number;
|
timeOffset: string | number;
|
||||||
|
continueOnFailure: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (uuid: string, schedule: number, task: number | undefined, { timeOffset, ...data }: Data): Promise<Task> => {
|
export default async (uuid: string, schedule: number, task: number | undefined, data: Data): Promise<Task> => {
|
||||||
return new Promise((resolve, reject) => {
|
const { data: response } = await http.post(`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`, {
|
||||||
http.post(`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`, {
|
action: data.action,
|
||||||
...data,
|
payload: data.payload,
|
||||||
time_offset: timeOffset,
|
continue_on_failure: data.continueOnFailure,
|
||||||
})
|
time_offset: data.timeOffset,
|
||||||
.then(({ data }) => resolve(rawDataToServerTask(data.attributes)))
|
|
||||||
.catch(reject);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return rawDataToServerTask(response.attributes);
|
||||||
};
|
};
|
||||||
|
|
|
@ -104,15 +104,15 @@ const EditScheduleModal = ({ schedule }: Props) => {
|
||||||
</p>
|
</p>
|
||||||
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
|
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
|
||||||
<FormikSwitch
|
<FormikSwitch
|
||||||
name={'only_when_online'}
|
name={'onlyWhenOnline'}
|
||||||
description={'If disabled this schedule will always run, regardless of the server\'s current power state.'}
|
description={'Only execute this schedule when the server is in a running state.'}
|
||||||
label={'Only When Server Is Online'}
|
label={'Only When Server Is Online'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
|
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
|
||||||
<FormikSwitch
|
<FormikSwitch
|
||||||
name={'enabled'}
|
name={'enabled'}
|
||||||
description={'If disabled this schedule and it\'s associated tasks will not run.'}
|
description={'This schedule will be executed automatically if enabled.'}
|
||||||
label={'Schedule Enabled'}
|
label={'Schedule Enabled'}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,12 +13,7 @@ export default ({ schedule }: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{visible &&
|
<TaskDetailsModal schedule={schedule} visible={visible} onModalDismissed={() => setVisible(false)}/>
|
||||||
<TaskDetailsModal
|
|
||||||
schedule={schedule}
|
|
||||||
onDismissed={() => setVisible(false)}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
<Button onClick={() => setVisible(true)} css={tw`flex-1`}>
|
<Button onClick={() => setVisible(true)} css={tw`flex-1`}>
|
||||||
New Task
|
New Task
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -67,7 +67,7 @@ export default () => {
|
||||||
}
|
}
|
||||||
<Can action={'schedule.create'}>
|
<Can action={'schedule.create'}>
|
||||||
<div css={tw`mt-8 flex justify-end`}>
|
<div css={tw`mt-8 flex justify-end`}>
|
||||||
{visible && <EditScheduleModal appear visible onDismissed={() => setVisible(false)}/>}
|
<EditScheduleModal visible={visible} onModalDismissed={() => setVisible(false)}/>
|
||||||
<Button type={'button'} onClick={() => setVisible(true)}>
|
<Button type={'button'} onClick={() => setVisible(true)}>
|
||||||
Create schedule
|
Create schedule
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -153,7 +153,7 @@ export default () => {
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EditScheduleModal visible={showEditModal} schedule={schedule} onDismissed={toggleEditModal}/>
|
<EditScheduleModal visible={showEditModal} schedule={schedule} onModalDismissed={toggleEditModal}/>
|
||||||
<div css={tw`mt-6 flex sm:justify-end`}>
|
<div css={tw`mt-6 flex sm:justify-end`}>
|
||||||
<Can action={'schedule.delete'}>
|
<Can action={'schedule.delete'}>
|
||||||
<DeleteScheduleButton
|
<DeleteScheduleButton
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
|
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faClock, faCode, faFileArchive, faPencilAlt, faToggleOn, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
|
import {
|
||||||
|
faArrowCircleDown,
|
||||||
|
faClock,
|
||||||
|
faCode,
|
||||||
|
faFileArchive,
|
||||||
|
faPencilAlt,
|
||||||
|
faToggleOn,
|
||||||
|
faTrashAlt,
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
|
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
|
@ -59,11 +67,12 @@ export default ({ schedule, task }: Props) => {
|
||||||
return (
|
return (
|
||||||
<div css={tw`sm:flex items-center p-3 sm:p-6 border-b border-neutral-800`}>
|
<div css={tw`sm:flex items-center p-3 sm:p-6 border-b border-neutral-800`}>
|
||||||
<SpinnerOverlay visible={isLoading} fixed size={'large'}/>
|
<SpinnerOverlay visible={isLoading} fixed size={'large'}/>
|
||||||
{isEditing && <TaskDetailsModal
|
<TaskDetailsModal
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
task={task}
|
task={task}
|
||||||
onDismissed={() => setIsEditing(false)}
|
visible={isEditing}
|
||||||
/>}
|
onModalDismissed={() => setIsEditing(false)}
|
||||||
|
/>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
title={'Confirm task deletion'}
|
title={'Confirm task deletion'}
|
||||||
buttonText={'Delete Task'}
|
buttonText={'Delete Task'}
|
||||||
|
@ -89,6 +98,14 @@ export default ({ schedule, task }: Props) => {
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-3 sm:mt-0 flex items-center w-full sm:w-auto`}>
|
<div css={tw`mt-3 sm:mt-0 flex items-center w-full sm:w-auto`}>
|
||||||
|
{task.continueOnFailure &&
|
||||||
|
<div css={tw`mr-6`}>
|
||||||
|
<div css={tw`flex items-center px-2 py-1 bg-yellow-500 text-yellow-800 text-sm rounded-full`}>
|
||||||
|
<Icon icon={faArrowCircleDown} css={tw`w-3 h-3 mr-2`}/>
|
||||||
|
Continues on Failure
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
{task.sequenceId > 1 && task.timeOffset > 0 &&
|
{task.sequenceId > 1 && task.timeOffset > 0 &&
|
||||||
<div css={tw`mr-6`}>
|
<div css={tw`mr-6`}>
|
||||||
<div css={tw`flex items-center px-2 py-1 bg-neutral-500 text-sm rounded-full`}>
|
<div css={tw`flex items-center px-2 py-1 bg-neutral-500 text-sm rounded-full`}>
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useContext, useEffect } from 'react';
|
||||||
import Modal from '@/components/elements/Modal';
|
|
||||||
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
|
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
|
||||||
import { Field as FormikField, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
|
import { Field as FormikField, Form, Formik, FormikHelpers, useField } from 'formik';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import createOrUpdateScheduleTask from '@/api/server/schedules/createOrUpdateScheduleTask';
|
import createOrUpdateScheduleTask from '@/api/server/schedules/createOrUpdateScheduleTask';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import Field from '@/components/elements/Field';
|
import Field from '@/components/elements/Field';
|
||||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||||
import { number, object, string } from 'yup';
|
import { boolean, number, object, string } from 'yup';
|
||||||
import useFlash from '@/plugins/useFlash';
|
import useFlash from '@/plugins/useFlash';
|
||||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||||
import tw from 'twin.macro';
|
import tw from 'twin.macro';
|
||||||
|
@ -15,40 +14,106 @@ import Label from '@/components/elements/Label';
|
||||||
import { Textarea } from '@/components/elements/Input';
|
import { Textarea } from '@/components/elements/Input';
|
||||||
import Button from '@/components/elements/Button';
|
import Button from '@/components/elements/Button';
|
||||||
import Select from '@/components/elements/Select';
|
import Select from '@/components/elements/Select';
|
||||||
|
import ModalContext from '@/context/ModalContext';
|
||||||
|
import asModal from '@/hoc/asModal';
|
||||||
|
import FormikSwitch from '@/components/elements/FormikSwitch';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
schedule: Schedule;
|
schedule: Schedule;
|
||||||
// If a task is provided we can assume we're editing it. If not provided,
|
// If a task is provided we can assume we're editing it. If not provided,
|
||||||
// we are creating a new one.
|
// we are creating a new one.
|
||||||
task?: Task;
|
task?: Task;
|
||||||
onDismissed: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Values {
|
interface Values {
|
||||||
action: string;
|
action: string;
|
||||||
payload: string;
|
payload: string;
|
||||||
timeOffset: string;
|
timeOffset: string;
|
||||||
|
continueOnFailure: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
const schema = object().shape({
|
||||||
const { values: { action }, initialValues, setFieldValue, setFieldTouched, isSubmitting } = useFormikContext<Values>();
|
action: string().required().oneOf([ 'command', 'power', 'backup' ]),
|
||||||
|
payload: string().when('action', {
|
||||||
|
is: v => v !== 'backup',
|
||||||
|
then: string().required('A task payload must be provided.'),
|
||||||
|
otherwise: string(),
|
||||||
|
}),
|
||||||
|
continueOnFailure: boolean(),
|
||||||
|
timeOffset: number().typeError('The time offset must be a valid number between 0 and 900.')
|
||||||
|
.required('A time offset value must be provided.')
|
||||||
|
.min(0, 'The time offset must be at least 0 seconds.')
|
||||||
|
.max(900, 'The time offset must be less than 900 seconds.'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ActionListener = () => {
|
||||||
|
const [ { value }, { initialValue: initialAction } ] = useField<string>('action');
|
||||||
|
const [ , { initialValue: initialPayload }, { setValue, setTouched } ] = useField<string>('payload');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (action !== initialValues.action) {
|
if (value !== initialAction) {
|
||||||
setFieldValue('payload', action === 'power' ? 'start' : '');
|
setValue(value === 'power' ? 'start' : '');
|
||||||
setFieldTouched('payload', false);
|
setTouched(false);
|
||||||
} else {
|
} else {
|
||||||
setFieldValue('payload', initialValues.payload);
|
setValue(initialPayload || '');
|
||||||
setFieldTouched('payload', false);
|
setTouched(false);
|
||||||
}
|
}
|
||||||
}, [ action ]);
|
}, [ value ]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TaskDetailsModal = ({ schedule, task }: Props) => {
|
||||||
|
const { dismiss } = useContext(ModalContext);
|
||||||
|
const { clearFlashes, addError } = useFlash();
|
||||||
|
|
||||||
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
|
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
clearFlashes('schedule:task');
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||||
|
clearFlashes('schedule:task');
|
||||||
|
createOrUpdateScheduleTask(uuid, schedule.id, task?.id, values)
|
||||||
|
.then(task => {
|
||||||
|
let tasks = schedule.tasks.map(t => t.id === task.id ? task : t);
|
||||||
|
if (!schedule.tasks.find(t => t.id === task.id)) {
|
||||||
|
tasks = [ ...tasks, task ];
|
||||||
|
}
|
||||||
|
|
||||||
|
appendSchedule({ ...schedule, tasks });
|
||||||
|
dismiss();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
setSubmitting(false);
|
||||||
|
addError({ message: httpErrorToHuman(error), key: 'schedule:task' });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Formik
|
||||||
|
onSubmit={submit}
|
||||||
|
validationSchema={schema}
|
||||||
|
initialValues={{
|
||||||
|
action: task?.action || 'command',
|
||||||
|
payload: task?.payload || '',
|
||||||
|
timeOffset: task?.timeOffset.toString() || '0',
|
||||||
|
continueOnFailure: task?.continueOnFailure || false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ isSubmitting, values }) => (
|
||||||
<Form css={tw`m-0`}>
|
<Form css={tw`m-0`}>
|
||||||
<h2 css={tw`text-2xl mb-6`}>{isEditingTask ? 'Edit Task' : 'Create Task'}</h2>
|
<FlashMessageRender byKey={'schedule:task'} css={tw`mb-4`}/>
|
||||||
|
<h2 css={tw`text-2xl mb-6`}>{task ? 'Edit Task' : 'Create Task'}</h2>
|
||||||
<div css={tw`flex`}>
|
<div css={tw`flex`}>
|
||||||
<div css={tw`mr-2 w-1/3`}>
|
<div css={tw`mr-2 w-1/3`}>
|
||||||
<Label>Action</Label>
|
<Label>Action</Label>
|
||||||
|
<ActionListener/>
|
||||||
<FormikFieldWrapper name={'action'}>
|
<FormikFieldWrapper name={'action'}>
|
||||||
<FormikField as={Select} name={'action'}>
|
<FormikField as={Select} name={'action'}>
|
||||||
<option value={'command'}>Send command</option>
|
<option value={'command'}>Send command</option>
|
||||||
|
@ -66,7 +131,7 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div css={tw`mt-6`}>
|
<div css={tw`mt-6`}>
|
||||||
{action === 'command' ?
|
{values.action === 'command' ?
|
||||||
<div>
|
<div>
|
||||||
<Label>Payload</Label>
|
<Label>Payload</Label>
|
||||||
<FormikFieldWrapper name={'payload'}>
|
<FormikFieldWrapper name={'payload'}>
|
||||||
|
@ -74,7 +139,7 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||||
</FormikFieldWrapper>
|
</FormikFieldWrapper>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
action === 'power' ?
|
values.action === 'power' ?
|
||||||
<div>
|
<div>
|
||||||
<Label>Payload</Label>
|
<Label>Payload</Label>
|
||||||
<FormikFieldWrapper name={'payload'}>
|
<FormikFieldWrapper name={'payload'}>
|
||||||
|
@ -98,75 +163,22 @@ const TaskDetailsForm = ({ isEditingTask }: { isEditingTask: boolean }) => {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
<div css={tw`mt-6 bg-neutral-700 border border-neutral-800 shadow-inner p-4 rounded`}>
|
||||||
|
<FormikSwitch
|
||||||
|
name={'continueOnFailure'}
|
||||||
|
description={'Future tasks will be run when this task fails.'}
|
||||||
|
label={'Continue on Failure'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div css={tw`flex justify-end mt-6`}>
|
<div css={tw`flex justify-end mt-6`}>
|
||||||
<Button type={'submit'} disabled={isSubmitting}>
|
<Button type={'submit'} disabled={isSubmitting}>
|
||||||
{isEditingTask ? 'Save Changes' : 'Create Task'}
|
{task ? 'Save Changes' : 'Create Task'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ({ task, schedule, onDismissed }: Props) => {
|
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
|
||||||
const { clearFlashes, addError } = useFlash();
|
|
||||||
const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
clearFlashes('schedule:task');
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
|
||||||
clearFlashes('schedule:task');
|
|
||||||
createOrUpdateScheduleTask(uuid, schedule.id, task?.id, values)
|
|
||||||
.then(task => {
|
|
||||||
let tasks = schedule.tasks.map(t => t.id === task.id ? task : t);
|
|
||||||
if (!schedule.tasks.find(t => t.id === task.id)) {
|
|
||||||
tasks = [ ...tasks, task ];
|
|
||||||
}
|
|
||||||
|
|
||||||
appendSchedule({ ...schedule, tasks });
|
|
||||||
onDismissed();
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
setSubmitting(false);
|
|
||||||
addError({ message: httpErrorToHuman(error), key: 'schedule:task' });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Formik
|
|
||||||
onSubmit={submit}
|
|
||||||
initialValues={{
|
|
||||||
action: task?.action || 'command',
|
|
||||||
payload: task?.payload || '',
|
|
||||||
timeOffset: task?.timeOffset.toString() || '0',
|
|
||||||
}}
|
|
||||||
validationSchema={object().shape({
|
|
||||||
action: string().required().oneOf([ 'command', 'power', 'backup' ]),
|
|
||||||
payload: string().when('action', {
|
|
||||||
is: v => v !== 'backup',
|
|
||||||
then: string().required('A task payload must be provided.'),
|
|
||||||
otherwise: string(),
|
|
||||||
}),
|
|
||||||
timeOffset: number().typeError('The time offset must be a valid number between 0 and 900.')
|
|
||||||
.required('A time offset value must be provided.')
|
|
||||||
.min(0, 'The time offset must be at least 0 seconds.')
|
|
||||||
.max(900, 'The time offset must be less than 900 seconds.'),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{({ isSubmitting }) => (
|
|
||||||
<Modal
|
|
||||||
visible
|
|
||||||
appear
|
|
||||||
onDismissed={() => onDismissed()}
|
|
||||||
showSpinnerOverlay={isSubmitting}
|
|
||||||
>
|
|
||||||
<FlashMessageRender byKey={'schedule:task'} css={tw`mb-4`} />
|
|
||||||
<TaskDetailsForm isEditingTask={typeof task !== 'undefined'} />
|
|
||||||
</Modal>
|
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default asModal<Props>()(TaskDetailsModal);
|
||||||
|
|
Loading…
Reference in New Issue