Skip to content

Commit 5345a2a

Browse files
committed
Add initial task listing for schedules
1 parent b3fb658 commit 5345a2a

File tree

8 files changed

+259
-24
lines changed

8 files changed

+259
-24
lines changed

app/Http/Controllers/Api/Client/Servers/ScheduleController.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
use Illuminate\Http\Request;
66
use Pterodactyl\Models\Server;
7+
use Pterodactyl\Models\Schedule;
78
use Pterodactyl\Transformers\Api\Client\ScheduleTransformer;
89
use Pterodactyl\Http\Controllers\Api\Client\ClientApiController;
10+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
911

1012
class ScheduleController extends ClientApiController
1113
{
@@ -25,4 +27,25 @@ public function index(Request $request, Server $server)
2527
->transformWith($this->getTransformer(ScheduleTransformer::class))
2628
->toArray();
2729
}
30+
31+
/**
32+
* Returns a specific schedule for the server.
33+
*
34+
* @param \Illuminate\Http\Request $request
35+
* @param \Pterodactyl\Models\Server $server
36+
* @param \Pterodactyl\Models\Schedule $schedule
37+
* @return array
38+
*/
39+
public function view(Request $request, Server $server, Schedule $schedule)
40+
{
41+
if ($schedule->server_id !== $server->id) {
42+
throw new NotFoundHttpException;
43+
}
44+
45+
$schedule->loadMissing('tasks');
46+
47+
return $this->fractal->item($schedule)
48+
->transformWith($this->getTransformer(ScheduleTransformer::class))
49+
->toArray();
50+
}
2851
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import http from '@/api/http';
2+
import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules';
3+
4+
export default (uuid: string, schedule: number): Promise<Schedule> => {
5+
return new Promise((resolve, reject) => {
6+
http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, {
7+
params: {
8+
include: ['tasks'],
9+
},
10+
})
11+
.then(({ data }) => resolve(rawDataToServerSchedule(data.attributes)))
12+
.catch(reject);
13+
});
14+
};

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

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,15 @@ import React, { useMemo, useState } from 'react';
22
import getServerSchedules, { Schedule } from '@/api/server/schedules/getServerSchedules';
33
import { ServerContext } from '@/state/server';
44
import Spinner from '@/components/elements/Spinner';
5-
import { RouteComponentProps } from 'react-router-dom';
5+
import { RouteComponentProps, Link } from 'react-router-dom';
66
import FlashMessageRender from '@/components/FlashMessageRender';
77
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
88
import { httpErrorToHuman } from '@/api/http';
99
import { Actions, useStoreActions } from 'easy-peasy';
1010
import { ApplicationStore } from '@/state';
11-
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
1211

13-
interface Params {
14-
schedule?: string;
15-
}
16-
17-
export default ({ history, match }: RouteComponentProps<Params>) => {
18-
const { id, uuid } = ServerContext.useStoreState(state => state.server.data!);
19-
const [ active, setActive ] = useState(0);
12+
export default ({ match, history }: RouteComponentProps) => {
13+
const { uuid } = ServerContext.useStoreState(state => state.server.data!);
2014
const [ schedules, setSchedules ] = useState<Schedule[] | null>(null);
2115
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
2216

@@ -30,34 +24,26 @@ export default ({ history, match }: RouteComponentProps<Params>) => {
3024
});
3125
}, [ setSchedules ]);
3226

33-
const matched = useMemo(() => {
34-
return schedules?.find(schedule => schedule.id === active);
35-
}, [ active ]);
36-
3727
return (
3828
<div className={'my-10 mb-6'}>
3929
<FlashMessageRender byKey={'schedules'} className={'mb-4'}/>
4030
{!schedules ?
4131
<Spinner size={'large'} centered={true}/>
4232
:
4333
schedules.map(schedule => (
44-
<div
34+
<a
4535
key={schedule.id}
46-
onClick={() => setActive(schedule.id)}
36+
href={`${match.url}/${schedule.id}`}
4737
className={'grey-row-box cursor-pointer'}
38+
onClick={e => {
39+
e.preventDefault();
40+
history.push(`${match.url}/${schedule.id}`, { schedule });
41+
}}
4842
>
4943
<ScheduleRow schedule={schedule}/>
50-
</div>
44+
</a>
5145
))
5246
}
53-
{matched &&
54-
<EditScheduleModal
55-
schedule={matched}
56-
visible={true}
57-
appear={true}
58-
onDismissed={() => setActive(0)}
59-
/>
60-
}
6147
</div>
6248
);
6349
};
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import React, { useEffect, useState } from 'react';
2+
import { RouteComponentProps } from 'react-router-dom';
3+
import { Schedule } from '@/api/server/schedules/getServerSchedules';
4+
import getServerSchedule from '@/api/server/schedules/getServerSchedule';
5+
import { ServerContext } from '@/state/server';
6+
import Spinner from '@/components/elements/Spinner';
7+
import FlashMessageRender from '@/components/FlashMessageRender';
8+
import { Actions, useStoreActions } from 'easy-peasy';
9+
import { ApplicationStore } from '@/state';
10+
import { httpErrorToHuman } from '@/api/http';
11+
import ScheduleRow from '@/components/server/schedules/ScheduleRow';
12+
import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow';
13+
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
14+
15+
interface Params {
16+
id: string;
17+
}
18+
19+
interface State {
20+
schedule?: Schedule;
21+
}
22+
23+
export default ({ match, location: { state } }: RouteComponentProps<Params, {}, State>) => {
24+
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
25+
const [ isLoading, setIsLoading ] = useState(true);
26+
const [ showEditModal, setShowEditModal ] = useState(false);
27+
const [ schedule, setSchedule ] = useState<Schedule | undefined>(state?.schedule);
28+
const { clearFlashes, addError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
29+
30+
useEffect(() => {
31+
if (schedule?.id === Number(match.params.id)) {
32+
setIsLoading(false);
33+
return;
34+
}
35+
36+
clearFlashes('schedules');
37+
getServerSchedule(uuid, Number(match.params.id))
38+
.then(schedule => setSchedule(schedule))
39+
.catch(error => {
40+
console.error(error);
41+
addError({ message: httpErrorToHuman(error), key: 'schedules' });
42+
})
43+
.then(() => setIsLoading(false));
44+
}, [ schedule, match ]);
45+
46+
return (
47+
<div className={'my-10 mb-6'}>
48+
<FlashMessageRender byKey={'schedules'} className={'mb-4'}/>
49+
{!schedule || isLoading ?
50+
<Spinner size={'large'} centered={true}/>
51+
:
52+
<>
53+
<div className={'grey-row-box'}>
54+
<ScheduleRow schedule={schedule}/>
55+
</div>
56+
<EditScheduleModal
57+
visible={showEditModal}
58+
schedule={schedule}
59+
onDismissed={() => setShowEditModal(false)}
60+
/>
61+
<div className={'flex items-center my-4'}>
62+
<div className={'flex-1'}>
63+
<h2>Schedule Tasks</h2>
64+
</div>
65+
<button className={'btn btn-secondary btn-sm'} onClick={() => setShowEditModal(true)}>
66+
Edit
67+
</button>
68+
<button className={'btn btn-primary btn-sm ml-4'}>
69+
New Task
70+
</button>
71+
</div>
72+
{schedule?.tasks.length > 0 ?
73+
<>
74+
{
75+
schedule.tasks
76+
.sort((a, b) => a.sequenceId - b.sequenceId)
77+
.map(task => (
78+
<div
79+
key={task.id}
80+
className={'bg-neutral-700 border border-neutral-600 mb-2 px-6 py-4 rounded'}
81+
>
82+
<ScheduleTaskRow task={task}/>
83+
</div>
84+
))
85+
}
86+
{schedule.tasks.length > 1 &&
87+
<p className={'text-xs text-neutral-400'}>
88+
Task delays are relative to the previous task in the listing.
89+
</p>
90+
}
91+
</>
92+
:
93+
<p className={'text-sm text-neutral-400'}>
94+
There are no tasks configured for this schedule. Consider adding a new one using the
95+
button above.
96+
</p>
97+
}
98+
</>
99+
}
100+
</div>
101+
);
102+
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import { Task } from '@/api/server/schedules/getServerSchedules';
3+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
4+
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
5+
import { faCode } from '@fortawesome/free-solid-svg-icons/faCode';
6+
import { faToggleOn } from '@fortawesome/free-solid-svg-icons/faToggleOn';
7+
8+
interface Props {
9+
task: Task;
10+
}
11+
12+
export default ({ task }: Props) => {
13+
return (
14+
<div className={'flex items-center'}>
15+
<FontAwesomeIcon icon={task.action === 'command' ? faCode : faToggleOn} className={'text-lg text-white'}/>
16+
<div className={'flex-1'}>
17+
<p className={'ml-6 text-neutral-300 mb-2 uppercase text-xs'}>
18+
{task.action === 'command' ? 'Send command' : 'Send power action'}
19+
</p>
20+
<code className={'ml-6 font-mono bg-neutral-800 rounded py-1 px-2 text-sm'}>
21+
{task.payload}
22+
</code>
23+
</div>
24+
{task.sequenceId > 1 &&
25+
<div className={'mr-6'}>
26+
<p className={'text-center mb-1'}>
27+
{task.timeOffset}s
28+
</p>
29+
<p className={'text-neutral-300 uppercase text-2xs'}>
30+
Delay Run By
31+
</p>
32+
</div>
33+
}
34+
<div>
35+
<a
36+
href={'#'}
37+
className={'text-sm p-2 text-neutral-500 hover:text-red-600 transition-color duration-150'}
38+
>
39+
<FontAwesomeIcon icon={faTrashAlt}/>
40+
</a>
41+
</div>
42+
</div>
43+
);
44+
};

resources/scripts/routers/ServerRouter.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import SuspenseSpinner from '@/components/elements/SuspenseSpinner';
1313
import FileEditContainer from '@/components/server/files/FileEditContainer';
1414
import SettingsContainer from '@/components/server/settings/SettingsContainer';
1515
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
16+
import ScheduleEditContainer from '@/components/server/schedules/ScheduleEditContainer';
1617

1718
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
1819
const server = ServerContext.useStoreState(state => state.server.data);
@@ -63,6 +64,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
6364
<Route path={`${match.path}/databases`} component={DatabasesContainer} exact/>
6465
{/* <Route path={`${match.path}/users`} component={UsersContainer} exact/> */}
6566
<Route path={`${match.path}/schedules`} component={ScheduleContainer} exact/>
67+
<Route path={`${match.path}/schedules/:id`} component={ScheduleEditContainer} exact/>
6668
<Route path={`${match.path}/settings`} component={SettingsContainer} exact/>
6769
</Switch>
6870
</React.Fragment>

routes/api-client.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161

6262
Route::group(['prefix' => '/schedules'], function () {
6363
Route::get('/', 'Servers\ScheduleController@index');
64+
Route::get('/{schedule}', 'Servers\ScheduleController@view');
6465
});
6566

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

tailwind.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -837,6 +837,23 @@ module.exports = {
837837
'current': 'currentColor',
838838
},
839839

840+
transitionDuration: {
841+
'75': '75ms',
842+
'100': '100ms',
843+
'150': '150ms',
844+
'250': '250ms',
845+
'500': '500ms',
846+
'750': '750ms',
847+
'1000': '1000ms',
848+
},
849+
850+
transitionTimingFunction: {
851+
'linear': 'linear',
852+
'in': 'cubic-bezier(0.4, 0, 1, 1)',
853+
'out': 'cubic-bezier(0, 0, 0.2, 1)',
854+
'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
855+
},
856+
840857
/*
841858
|-----------------------------------------------------------------------------
842859
| Modules https://tailwindcss.com/docs/configuration#modules
@@ -925,6 +942,52 @@ module.exports = {
925942
require('tailwindcss/plugins/container')({
926943
center: true,
927944
}),
945+
946+
function ({ addUtilities }) {
947+
addUtilities({
948+
'.transition-none': {
949+
'transition-property': 'none',
950+
},
951+
'.transition-all': {
952+
'transition-property': 'all',
953+
},
954+
'.transition': {
955+
'transition-property': 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform',
956+
},
957+
'.transition-colors': {
958+
'transition-property': 'background-color, border-color, color, fill, stroke',
959+
},
960+
'.transition-opacity': {
961+
'transition-property': 'opacity',
962+
},
963+
'.transition-shadow': {
964+
'transition-property': 'box-shadow',
965+
},
966+
'.transition-transform': {
967+
'transition-property': 'transform',
968+
},
969+
}, ['hover', 'focus']);
970+
},
971+
972+
function ({ addUtilities, config }) {
973+
const durations = config('transitionDuration', {});
974+
975+
addUtilities(Object.keys(durations).map(key => ({
976+
[`.duration-${key}`]: {
977+
'transition-duration': durations[key],
978+
},
979+
})), ['hover', 'focus']);
980+
},
981+
982+
function ({ addUtilities, config }) {
983+
const timingFunctions = config('transitionTimingFunction', {});
984+
985+
addUtilities(Object.keys(timingFunctions).map(key => ({
986+
[`.ease-${key}`]: {
987+
'transition-timing-function': timingFunctions[key],
988+
},
989+
})));
990+
},
928991
],
929992

930993
/*

0 commit comments

Comments
 (0)