diff --git a/app/Contracts/Repository/ScheduleRepositoryInterface.php b/app/Contracts/Repository/ScheduleRepositoryInterface.php index 4f340601c..32650bdcf 100644 --- a/app/Contracts/Repository/ScheduleRepositoryInterface.php +++ b/app/Contracts/Repository/ScheduleRepositoryInterface.php @@ -15,16 +15,6 @@ interface ScheduleRepositoryInterface extends RepositoryInterface */ public function findServerSchedules(int $server): Collection; - /** - * Load the tasks relationship onto the Schedule module if they are not - * already present. - * - * @param \Pterodactyl\Models\Schedule $schedule - * @param bool $refresh - * @return \Pterodactyl\Models\Schedule - */ - public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule; - /** * Return a schedule model with all of the associated tasks as a relationship. * diff --git a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php index d35b597ed..a9310abd6 100644 --- a/app/Http/Controllers/Api/Client/Servers/ScheduleController.php +++ b/app/Http/Controllers/Api/Client/Servers/ScheduleController.php @@ -10,15 +10,19 @@ use Pterodactyl\Models\Server; use Pterodactyl\Models\Schedule; use Illuminate\Http\JsonResponse; use Pterodactyl\Helpers\Utilities; +use Pterodactyl\Jobs\Schedule\RunTaskJob; use Pterodactyl\Exceptions\DisplayException; use Pterodactyl\Repositories\Eloquent\ScheduleRepository; +use Pterodactyl\Services\Schedules\ProcessScheduleService; use Pterodactyl\Transformers\Api\Client\ScheduleTransformer; use Pterodactyl\Http\Controllers\Api\Client\ClientApiController; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\ViewScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\StoreScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\DeleteScheduleRequest; use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\UpdateScheduleRequest; +use Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest; class ScheduleController extends ClientApiController { @@ -27,16 +31,23 @@ class ScheduleController extends ClientApiController */ private $repository; + /** + * @var \Pterodactyl\Services\Schedules\ProcessScheduleService + */ + private $service; + /** * ScheduleController constructor. * * @param \Pterodactyl\Repositories\Eloquent\ScheduleRepository $repository + * @param \Pterodactyl\Services\Schedules\ProcessScheduleService $service */ - public function __construct(ScheduleRepository $repository) + public function __construct(ScheduleRepository $repository, ProcessScheduleService $service) { parent::__construct(); $this->repository = $repository; + $this->service = $service; } /** @@ -147,6 +158,30 @@ class ScheduleController extends ClientApiController ->toArray(); } + /** + * Executes a given schedule immediately rather than waiting on it's normally scheduled time + * to pass. This does not care about the schedule state. + * + * @param \Pterodactyl\Http\Requests\Api\Client\Servers\Schedules\TriggerScheduleRequest $request + * @param \Pterodactyl\Models\Server $server + * @param \Pterodactyl\Models\Schedule $schedule + * @return \Illuminate\Http\JsonResponse + * + * @throws \Throwable + */ + public function execute(TriggerScheduleRequest $request, Server $server, Schedule $schedule) + { + if (!$schedule->is_active) { + throw new BadRequestHttpException( + 'Cannot trigger schedule exection for a schedule that is not currently active.' + ); + } + + $this->service->handle($schedule, true); + + return new JsonResponse([], JsonResponse::HTTP_ACCEPTED); + } + /** * Deletes a schedule and it's associated tasks. * diff --git a/app/Http/Requests/Api/Client/Servers/Schedules/TriggerScheduleRequest.php b/app/Http/Requests/Api/Client/Servers/Schedules/TriggerScheduleRequest.php new file mode 100644 index 000000000..d89f5ed30 --- /dev/null +++ b/app/Http/Requests/Api/Client/Servers/Schedules/TriggerScheduleRequest.php @@ -0,0 +1,25 @@ +task->schedule->is_active) { diff --git a/app/Models/Schedule.php b/app/Models/Schedule.php index d737edd2c..de0475639 100644 --- a/app/Models/Schedule.php +++ b/app/Models/Schedule.php @@ -2,6 +2,8 @@ namespace Pterodactyl\Models; +use Cron\CronExpression; +use Carbon\CarbonImmutable; use Illuminate\Container\Container; use Pterodactyl\Contracts\Extensions\HashidsInterface; @@ -114,6 +116,20 @@ class Schedule extends Model 'next_run_at' => 'nullable|date', ]; + /** + * Returns the schedule's execution crontab entry as a string. + * + * @return \Carbon\CarbonImmutable + */ + public function getNextRunDate() + { + $formatted = sprintf('%s %s %s * %s', $this->cron_minute, $this->cron_hour, $this->cron_day_of_month, $this->cron_day_of_week); + + return CarbonImmutable::createFromTimestamp( + CronExpression::factory($formatted)->getNextRunDate()->getTimestamp() + ); + } + /** * Return a hashid encoded string to represent the ID of the schedule. * diff --git a/app/Repositories/Eloquent/ScheduleRepository.php b/app/Repositories/Eloquent/ScheduleRepository.php index 389d06720..030939da7 100644 --- a/app/Repositories/Eloquent/ScheduleRepository.php +++ b/app/Repositories/Eloquent/ScheduleRepository.php @@ -31,23 +31,6 @@ class ScheduleRepository extends EloquentRepository implements ScheduleRepositor return $this->getBuilder()->withCount('tasks')->where('server_id', '=', $server)->get($this->getColumns()); } - /** - * Load the tasks relationship onto the Schedule module if they are not - * already present. - * - * @param \Pterodactyl\Models\Schedule $schedule - * @param bool $refresh - * @return \Pterodactyl\Models\Schedule - */ - public function loadTasks(Schedule $schedule, bool $refresh = false): Schedule - { - if (! $schedule->relationLoaded('tasks') || $refresh) { - $schedule->load('tasks'); - } - - return $schedule; - } - /** * Return a schedule model with all of the associated tasks as a relationship. * diff --git a/app/Services/Schedules/ProcessScheduleService.php b/app/Services/Schedules/ProcessScheduleService.php index 3fa3604a4..1f810d6f5 100644 --- a/app/Services/Schedules/ProcessScheduleService.php +++ b/app/Services/Schedules/ProcessScheduleService.php @@ -2,12 +2,11 @@ namespace Pterodactyl\Services\Schedules; -use Cron\CronExpression; use Pterodactyl\Models\Schedule; use Illuminate\Contracts\Bus\Dispatcher; use Pterodactyl\Jobs\Schedule\RunTaskJob; -use Pterodactyl\Contracts\Repository\TaskRepositoryInterface; -use Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface; +use Illuminate\Database\ConnectionInterface; +use Pterodactyl\Exceptions\DisplayException; class ProcessScheduleService { @@ -17,62 +16,51 @@ class ProcessScheduleService private $dispatcher; /** - * @var \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface + * @var \Illuminate\Database\ConnectionInterface */ - private $scheduleRepository; - - /** - * @var \Pterodactyl\Contracts\Repository\TaskRepositoryInterface - */ - private $taskRepository; + private $connection; /** * ProcessScheduleService constructor. * + * @param \Illuminate\Database\ConnectionInterface $connection * @param \Illuminate\Contracts\Bus\Dispatcher $dispatcher - * @param \Pterodactyl\Contracts\Repository\ScheduleRepositoryInterface $scheduleRepository - * @param \Pterodactyl\Contracts\Repository\TaskRepositoryInterface $taskRepository */ - public function __construct( - Dispatcher $dispatcher, - ScheduleRepositoryInterface $scheduleRepository, - TaskRepositoryInterface $taskRepository - ) { + public function __construct(ConnectionInterface $connection, Dispatcher $dispatcher) + { $this->dispatcher = $dispatcher; - $this->scheduleRepository = $scheduleRepository; - $this->taskRepository = $taskRepository; + $this->connection = $connection; } /** * Process a schedule and push the first task onto the queue worker. * * @param \Pterodactyl\Models\Schedule $schedule + * @param bool $now * - * @throws \Pterodactyl\Exceptions\Model\DataValidationException - * @throws \Pterodactyl\Exceptions\Repository\RecordNotFoundException + * @throws \Throwable */ - public function handle(Schedule $schedule) + public function handle(Schedule $schedule, bool $now = false) { - $this->scheduleRepository->loadTasks($schedule); - /** @var \Pterodactyl\Models\Task $task */ - $task = $schedule->getRelation('tasks')->where('sequence_id', 1)->first(); + $task = $schedule->tasks()->where('sequence_id', 1)->first(); - $formattedCron = sprintf('%s %s %s * %s', - $schedule->cron_minute, - $schedule->cron_hour, - $schedule->cron_day_of_month, - $schedule->cron_day_of_week - ); + if (is_null($task)) { + throw new DisplayException( + 'Cannot process schedule for task execution: no tasks are registered.' + ); + } - $this->scheduleRepository->update($schedule->id, [ - 'is_processing' => true, - 'next_run_at' => CronExpression::factory($formattedCron)->getNextRunDate(), - ]); + $this->connection->transaction(function () use ($schedule, $task) { + $schedule->forceFill([ + 'is_processing' => true, + 'next_run_at' => $schedule->getNextRunDate(), + ])->saveOrFail(); - $this->taskRepository->update($task->id, ['is_queued' => true]); + $task->update(['is_queued' => true]); + }); - $this->dispatcher->dispatch( + $this->dispatcher->{$now ? 'dispatchNow' : 'dispatch'}( (new RunTaskJob($task))->delay($task->time_offset) ); } diff --git a/package.json b/package.json index e0603db4c..8d2aec795 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "pterodactyl-panel", "dependencies": { - "@fortawesome/fontawesome-svg-core": "1.2.19", - "@fortawesome/free-solid-svg-icons": "^5.9.0", - "@fortawesome/react-fontawesome": "0.1.4", + "@fortawesome/fontawesome-svg-core": "^1.2.32", + "@fortawesome/free-solid-svg-icons": "^5.15.1", + "@fortawesome/react-fontawesome": "^0.1.11", "axios": "^0.19.2", "chart.js": "^2.8.0", "codemirror": "^5.57.0", @@ -23,9 +23,9 @@ "react": "^16.13.1", "react-dom": "npm:@hot-loader/react-dom", "react-fast-compare": "^3.2.0", + "react-ga": "^3.1.2", "react-google-recaptcha": "^2.0.1", "react-helmet": "^6.1.0", - "react-ga": "^3.1.2", "react-hot-loader": "^4.12.21", "react-i18next": "^11.2.1", "react-redux": "^7.1.0", diff --git a/resources/scripts/api/server/schedules/getServerSchedules.tsx b/resources/scripts/api/server/schedules/getServerSchedules.ts similarity index 100% rename from resources/scripts/api/server/schedules/getServerSchedules.tsx rename to resources/scripts/api/server/schedules/getServerSchedules.ts diff --git a/resources/scripts/api/server/schedules/triggerScheduleExecution.ts b/resources/scripts/api/server/schedules/triggerScheduleExecution.ts new file mode 100644 index 000000000..92f7a589f --- /dev/null +++ b/resources/scripts/api/server/schedules/triggerScheduleExecution.ts @@ -0,0 +1,4 @@ +import http from '@/api/http'; + +export default async (server: string, schedule: number): Promise => + await http.post(`/api/client/servers/${server}/schedules/${schedule}/execute`); diff --git a/resources/scripts/components/elements/Button.tsx b/resources/scripts/components/elements/Button.tsx index 300f1a9ea..8577fad7f 100644 --- a/resources/scripts/components/elements/Button.tsx +++ b/resources/scripts/components/elements/Button.tsx @@ -57,8 +57,8 @@ const ButtonStyle = styled.button>` `}; `}; - ${props => props.size === 'xsmall' && tw`p-2 text-xs`}; - ${props => (!props.size || props.size === 'small') && tw`p-3`}; + ${props => props.size === 'xsmall' && tw`px-2 py-1 text-xs`}; + ${props => (!props.size || props.size === 'small') && tw`px-4 py-2`}; ${props => props.size === 'large' && tw`p-4 text-sm`}; ${props => props.size === 'xlarge' && tw`p-4 w-full`}; diff --git a/resources/scripts/components/elements/Icon.tsx b/resources/scripts/components/elements/Icon.tsx new file mode 100644 index 000000000..a7d837896 --- /dev/null +++ b/resources/scripts/components/elements/Icon.tsx @@ -0,0 +1,31 @@ +import React, { CSSProperties } from 'react'; +import { IconDefinition } from '@fortawesome/fontawesome-svg-core'; +import tw from 'twin.macro'; + +interface Props { + icon: IconDefinition; + className?: string; + style?: CSSProperties; +} + +const Icon = ({ icon, className, style }: Props) => { + let [ width, height, , , paths ] = icon.icon; + + paths = Array.isArray(paths) ? paths : [ paths ]; + + return ( + + {paths.map((path, index) => ( + + ))} + + ); +}; + +export default Icon; diff --git a/resources/scripts/components/elements/Modal.tsx b/resources/scripts/components/elements/Modal.tsx index 68d6493de..ae618a42d 100644 --- a/resources/scripts/components/elements/Modal.tsx +++ b/resources/scripts/components/elements/Modal.tsx @@ -1,9 +1,10 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useRef, useState } from 'react'; import Spinner from '@/components/elements/Spinner'; import tw from 'twin.macro'; import styled, { css } from 'styled-components/macro'; import { breakpoint } from '@/theme'; import Fade from '@/components/elements/Fade'; +import { createPortal } from 'react-dom'; export interface RequiredModalProps { visible: boolean; @@ -124,4 +125,10 @@ const Modal: React.FC = ({ visible, appear, dismissable, showSpinner ); }; -export default Modal; +const PortaledModal: React.FC = ({ children, ...props }) => { + const element = useRef(document.getElementById('modal-portal')); + + return createPortal({children}, element.current!); +}; + +export default PortaledModal; diff --git a/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx b/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx index 198060388..463202dce 100644 --- a/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx +++ b/resources/scripts/components/server/schedules/DeleteScheduleButton.tsx @@ -49,7 +49,7 @@ export default ({ scheduleId, onDeleted }: Props) => { Are you sure you want to delete this schedule? All tasks will be removed and any running processes will be terminated. - diff --git a/resources/scripts/components/server/schedules/NewTaskButton.tsx b/resources/scripts/components/server/schedules/NewTaskButton.tsx index b46124e64..9234f5b42 100644 --- a/resources/scripts/components/server/schedules/NewTaskButton.tsx +++ b/resources/scripts/components/server/schedules/NewTaskButton.tsx @@ -2,6 +2,7 @@ import React, { useState } from 'react'; import { Schedule } from '@/api/server/schedules/getServerSchedules'; import TaskDetailsModal from '@/components/server/schedules/TaskDetailsModal'; import Button from '@/components/elements/Button'; +import tw from 'twin.macro'; interface Props { schedule: Schedule; @@ -18,7 +19,7 @@ export default ({ schedule }: Props) => { onDismissed={() => setVisible(false)} /> } - diff --git a/resources/scripts/components/server/schedules/RunScheduleButton.tsx b/resources/scripts/components/server/schedules/RunScheduleButton.tsx new file mode 100644 index 000000000..96424b06e --- /dev/null +++ b/resources/scripts/components/server/schedules/RunScheduleButton.tsx @@ -0,0 +1,48 @@ +import React, { useCallback, useState } from 'react'; +import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; +import tw from 'twin.macro'; +import Button from '@/components/elements/Button'; +import triggerScheduleExecution from '@/api/server/schedules/triggerScheduleExecution'; +import { ServerContext } from '@/state/server'; +import useFlash from '@/plugins/useFlash'; +import { Schedule } from '@/api/server/schedules/getServerSchedules'; + +const RunScheduleButton = ({ schedule }: { schedule: Schedule }) => { + const [ loading, setLoading ] = useState(false); + const { clearFlashes, clearAndAddHttpError } = useFlash(); + + const id = ServerContext.useStoreState(state => state.server.data!.id); + const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule); + + const onTriggerExecute = useCallback(() => { + clearFlashes('schedule'); + setLoading(true); + triggerScheduleExecution(id, schedule.id) + .then(() => { + setLoading(false); + appendSchedule({ ...schedule, isProcessing: true }); + }) + .catch(error => { + console.error(error); + clearAndAddHttpError({ error, key: 'schedules' }); + }) + .then(() => setLoading(false)); + }, []); + + return ( + <> + + + + ); +}; + +export default RunScheduleButton; diff --git a/resources/scripts/components/server/schedules/ScheduleCronRow.tsx b/resources/scripts/components/server/schedules/ScheduleCronRow.tsx new file mode 100644 index 000000000..e7918a132 --- /dev/null +++ b/resources/scripts/components/server/schedules/ScheduleCronRow.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import tw from 'twin.macro'; +import { Schedule } from '@/api/server/schedules/getServerSchedules'; + +interface Props { + cron: Schedule['cron']; + className?: string; +} + +const ScheduleCronRow = ({ cron, className }: Props) => ( +
+
+

{cron.minute}

+

Minute

+
+
+

{cron.hour}

+

Hour

+
+
+

{cron.dayOfMonth}

+

Day (Month)

+
+
+

*

+

Month

+
+
+

{cron.dayOfWeek}

+

Day (Week)

+
+
+); + +export default ScheduleCronRow; diff --git a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx index 8f9f07fce..d7c5f2abf 100644 --- a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx @@ -1,12 +1,9 @@ -import React, { useEffect, useState } from 'react'; +import React, { useCallback, 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 Spinner from '@/components/elements/Spinner'; import FlashMessageRender from '@/components/FlashMessageRender'; -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'; import NewTaskButton from '@/components/server/schedules/NewTaskButton'; import DeleteScheduleButton from '@/components/server/schedules/DeleteScheduleButton'; @@ -16,7 +13,11 @@ import { ServerContext } from '@/state/server'; import PageContentBlock from '@/components/elements/PageContentBlock'; import tw from 'twin.macro'; import Button from '@/components/elements/Button'; -import GreyRowBox from '@/components/elements/GreyRowBox'; +import ScheduleTaskRow from '@/components/server/schedules/ScheduleTaskRow'; +import isEqual from 'react-fast-compare'; +import { format } from 'date-fns'; +import ScheduleCronRow from '@/components/server/schedules/ScheduleCronRow'; +import RunScheduleButton from '@/components/server/schedules/RunScheduleButton'; interface Params { id: string; @@ -26,15 +27,34 @@ interface State { schedule?: Schedule; } +const CronBox = ({ title, value }: { title: string; value: string }) => ( +
+

{title}

+

{value}

+
+); + +const ActivePill = ({ active }: { active: boolean }) => ( + + {active ? 'Active' : 'Inactive'} + +); + export default ({ match, history, location: { state } }: RouteComponentProps, State>) => { const id = ServerContext.useStoreState(state => state.server.data!.id); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); - const { clearFlashes, addError } = useFlash(); + const { clearFlashes, clearAndAddHttpError } = useFlash(); const [ isLoading, setIsLoading ] = useState(true); const [ showEditModal, setShowEditModal ] = useState(false); - const schedule = ServerContext.useStoreState(st => st.schedules.data.find(s => s.id === state.schedule?.id), [ match ]); + // @ts-ignore + const schedule: Schedule | undefined = ServerContext.useStoreState(st => st.schedules.data.find(s => s.id === state.schedule?.id), isEqual); const appendSchedule = ServerContext.useStoreActions(actions => actions.schedules.appendSchedule); useEffect(() => { @@ -48,11 +68,15 @@ export default ({ match, history, location: { state } }: RouteComponentProps appendSchedule(schedule)) .catch(error => { console.error(error); - addError({ message: httpErrorToHuman(error), key: 'schedules' }); + clearAndAddHttpError({ error, key: 'schedules' }); }) .then(() => setIsLoading(false)); }, [ match ]); + const toggleEditModal = useCallback(() => { + setShowEditModal(s => !s); + }, []); + return ( @@ -60,52 +84,73 @@ export default ({ match, history, location: { state } }: RouteComponentProps : <> - - - - setShowEditModal(false)} - /> -
-
-

Configured Tasks

+ +
+ + + + + +
+
+
+
+

+ {schedule.name} + {schedule.isProcessing ? + + + Processing + + : + + } +

+

+ Last run at:  + {schedule.lastRunAt ? format(schedule.lastRunAt, 'MMM do \'at\' h:mma') : 'never'} +

+
+
+ + + + +
+
+
+ {schedule.tasks.length > 0 ? + schedule.tasks.map(task => ( + + )) + : + null + }
- {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. -

- } -
+ +
history.push(`/server/${id}/schedules`)} /> + {schedule.isActive && schedule.tasks.length > 0 && - - + + }
} diff --git a/resources/scripts/components/server/schedules/ScheduleRow.tsx b/resources/scripts/components/server/schedules/ScheduleRow.tsx index eb9ff68a5..eccdd0f96 100644 --- a/resources/scripts/components/server/schedules/ScheduleRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleRow.tsx @@ -4,6 +4,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faCalendarAlt } from '@fortawesome/free-solid-svg-icons'; import { format } from 'date-fns'; import tw from 'twin.macro'; +import ScheduleCronRow from '@/components/server/schedules/ScheduleCronRow'; export default ({ schedule }: { schedule: Schedule }) => ( <> @@ -27,36 +28,19 @@ export default ({ schedule }: { schedule: Schedule }) => ( {schedule.isActive ? 'Active' : 'Inactive'}

-
-
-

{schedule.cron.minute}

-

Minute

-
-
-

{schedule.cron.hour}

-

Hour

-
-
-

{schedule.cron.dayOfMonth}

-

Day (Month)

-
-
-

*

-

Month

-
-
-

{schedule.cron.dayOfWeek}

-

Day (Week)

-
-
+
diff --git a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx index c79fafd03..1ee55af2c 100644 --- a/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx +++ b/resources/scripts/components/server/schedules/ScheduleTaskRow.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Schedule, Task } from '@/api/server/schedules/getServerSchedules'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faCode, faFileArchive, faPencilAlt, faToggleOn, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; +import { faClock, faCode, faFileArchive, faPencilAlt, faToggleOn, faTrashAlt } from '@fortawesome/free-solid-svg-icons'; import deleteScheduleTask from '@/api/server/schedules/deleteScheduleTask'; import { httpErrorToHuman } from '@/api/http'; import SpinnerOverlay from '@/components/elements/SpinnerOverlay'; @@ -11,6 +11,7 @@ import useFlash from '@/plugins/useFlash'; import { ServerContext } from '@/state/server'; import tw from 'twin.macro'; import ConfirmationModal from '@/components/elements/ConfirmationModal'; +import Icon from '@/components/elements/Icon'; interface Props { schedule: Schedule; @@ -56,7 +57,7 @@ export default ({ schedule, task }: Props) => { const [ title, icon ] = getActionDetails(task.action); return ( -
+
{isEditing && { Are you sure you want to delete this task? This action cannot be undone.