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\Models\Permission;
|
||||||
use Pterodactyl\Repositories\Eloquent\TaskRepository;
|
use Pterodactyl\Repositories\Eloquent\TaskRepository;
|
||||||
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
|
||||||
|
use Pterodactyl\Transformers\Api\Client\TaskTransformer;
|
||||||
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
|
||||||
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 Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreTaskRequest;
|
||||||
|
|
||||||
class ScheduleTaskController extends ClientApiController
|
class ScheduleTaskController extends ClientApiController
|
||||||
{
|
{
|
||||||
|
@ -33,6 +35,67 @@ class ScheduleTaskController extends ClientApiController
|
||||||
$this->repository = $repository;
|
$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.
|
* 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
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $attributes = [
|
protected $attributes = [
|
||||||
|
'time_offset' => 0,
|
||||||
'is_queued' => false,
|
'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
|
schedule.tasks
|
||||||
.sort((a, b) => a.sequenceId - b.sequenceId)
|
.sort((a, b) => a.sequenceId - b.sequenceId)
|
||||||
.map(task => (
|
.map(task => (
|
||||||
<div
|
|
||||||
key={task.id}
|
|
||||||
className={'bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}
|
|
||||||
>
|
|
||||||
<ScheduleTaskRow
|
<ScheduleTaskRow
|
||||||
|
key={task.id}
|
||||||
task={task}
|
task={task}
|
||||||
schedule={schedule.id}
|
schedule={schedule.id}
|
||||||
|
onTaskUpdated={task => setSchedule(s => ({
|
||||||
|
...s!, tasks: s!.tasks.map(t => t.id === task.id ? task : t),
|
||||||
|
}))}
|
||||||
onTaskRemoved={() => setSchedule(s => ({
|
onTaskRemoved={() => setSchedule(s => ({
|
||||||
...s!,
|
...s!, tasks: s!.tasks.filter(t => t.id !== task.id),
|
||||||
tasks: s!.tasks.filter(t => t.id !== task.id),
|
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
{schedule.tasks.length > 1 &&
|
{schedule.tasks.length > 1 &&
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react';
|
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 { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
|
||||||
import { faCode } from '@fortawesome/free-solid-svg-icons/faCode';
|
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 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';
|
||||||
|
import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal';
|
||||||
|
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
schedule: number;
|
schedule: number;
|
||||||
task: Task;
|
task: Task;
|
||||||
|
onTaskUpdated: (task: Task) => void;
|
||||||
onTaskRemoved: () => void;
|
onTaskRemoved: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ({ schedule, task, onTaskRemoved }: Props) => {
|
export default ({ schedule, task, onTaskUpdated, onTaskRemoved }: Props) => {
|
||||||
const [ visible, setVisible ] = useState(false);
|
const [ visible, setVisible ] = useState(false);
|
||||||
const [ isLoading, setIsLoading ] = useState(false);
|
const [ isLoading, setIsLoading ] = useState(false);
|
||||||
|
const [ isEditing, setIsEditing ] = useState(false);
|
||||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||||
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||||
|
|
||||||
|
@ -37,8 +41,16 @@ export default ({ schedule, task, onTaskRemoved }: Props) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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'}/>
|
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
|
||||||
|
{isEditing && <TaskDetailsModal
|
||||||
|
scheduleId={schedule}
|
||||||
|
task={task}
|
||||||
|
onDismissed={task => {
|
||||||
|
task && onTaskUpdated(task);
|
||||||
|
setIsEditing(false);
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
<ConfirmTaskDeletionModal
|
<ConfirmTaskDeletionModal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
onDismissed={() => setVisible(false)}
|
onDismissed={() => setVisible(false)}
|
||||||
|
@ -63,16 +75,22 @@ export default ({ schedule, task, onTaskRemoved }: Props) => {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
<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
|
<a
|
||||||
href={'#'}
|
href={'#'}
|
||||||
aria-label={'Delete scheduled task'}
|
aria-label={'Delete scheduled task'}
|
||||||
className={'text-sm p-2 text-neutral-500 hover:text-red-600 transition-color duration-150'}
|
className={'block text-sm p-2 text-neutral-500 hover:text-red-600 transition-color duration-150'}
|
||||||
onClick={() => setVisible(true)}
|
onClick={() => setVisible(true)}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faTrashAlt}/>
|
<FontAwesomeIcon icon={faTrashAlt}/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</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::group(['prefix' => '/schedules'], function () {
|
||||||
Route::get('/', 'Servers\ScheduleController@index');
|
Route::get('/', 'Servers\ScheduleController@index');
|
||||||
Route::get('/{schedule}', 'Servers\ScheduleController@view');
|
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');
|
Route::delete('/{schedule}/tasks/{task}', 'Servers\ScheduleTaskController@delete');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue