From f33d0b1d724b327ab3e513feb76f44b5b0d529c3 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Wed, 14 Oct 2020 20:13:36 -0700 Subject: [PATCH] Update schedule view UI --- package.json | 8 +- .../scripts/components/elements/Button.tsx | 4 +- .../scripts/components/elements/Icon.tsx | 31 +++++ .../scripts/components/elements/Modal.tsx | 11 +- .../server/schedules/DeleteScheduleButton.tsx | 2 +- .../server/schedules/NewTaskButton.tsx | 3 +- .../server/schedules/ScheduleCronRow.tsx | 35 +++++ .../schedules/ScheduleEditContainer.tsx | 126 ++++++++++++------ .../server/schedules/ScheduleRow.tsx | 32 ++--- .../server/schedules/ScheduleTaskRow.tsx | 67 +++++----- resources/views/templates/base/core.blade.php | 1 + yarn.lock | 44 +++--- 12 files changed, 230 insertions(+), 134 deletions(-) create mode 100644 resources/scripts/components/elements/Icon.tsx create mode 100644 resources/scripts/components/server/schedules/ScheduleCronRow.tsx diff --git a/package.json b/package.json index 15e96c4bd..251623fc1 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/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/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..aea4778ae 100644 --- a/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx +++ b/resources/scripts/components/server/schedules/ScheduleEditContainer.tsx @@ -1,12 +1,10 @@ -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 +14,10 @@ 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'; interface Params { id: string; @@ -26,6 +27,24 @@ 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); @@ -34,7 +53,8 @@ export default ({ match, history, location: { state } }: RouteComponentProps 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(() => { @@ -53,6 +73,10 @@ export default ({ match, history, location: { state } }: RouteComponentProps setIsLoading(false)); }, [ match ]); + const toggleEditModal = useCallback(() => { + setShowEditModal(s => !s); + }, []); + return ( @@ -60,52 +84,68 @@ 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`)} /> - - - -
} 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.