Add base support for editing an existing task
This commit is contained in:
parent
edb9657e2b
commit
ef38a51d6d
8 changed files with 266 additions and 27 deletions
|
@ -10,9 +10,11 @@ use Illuminate\Http\JsonResponse;
|
|||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Repositories\Eloquent\TaskRepository;
|
||||
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
||||
use Pterodactyl\Transformers\Api\Client\TaskTransformer;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest;
|
||||
|
||||
class ScheduleTaskController extends ClientApiController
|
||||
{
|
||||
|
@ -33,6 +35,67 @@ class ScheduleTaskController extends ClientApiController
|
|||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new task for a given schedule and store it in the database.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Schedule $schedule
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
*/
|
||||
public function store(StoreTaskRequest $request, Server $server, Schedule $schedule)
|
||||
{
|
||||
if ($schedule->server_id !== $server->id) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
$lastTask = $schedule->tasks->last();
|
||||
|
||||
/** @var \Pterodactyl\Models\Task $task */
|
||||
$task = $this->repository->create([
|
||||
'schedule_id' => $schedule->id,
|
||||
'sequence_id' => ($lastTask->sequence_id ?? 0) + 1,
|
||||
'action' => $request->input('action'),
|
||||
'payload' => $request->input('payload'),
|
||||
'time_offset' => $request->input('time_offset'),
|
||||
]);
|
||||
|
||||
return $this->fractal->item($task)
|
||||
->transformWith($this->getTransformer(TaskTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a given task for a server.
|
||||
*
|
||||
* @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest $request
|
||||
* @param \Pterodactyl\Models\Server $server
|
||||
* @param \Pterodactyl\Models\Schedule $schedule
|
||||
* @param \Pterodactyl\Models\Task $task
|
||||
* @return array
|
||||
*
|
||||
* @throws \Pterodactyl\Exceptions\Model\DataValidationException
|
||||
* @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException
|
||||
*/
|
||||
public function update(StoreTaskRequest $request, Server $server, Schedule $schedule, Task $task)
|
||||
{
|
||||
if ($schedule->id !== $task->schedule_id || $server->id !== $schedule->server_id) {
|
||||
throw new NotFoundHttpException;
|
||||
}
|
||||
|
||||
$this->repository->update($task->id, [
|
||||
'action' => $request->input('action'),
|
||||
'payload' => $request->input('payload'),
|
||||
'time_offset' => $request->input('time_offset'),
|
||||
]);
|
||||
|
||||
return $this->fractal->item($task->refresh())
|
||||
->transformWith($this->getTransformer(TaskTransformer::class))
|
||||
->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a user can delete the task for a given server.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace Pterodactyl\Http\Requests\Api\Client\Servers\Schedules;
|
||||
|
||||
use Pterodactyl\Models\Permission;
|
||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||
|
||||
class StoreTaskRequest extends ClientApiRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is allowed to create a new task for this schedule. We simply
|
||||
* check if they can modify a schedule to determine if they're able to do this. There
|
||||
* are no task specific permissions.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
return Permission::ACTION_SCHEDULE_UPDATE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'action' => 'required|in:command,power',
|
||||
'payload' => 'required|string',
|
||||
'time_offset' => 'required|numeric|min:0|max:900',
|
||||
'sequence_id' => 'sometimes|required|numeric|min:1',
|
||||
];
|
||||
}
|
||||
}
|
|
@ -79,6 +79,7 @@ class Task extends Validable
|
|||
* @var array
|
||||
*/
|
||||
protected $attributes = [
|
||||
'time_offset' => 0,
|
||||
'is_queued' => false,
|
||||
];
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import { rawDataToServerTask, Task } from '@/api/server/schedules/getServerSchedules';
|
||||
import http from '@/api/http';
|
||||
|
||||
interface Data {
|
||||
action: string;
|
||||
payload: string;
|
||||
timeOffset: string | number;
|
||||
}
|
||||
|
||||
export default (uuid: string, schedule: number, task: number | undefined, { timeOffset, ...data }: Data): Promise<Task> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.post(`/api/client/servers/${uuid}/schedules/${schedule}/tasks${task ? `/${task}` : ''}`, {
|
||||
...data,
|
||||
// eslint-disable-next-line @typescript-eslint/camelcase
|
||||
time_offset: timeOffset,
|
||||
})
|
||||
.then(({ data }) => resolve(rawDataToServerTask(data.attributes)))
|
||||
.catch(reject);
|
||||
});
|
||||
};
|
|
@ -75,19 +75,17 @@ export default ({ match, location: { state } }: RouteComponentProps<Params, {},
|
|||
schedule.tasks
|
||||
.sort((a, b) => a.sequenceId - b.sequenceId)
|
||||
.map(task => (
|
||||
<div
|
||||
<ScheduleTaskRow
|
||||
key={task.id}
|
||||
className={'bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}
|
||||
>
|
||||
<ScheduleTaskRow
|
||||
task={task}
|
||||
schedule={schedule.id}
|
||||
onTaskRemoved={() => setSchedule(s => ({
|
||||
...s!,
|
||||
tasks: s!.tasks.filter(t => t.id !== task.id),
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
task={task}
|
||||
schedule={schedule.id}
|
||||
onTaskUpdated={task => setSchedule(s => ({
|
||||
...s!, tasks: s!.tasks.map(t => t.id === task.id ? task : t),
|
||||
}))}
|
||||
onTaskRemoved={() => setSchedule(s => ({
|
||||
...s!, tasks: s!.tasks.filter(t => t.id !== task.id),
|
||||
}))}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{schedule.tasks.length > 1 &&
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
|
||||
import { Task } from '@/api/server/schedules/getServerSchedules';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
||||
import { faCode } from '@fortawesome/free-solid-svg-icons/faCode';
|
||||
|
@ -11,16 +11,20 @@ import { ApplicationStore } from '@/state';
|
|||
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
|
||||
|
||||
interface Props {
|
||||
schedule: number;
|
||||
task: Task;
|
||||
onTaskUpdated: (task: Task) => void;
|
||||
onTaskRemoved: () => void;
|
||||
}
|
||||
|
||||
export default ({ schedule, task, onTaskRemoved }: Props) => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
export default ({ schedule, task, onTaskUpdated, onTaskRemoved }: Props) => {
|
||||
const [ visible, setVisible ] = useState(false);
|
||||
const [ isLoading, setIsLoading ] = useState(false);
|
||||
const [ isEditing, setIsEditing ] = useState(false);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
|
@ -37,8 +41,16 @@ export default ({ schedule, task, onTaskRemoved }: Props) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className={'flex items-center'}>
|
||||
<div className={'flex items-center bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}>
|
||||
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
|
||||
{isEditing && <TaskDetailsModal
|
||||
scheduleId={schedule}
|
||||
task={task}
|
||||
onDismissed={task => {
|
||||
task && onTaskUpdated(task);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
/>}
|
||||
<ConfirmTaskDeletionModal
|
||||
visible={visible}
|
||||
onDismissed={() => setVisible(false)}
|
||||
|
@ -63,16 +75,22 @@ export default ({ schedule, task, onTaskRemoved }: Props) => {
|
|||
</p>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<a
|
||||
href={'#'}
|
||||
aria-label={'Delete scheduled task'}
|
||||
className={'text-sm p-2 text-neutral-500 hover:text-red-600 transition-color duration-150'}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt}/>
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href={'#'}
|
||||
aria-label={'Edit scheduled task'}
|
||||
className={'block text-sm p-2 text-neutral-500 hover:text-neutral-100 transition-color duration-150 mr-4'}
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPencilAlt}/>
|
||||
</a>
|
||||
<a
|
||||
href={'#'}
|
||||
aria-label={'Delete scheduled task'}
|
||||
className={'block text-sm p-2 text-neutral-500 hover:text-red-600 transition-color duration-150'}
|
||||
onClick={() => setVisible(true)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt}/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import Modal from '@/components/elements/Modal';
|
||||
import { Task } from '@/api/server/schedules/getServerSchedules';
|
||||
import { Form, Formik, Field as FormikField, FormikHelpers } from 'formik';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { Actions, useStoreActions } from 'easy-peasy';
|
||||
import { ApplicationStore } from '@/state';
|
||||
import createOrUpdateScheduleTask from '@/api/server/schedules/createOrUpdateScheduleTask';
|
||||
import { httpErrorToHuman } from '@/api/http';
|
||||
import Field from '@/components/elements/Field';
|
||||
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
||||
interface Props {
|
||||
scheduleId: number;
|
||||
// If a task is provided we can assume we're editing it. If not provided,
|
||||
// we are creating a new one.
|
||||
task?: Task;
|
||||
onDismissed: (task: Task | undefined | void) => void;
|
||||
}
|
||||
|
||||
interface Values {
|
||||
action: string;
|
||||
payload: string;
|
||||
timeOffset: string;
|
||||
}
|
||||
|
||||
export default ({ task, scheduleId, onDismissed }: Props) => {
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('schedule:task');
|
||||
}, []);
|
||||
|
||||
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
|
||||
clearFlashes('schedule:task');
|
||||
createOrUpdateScheduleTask(uuid, scheduleId, task?.id, values)
|
||||
.then(task => onDismissed(task))
|
||||
.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',
|
||||
}}
|
||||
>
|
||||
{({ values, isSubmitting }) => (
|
||||
<Modal
|
||||
visible={true}
|
||||
appear={true}
|
||||
onDismissed={() => onDismissed()}
|
||||
showSpinnerOverlay={isSubmitting}
|
||||
>
|
||||
<FlashMessageRender byKey={'schedule:task'} className={'mb-4'}/>
|
||||
<Form className={'m-0'}>
|
||||
<h3 className={'mb-6'}>Edit Task</h3>
|
||||
<div className={'flex'}>
|
||||
<div className={'mr-2'}>
|
||||
<label className={'input-dark-label'}>Action</label>
|
||||
<FormikField as={'select'} name={'action'} className={'input-dark'}>
|
||||
<option value={'command'}>Send command</option>
|
||||
<option value={'power'}>Send power action</option>
|
||||
</FormikField>
|
||||
</div>
|
||||
<div className={'flex-1'}>
|
||||
<Field
|
||||
name={'payload'}
|
||||
label={'Payload'}
|
||||
description={
|
||||
values.action === 'command'
|
||||
? 'The command to send to the server when this task executes.'
|
||||
: 'The power action to send when this task executes. Options are "start", "stop", "restart", or "kill".'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={'mt-6'}>
|
||||
<Field
|
||||
name={'timeOffset'}
|
||||
label={'Time offset (in seconds)'}
|
||||
description={'The amount of time to wait after the previous task executes before running this one. If this is the first task on a schedule this will not be applied.'}
|
||||
/>
|
||||
</div>
|
||||
<div className={'flex justify-end mt-6'}>
|
||||
<button type={'submit'} className={'btn btn-primary btn-sm'}>
|
||||
{task ? 'Save Changes' : 'Create Task'}
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
</Modal>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
|
@ -62,6 +62,8 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ
|
|||
Route::group(['prefix' => '/schedules'], function () {
|
||||
Route::get('/', 'Servers\ScheduleController@index');
|
||||
Route::get('/{schedule}', 'Servers\ScheduleController@view');
|
||||
Route::post('/{schedule}/tasks', 'Servers\ScheduleTaskController@store');
|
||||
Route::post('/{schedule}/tasks/{task}', 'Servers\ScheduleTaskController@update');
|
||||
Route::delete('/{schedule}/tasks/{task}', 'Servers\ScheduleTaskController@delete');
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue