Skip to content

Commit 78ed343

Browse files
committed
Support deleting a task from a schedule
1 parent 5345a2a commit 78ed343

File tree

10 files changed

+172
-8
lines changed

10 files changed

+172
-8
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Pterodactyl\Exceptions\Http;
4+
5+
use Illuminate\Http\Response;
6+
use Symfony\Component\HttpKernel\Exception\HttpException;
7+
8+
class HttpForbiddenException extends HttpException
9+
{
10+
/**
11+
* HttpForbiddenException constructor.
12+
*
13+
* @param string|null $message
14+
* @param \Throwable|null $previous
15+
*/
16+
public function __construct(string $message = null, \Throwable $previous = null)
17+
{
18+
parent::__construct(Response::HTTP_FORBIDDEN, $message, $previous);
19+
}
20+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Pterodactyl\Http\Controllers\Api\Client\Servers;
4+
5+
use Pterodactyl\Models\Task;
6+
use Illuminate\Http\Response;
7+
use Pterodactyl\Models\Server;
8+
use Pterodactyl\Models\Schedule;
9+
use Illuminate\Http\JsonResponse;
10+
use Pterodactyl\Models\Permission;
11+
use Pterodactyl\Repositories\Eloquent\TaskRepository;
12+
use Pterodactyl\Exceptions\Http\HttpForbiddenException;
13+
use Pterodactyl\Http\Requests\Api\Client\ClientApiRequest;
14+
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
15+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
16+
17+
class ScheduleTaskController extends ClientApiController
18+
{
19+
/**
20+
* @var \Pterodactyl\Repositories\Eloquent\TaskRepository
21+
*/
22+
private $repository;
23+
24+
/**
25+
* ScheduleTaskController constructor.
26+
*
27+
* @param \Pterodactyl\Repositories\Eloquent\TaskRepository $repository
28+
*/
29+
public function __construct(TaskRepository $repository)
30+
{
31+
parent::__construct();
32+
33+
$this->repository = $repository;
34+
}
35+
36+
/**
37+
* Determines if a user can delete the task for a given server.
38+
*
39+
* @param \Pterodactyl\Http\Requests\Api\Client\ClientApiRequest $request
40+
* @param \Pterodactyl\Models\Server $server
41+
* @param \Pterodactyl\Models\Schedule $schedule
42+
* @param \Pterodactyl\Models\Task $task
43+
* @return \Illuminate\Http\JsonResponse
44+
*/
45+
public function delete(ClientApiRequest $request, Server $server, Schedule $schedule, Task $task)
46+
{
47+
if ($task->schedule_id !== $schedule->id || $schedule->server_id !== $server->id) {
48+
throw new NotFoundHttpException;
49+
}
50+
51+
if (! $request->user()->can(Permission::ACTION_SCHEDULE_UPDATE, $server)) {
52+
throw new HttpForbiddenException('You do not have permission to perform this action.');
53+
}
54+
55+
$this->repository->delete($task->id);
56+
57+
return JsonResponse::create(null, Response::HTTP_NO_CONTENT);
58+
}
59+
}

app/Http/Requests/Api/Client/ClientApiRequest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
use Pterodactyl\Http\Requests\Api\Application\ApplicationApiRequest;
99

1010
/**
11-
* @method User user($guard = null)
11+
* @method \Pterodactyl\Models\User user($guard = null)
1212
*/
13-
abstract class ClientApiRequest extends ApplicationApiRequest
13+
class ClientApiRequest extends ApplicationApiRequest
1414
{
1515
/**
1616
* Determine if the current user is authorized to perform the requested action against the API.

app/Models/Permission.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ class Permission extends Validable
1212
*/
1313
const RESOURCE_NAME = 'subuser_permission';
1414

15+
/**
16+
* Constants defining different permissions available.
17+
*/
18+
const ACTION_SCHEDULE_READ = 'schedule.read';
19+
const ACTION_SCHEDULE_CREATE = 'schedule.create';
20+
const ACTION_SCHEDULE_UPDATE = 'schedule.update';
21+
const ACTION_SCHEDULE_DELETE = 'schedule.delete';
22+
1523
/**
1624
* Should timestamps be used on this model.
1725
*
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import http from '@/api/http';
2+
3+
export default (uuid: string, scheduleId: number, taskId: number): Promise<void> => {
4+
return new Promise((resolve, reject) => {
5+
http.delete(`/api/client/servers/${uuid}/schedules/${scheduleId}/tasks/${taskId}`)
6+
.then(() => resolve())
7+
.catch(reject);
8+
})
9+
};

resources/scripts/components/elements/SpinnerOverlay.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ interface Props {
1313
const SpinnerOverlay = ({ size, fixed, visible, backgroundOpacity }: Props) => (
1414
<CSSTransition timeout={150} classNames={'fade'} in={visible} unmountOnExit={true}>
1515
<div
16-
className={classNames('z-50 pin-t pin-l flex items-center justify-center w-full h-full rounded', {
16+
className={classNames('pin-t pin-l flex items-center justify-center w-full h-full rounded', {
1717
absolute: !fixed,
1818
fixed: fixed,
1919
})}
20-
style={{ background: `rgba(0, 0, 0, ${backgroundOpacity || 0.45})` }}
20+
style={{ zIndex: 9999, background: `rgba(0, 0, 0, ${backgroundOpacity || 0.45})` }}
2121
>
2222
<Spinner size={size}/>
2323
</div>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from 'react';
2+
import Modal, { RequiredModalProps } from '@/components/elements/Modal';
3+
4+
type Props = RequiredModalProps & {
5+
onConfirmed: () => void;
6+
}
7+
8+
export default ({ onConfirmed, ...props }: Props) => (
9+
<Modal {...props}>
10+
<h2>Confirm task deletion</h2>
11+
<p className={'text-sm mt-4'}>
12+
Are you sure you want to delete this task? This action cannot be undone.
13+
</p>
14+
<div className={'flex items-center justify-end mt-8'}>
15+
<button className={'btn btn-secondary btn-sm'} onClick={() => props.onDismissed()}>
16+
Cancel
17+
</button>
18+
<button className={'btn btn-red btn-sm ml-4'} onClick={() => {
19+
props.onDismissed();
20+
onConfirmed();
21+
}}>
22+
Delete Task
23+
</button>
24+
</div>
25+
</Modal>
26+
);

resources/scripts/components/server/schedules/ScheduleEditContainer.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,14 @@ export default ({ match, location: { state } }: RouteComponentProps<Params, {},
7979
key={task.id}
8080
className={'bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}
8181
>
82-
<ScheduleTaskRow task={task}/>
82+
<ScheduleTaskRow
83+
task={task}
84+
schedule={schedule.id}
85+
onTaskRemoved={() => setSchedule(s => ({
86+
...s!,
87+
tasks: s!.tasks.filter(t => t.id !== task.id),
88+
}))}
89+
/>
8390
</div>
8491
))
8592
}

resources/scripts/components/server/schedules/ScheduleTaskRow.tsx

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,49 @@
1-
import React from 'react';
2-
import { Task } from '@/api/server/schedules/getServerSchedules';
1+
import React, { useState } from 'react';
2+
import { Schedule, Task } from '@/api/server/schedules/getServerSchedules';
33
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
44
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
55
import { faCode } from '@fortawesome/free-solid-svg-icons/faCode';
66
import { faToggleOn } from '@fortawesome/free-solid-svg-icons/faToggleOn';
7+
import ConfirmTaskDeletionModal from '@/components/server/schedules/ConfirmTaskDeletionModal';
8+
import { ServerContext } from '@/state/server';
9+
import { Actions, useStoreActions } from 'easy-peasy';
10+
import { ApplicationStore } from '@/state';
11+
import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask';
12+
import { httpErrorToHuman } from '@/api/http';
13+
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
714

815
interface Props {
16+
schedule: number;
917
task: Task;
18+
onTaskRemoved: () => void;
1019
}
1120

12-
export default ({ task }: Props) => {
21+
export default ({ schedule, task, onTaskRemoved }: Props) => {
22+
const [visible, setVisible] = useState(false);
23+
const [isLoading, setIsLoading] = useState(false);
24+
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
25+
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
26+
27+
const onConfirmDeletion = () => {
28+
setIsLoading(true);
29+
clearFlashes('schedules');
30+
deleteScheduleTask(uuid, schedule, task.id)
31+
.then(() => onTaskRemoved())
32+
.catch(error => {
33+
console.error(error);
34+
setIsLoading(false);
35+
addError({ message: httpErrorToHuman(error), key: 'schedules' });
36+
});
37+
};
38+
1339
return (
1440
<div className={'flex items-center'}>
41+
<SpinnerOverlay visible={isLoading} fixed={true} size={'large'}/>
42+
<ConfirmTaskDeletionModal
43+
visible={visible}
44+
onDismissed={() => setVisible(false)}
45+
onConfirmed={() => onConfirmDeletion()}
46+
/>
1547
<FontAwesomeIcon icon={task.action === 'command' ? faCode : faToggleOn} className={'text-lg text-white'}/>
1648
<div className={'flex-1'}>
1749
<p className={'ml-6 text-neutral-300 mb-2 uppercase text-xs'}>
@@ -34,7 +66,9 @@ export default ({ task }: Props) => {
3466
<div>
3567
<a
3668
href={'#'}
69+
aria-label={'Delete scheduled task'}
3770
className={'text-sm p-2 text-neutral-500 hover:text-red-600 transition-color duration-150'}
71+
onClick={() => setVisible(true)}
3872
>
3973
<FontAwesomeIcon icon={faTrashAlt}/>
4074
</a>

routes/api-client.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
Route::group(['prefix' => '/schedules'], function () {
6363
Route::get('/', 'Servers\ScheduleController@index');
6464
Route::get('/{schedule}', 'Servers\ScheduleController@view');
65+
Route::delete('/{schedule}/tasks/{task}', 'Servers\ScheduleTaskController@delete');
6566
});
6667

6768
Route::group(['prefix' => '/network'], function () {

0 commit comments

Comments
 (0)