diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php index ea74a394a..42e0a03fa 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php @@ -4,8 +4,10 @@ namespace Pterodactyl\Http\Controllers\Api\Client\Servers; use Illuminate\Http\Request; use Pterodactyl\Models\Server; +use Pterodactyl\Models\Schedule; use Pterodactyl\Transformers\Api\Client\ScheduleTransformer; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class ScheduleController extends ClientApiController { @@ -25,4 +27,25 @@ class ScheduleController extends ClientApiController ->transformWith($this->getTransformer(ScheduleTransformer::class)) ->toArray(); } + + /** + * Returns a specific schedule for the server. + * + * @param \Illuminate\Http\Request $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Schedule $schedule + * @return array + */ + public function view(Request $request, Server $server, Schedule $schedule) + { + if ($schedule->server_id !== $server->id) { + throw new NotFoundHttpException; + } + + $schedule->loadMissing('tasks'); + + return $this->fractal->item($schedule) + ->transformWith($this->getTransformer(ScheduleTransformer::class)) + ->toArray(); + } } diff --git a/resources/scripts/api/server/schedules/getServerSchedule.ts b/resources/scripts/api/server/schedules/getServerSchedule.ts index e69de29bb..537124bd6 100644 --- a/resources/scripts/api/server/schedules/getServerSchedule.ts +++ b/resources/scripts/api/server/schedules/getServerSchedule.ts @@ -0,0 +1,14 @@ +import http from '@/api/http'; +import { rawDataToServerSchedule, Schedule } from '@/api/server/schedules/getServerSchedules'; + +export default (uuid: string, schedule: number): Promise => { + return new Promise((resolve, reject) => { + http.get(`/api/client/servers/${uuid}/schedules/${schedule}`, { + params: { + include: ['tasks'], + }, + }) + .then(({ data }) => resolve(rawDataToServerSchedule(data.attributes))) + .catch(reject); + }); +}; diff --git a/resources/scripts/components/server/schedules/ScheduleContainer.tsx b/resources/scripts/components/server/schedules/ScheduleContainer.tsx index e7495abe9..48332554c 100644 --- a/resources/scripts/components/server/schedules/ScheduleContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleContainer.tsx @@ -2,21 +2,15 @@ import React, { useMemo, useState } from 'react'; import getServerSchedules, { Schedule } from '@/api/server/schedules/getServerSchedules'; import { ServerContext } from '@/state/server'; import Spinner from '@/components/elements/Spinner'; -import { RouteComponentProps } from 'react-router-dom'; +import { RouteComponentProps, Link } from 'react-router-dom'; import FlashMessageRender from '@/components/FlashMessageRender'; import ScheduleRow from '@/components/server/schedules/ScheduleRow'; import { httpErrorToHuman } from '@/api/http'; import { Actions, useStoreActions } from 'easy-peasy'; import { ApplicationStore } from '@/state'; -import EditScheduleModal from '@/components/server/schedules/EditScheduleModal'; -interface Params { - schedule?: string; -} - -export default ({ history, match }: RouteComponentProps) => { - const { id, uuid } = ServerContext.useStoreState(state => state.server.data!); - const [ active, setActive ] = useState(0); +export default ({ match, history }: RouteComponentProps) => { + const { uuid } = ServerContext.useStoreState(state => state.server.data!); const [ schedules, setSchedules ] = useState(null); const { clearFlashes, addError } = useStoreActions((actions: Actions) => actions.flashes); @@ -30,10 +24,6 @@ export default ({ history, match }: RouteComponentProps) => { }); }, [ setSchedules ]); - const matched = useMemo(() => { - return schedules?.find(schedule => schedule.id === active); - }, [ active ]); - return (
@@ -41,23 +31,19 @@ export default ({ history, match }: RouteComponentProps) => { : schedules.map(schedule => ( -
setActive(schedule.id)} + href={`${match.url}/${schedule.id}`} className={'grey-row-box cursor-pointer'} + onClick={e => { + e.preventDefault(); + history.push(`${match.url}/${schedule.id}`, { schedule }); + }} > -
+ )) } - {matched && - setActive(0)} - /> - }
); }; diff --git a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx new file mode 100644 index 000000000..77a38f027 --- /dev/null +++ b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx @@ -0,0 +1,102 @@ +import React, { useEffect, useState } from 'react'; +import { RouteComponentProps } from 'react-router-dom'; +import { Schedule } from '@/api/server/schedules/getServerSchedules'; +import getServerSchedule from '@/api/server/schedules/getServerSchedule'; +import { ServerContext } from '@/state/server'; +import Spinner from '@/components/elements/Spinner'; +import FlashMessageRender from '@/components/FlashMessageRender'; +import { Actions, useStoreActions } from 'easy-peasy'; +import { ApplicationStore } from '@/state'; +import { httpErrorToHuman } from '@/api/http'; +import ScheduleRow from '@/components/server/schedules/ScheduleRow'; +import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow'; +import EditScheduleModal from '@/components/server/schedules/EditScheduleModal'; + +interface Params { + id: string; +} + +interface State { + schedule?: Schedule; +} + +export default ({ match, location: { state } }: RouteComponentProps) => { + const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); + const [ isLoading, setIsLoading ] = useState(true); + const [ showEditModal, setShowEditModal ] = useState(false); + const [ schedule, setSchedule ] = useState(state?.schedule); + const { clearFlashes, addError } = useStoreActions((actions: Actions) => actions.flashes); + + useEffect(() => { + if (schedule?.id === Number(match.params.id)) { + setIsLoading(false); + return; + } + + clearFlashes('schedules'); + getServerSchedule(uuid, Number(match.params.id)) + .then(schedule => setSchedule(schedule)) + .catch(error => { + console.error(error); + addError({ message: httpErrorToHuman(error), key: 'schedules' }); + }) + .then(() => setIsLoading(false)); + }, [ schedule, match ]); + + return ( +
+ + {!schedule || isLoading ? + + : + <> +
+ +
+ setShowEditModal(false)} + /> +
+
+

Schedule Tasks

+
+ + +
+ {schedule?.tasks.length > 0 ? + <> + { + schedule.tasks + .sort((a, b) => a.sequenceId - b.sequenceId) + .map(task => ( +
+ +
+ )) + } + {schedule.tasks.length > 1 && +

+ Task delays are relative to the previous task in the listing. +

+ } + + : +

+ There are no tasks configured for this schedule. Consider adding a new one using the + button above. +

+ } + + } +
+ ); +}; diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx new file mode 100644 index 000000000..b7d4e5d33 --- /dev/null +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +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'; +import { faToggleOn } from '@fortawesome/free-solid-svg-icons/faToggleOn'; + +interface Props { + task: Task; +} + +export default ({ task }: Props) => { + return ( +
+ +
+

+ {task.action === 'command' ? 'Send command' : 'Send power action'} +

+ + {task.payload} + +
+ {task.sequenceId > 1 && +
+

+ {task.timeOffset}s +

+

+ Delay Run By +

+
+ } +
+ + + +
+
+ ); +}; diff --git a/resources/scripts/routers/ServerRouter.tsx b/resources/scripts/routers/ServerRouter.tsx index 7589a0d03..2998c6ee3 100644 --- a/resources/scripts/routers/ServerRouter.tsx +++ b/resources/scripts/routers/ServerRouter.tsx @@ -13,6 +13,7 @@ import SuspenseSpinner from '@/components/elements/SuspenseSpinner'; import FileEditContainer from '@/components/server/files/FileEditContainer'; import SettingsContainer from '@/components/server/settings/SettingsContainer'; import ScheduleContainer from '@/components/server/schedules/ScheduleContainer'; +import ScheduleEditContainer from '@/components/server/schedules/ScheduleEditContainer'; const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const server = ServerContext.useStoreState(state => state.server.data); @@ -63,6 +64,7 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) {/* */} + diff --git a/routes/api-client.php b/routes/api-client.php index bdb3c609f..54937a876 100644 --- a/routes/api-client.php +++ b/routes/api-client.php @@ -61,6 +61,7 @@ Route::group(['prefix' => '/servers/{server}', 'middleware' => [AuthenticateServ Route::group(['prefix' => '/schedules'], function () { Route::get('/', 'Servers\ScheduleController@index'); + Route::get('/{schedule}', 'Servers\ScheduleController@view'); }); Route::group(['prefix' => '/network'], function () { diff --git a/tailwind.js b/tailwind.js index b98b2c785..e83e6d675 100644 --- a/tailwind.js +++ b/tailwind.js @@ -837,6 +837,23 @@ module.exports = { 'current': 'currentColor', }, + transitionDuration: { + '75': '75ms', + '100': '100ms', + '150': '150ms', + '250': '250ms', + '500': '500ms', + '750': '750ms', + '1000': '1000ms', + }, + + transitionTimingFunction: { + 'linear': 'linear', + 'in': 'cubic-bezier(0.4, 0, 1, 1)', + 'out': 'cubic-bezier(0, 0, 0.2, 1)', + 'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)', + }, + /* |----------------------------------------------------------------------------- | Modules https://tailwindcss.com/docs/configuration#modules @@ -925,6 +942,52 @@ module.exports = { require('tailwindcss/plugins/container')({ center: true, }), + + function ({ addUtilities }) { + addUtilities({ + '.transition-none': { + 'transition-property': 'none', + }, + '.transition-all': { + 'transition-property': 'all', + }, + '.transition': { + 'transition-property': 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform', + }, + '.transition-colors': { + 'transition-property': 'background-color, border-color, color, fill, stroke', + }, + '.transition-opacity': { + 'transition-property': 'opacity', + }, + '.transition-shadow': { + 'transition-property': 'box-shadow', + }, + '.transition-transform': { + 'transition-property': 'transform', + }, + }, ['hover', 'focus']); + }, + + function ({ addUtilities, config }) { + const durations = config('transitionDuration', {}); + + addUtilities(Object.keys(durations).map(key => ({ + [`.duration-${key}`]: { + 'transition-duration': durations[key], + }, + })), ['hover', 'focus']); + }, + + function ({ addUtilities, config }) { + const timingFunctions = config('transitionTimingFunction', {}); + + addUtilities(Object.keys(timingFunctions).map(key => ({ + [`.ease-${key}`]: { + 'transition-timing-function': timingFunctions[key], + }, + }))); + }, ], /*