diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php index 4809e6c2b..340e2541a 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleTaskController.php @@ -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. * diff --git a/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php b/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php new file mode 100644 index 000000000..990102e74 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Schedules/StoreTaskRequest.php @@ -0,0 +1,34 @@ + 'required|in:command,power', + 'payload' => 'required|string', + 'time_offset' => 'required|numeric|min:0|max:900', + 'sequence_id' => 'sometimes|required|numeric|min:1', + ]; + } +} diff --git a/app/Models/Task.php b/app/Models/Task.php index 93c9b6008..83d4119ff 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -79,6 +79,7 @@ class Task extends Validable * @var array */ protected $attributes = [ + 'time_offset' => 0, 'is_queued' => false, ]; diff --git a/resources/scripts/api/server/schedules/createOrUpdateScheduleTask.ts b/resources/scripts/api/server/schedules/createOrUpdateScheduleTask.ts new file mode 100644 index 000000000..d48214d09 --- /dev/null +++ b/resources/scripts/api/server/schedules/createOrUpdateScheduleTask.ts @@ -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 => { + 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); + }); +}; diff --git a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx index caa742217..7ee73ab26 100644 --- a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx @@ -75,19 +75,17 @@ export default ({ match, location: { state } }: RouteComponentProps a.sequenceId - b.sequenceId) .map(task => ( -
- setSchedule(s => ({ - ...s!, - tasks: s!.tasks.filter(t => t.id !== task.id), - }))} - /> -
+ 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 && diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index 9c4cf5cca..f118a2144 100644 --- a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -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) => actions.flashes); @@ -37,8 +41,16 @@ export default ({ schedule, task, onTaskRemoved }: Props) => { }; return ( -
+
+ {isEditing && { + task && onTaskUpdated(task); + setIsEditing(false); + }} + />} setVisible(false)} @@ -63,16 +75,22 @@ export default ({ schedule, task, onTaskRemoved }: Props) => {

} -
- setVisible(true)} - > - - -
+ setIsEditing(true)} + > + + + setVisible(true)} + > + +
); }; diff --git a/resources/scripts/components/server/schedules/TaskDetailsModal.tsx b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx new file mode 100644 index 000000000..dbe819338 --- /dev/null +++ b/resources/scripts/components/server/schedules/TaskDetailsModal.tsx @@ -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) => actions.flashes); + + useEffect(() => { + clearFlashes('schedule:task'); + }, []); + + const submit = (values: Values, { setSubmitting }: FormikHelpers) => { + 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 ( + + {({ values, isSubmitting }) => ( + onDismissed()} + showSpinnerOverlay={isSubmitting} + > + +
+

Edit Task

+
+
+ + + + + +
+
+ +
+
+
+ +
+
+ +
+
+
+ )} +
+ ); +}; diff --git a/routes/api-client.php b/routes/api-client.php index 545798ff5..d23c566be 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -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'); });