From faff263f17238d4967f723cebba072e068abece6 Mon Sep 17 00:00:00 2001 From: DaneEveritt Date: Mon, 20 Jun 2022 17:26:47 -0400 Subject: [PATCH] First pass at new server console design --- .../elements/button/style.module.css | 2 +- .../elements/dialog/ConfirmationDialog.tsx | 2 +- .../components/elements/tooltip/Tooltip.tsx | 2 +- .../components/server/PowerControls.tsx | 55 ----- .../components/server/ServerConsole.tsx | 31 ++- .../components/server/ServerDetailsBlock.tsx | 210 ++++++++++-------- .../components/server/StopOrKillButton.tsx | 31 --- .../server/console/PowerButtons.tsx | 73 ++++++ .../components/server/console/StatBlock.tsx | 46 ++++ 9 files changed, 263 insertions(+), 189 deletions(-) delete mode 100644 resources/scripts/components/server/PowerControls.tsx delete mode 100644 resources/scripts/components/server/StopOrKillButton.tsx create mode 100644 resources/scripts/components/server/console/PowerButtons.tsx create mode 100644 resources/scripts/components/server/console/StatBlock.tsx diff --git a/resources/scripts/components/elements/button/style.module.css b/resources/scripts/components/elements/button/style.module.css index 27f7fd61d..700c67676 100644 --- a/resources/scripts/components/elements/button/style.module.css +++ b/resources/scripts/components/elements/button/style.module.css @@ -16,7 +16,7 @@ @apply text-gray-50 bg-transparent; &:disabled { - @apply bg-transparent; + background: transparent !important; } } diff --git a/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx b/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx index ad0885a28..010f738da 100644 --- a/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx +++ b/resources/scripts/components/elements/dialog/ConfirmationDialog.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/elements/button/index'; type ConfirmationProps = Omit & { children: React.ReactNode; confirm?: string | undefined; - onConfirmed: () => void; + onConfirmed: (e: React.MouseEvent) => void; } export default ({ confirm = 'Okay', children, onConfirmed, ...props }: ConfirmationProps) => { diff --git a/resources/scripts/components/elements/tooltip/Tooltip.tsx b/resources/scripts/components/elements/tooltip/Tooltip.tsx index 476c7db49..b73f0bb63 100644 --- a/resources/scripts/components/elements/tooltip/Tooltip.tsx +++ b/resources/scripts/components/elements/tooltip/Tooltip.tsx @@ -90,7 +90,7 @@ export default ({ transition={{ type: 'spring', damping: 20, stiffness: 300, duration: 0.075 }} {...getFloatingProps({ ref: floating, - className: 'absolute top-0 left-0 bg-gray-900 text-sm text-gray-200 px-3 py-2 rounded pointer-events-none max-w-[90vw]', + className: 'absolute top-0 left-0 bg-gray-900 text-sm text-gray-200 px-3 py-2 rounded pointer-events-none max-w-[20rem] z-[9999]', style: { position: strategy, top: `${y || 0}px`, diff --git a/resources/scripts/components/server/PowerControls.tsx b/resources/scripts/components/server/PowerControls.tsx deleted file mode 100644 index e88ffe3f0..000000000 --- a/resources/scripts/components/server/PowerControls.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import tw from 'twin.macro'; -import Can from '@/components/elements/Can'; -import Button from '@/components/elements/Button'; -import StopOrKillButton from '@/components/server/StopOrKillButton'; -import { PowerAction } from '@/components/server/ServerConsole'; -import { ServerContext } from '@/state/server'; - -const PowerControls = () => { - const status = ServerContext.useStoreState(state => state.status.value); - const instance = ServerContext.useStoreState(state => state.socket.instance); - - const sendPowerCommand = (command: PowerAction) => { - instance && instance.send('set state', command); - }; - - return ( -
- - - - - - - - sendPowerCommand(action)}/> - -
- ); -}; - -export default PowerControls; diff --git a/resources/scripts/components/server/ServerConsole.tsx b/resources/scripts/components/server/ServerConsole.tsx index da87da775..296513772 100644 --- a/resources/scripts/components/server/ServerConsole.tsx +++ b/resources/scripts/components/server/ServerConsole.tsx @@ -4,26 +4,39 @@ import Can from '@/components/elements/Can'; import ContentContainer from '@/components/elements/ContentContainer'; import tw from 'twin.macro'; import ServerContentBlock from '@/components/elements/ServerContentBlock'; -import ServerDetailsBlock from '@/components/server/ServerDetailsBlock'; import isEqual from 'react-fast-compare'; -import PowerControls from '@/components/server/PowerControls'; import ErrorBoundary from '@/components/elements/ErrorBoundary'; import Spinner from '@/components/elements/Spinner'; import Features from '@feature/Features'; import Console from '@/components/server/Console'; import StatGraphs from '@/components/server/StatGraphs'; +import PowerButtons from '@/components/server/console/PowerButtons'; +import ServerDetailsBlock from '@/components/server/ServerDetailsBlock'; export type PowerAction = 'start' | 'stop' | 'restart' | 'kill'; const ServerConsole = () => { + const name = ServerContext.useStoreState(state => state.server.data!.name); + const description = ServerContext.useStoreState(state => state.server.data!.description); const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling); const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring); const eggFeatures = ServerContext.useStoreState(state => state.server.data!.eggFeatures, isEqual); return ( - -
- + +
+
+

{name}

+

{description}

+
+
+ + + +
+
+
+ {isInstalling ?
@@ -44,19 +57,17 @@ const ServerConsole = () => {
: - - - + null }
-
+
- +
); diff --git a/resources/scripts/components/server/ServerDetailsBlock.tsx b/resources/scripts/components/server/ServerDetailsBlock.tsx index 3eb5f01e9..e07dbad86 100644 --- a/resources/scripts/components/server/ServerDetailsBlock.tsx +++ b/resources/scripts/components/server/ServerDetailsBlock.tsx @@ -1,48 +1,61 @@ import React, { useEffect, useState } from 'react'; -import tw, { TwStyle } from 'twin.macro'; import { - faArrowCircleDown, - faArrowCircleUp, - faCircle, - faEthernet, + faClock, + faCloudDownloadAlt, + faCloudUploadAlt, faHdd, faMemory, faMicrochip, - faServer, + faWifi, } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { bytesToHuman, formatIp, megabytesToHuman } from '@/helpers'; -import TitledGreyBox from '@/components/elements/TitledGreyBox'; import { ServerContext } from '@/state/server'; -import CopyOnClick from '@/components/elements/CopyOnClick'; import { SocketEvent, SocketRequest } from '@/components/server/events'; import UptimeDuration from '@/components/server/UptimeDuration'; +import StatBlock from '@/components/server/console/StatBlock'; +import useWebsocketEvent from '@/plugins/useWebsocketEvent'; type Stats = Record<'memory' | 'cpu' | 'disk' | 'uptime' | 'rx' | 'tx', number>; -function statusToColor (status: string | null, installing: boolean): TwStyle { - if (installing) { - status = ''; - } - - switch (status) { - case 'offline': - return tw`text-red-500`; - case 'running': - return tw`text-green-500`; - default: - return tw`text-yellow-500`; - } +interface DetailBlockProps { + className?: string; } -const ServerDetailsBlock = () => { +const getBackgroundColor = (value: number, max: number | null): string | undefined => { + const delta = !max ? 0 : (value / max); + + if (delta > 0.8) { + if (delta > 0.9) { + return 'bg-red-500'; + } + return 'bg-yellow-500'; + } + + return undefined; +}; + +const ServerDetailsBlock = ({ className }: DetailBlockProps) => { const [ stats, setStats ] = useState({ memory: 0, cpu: 0, disk: 0, uptime: 0, tx: 0, rx: 0 }); const status = ServerContext.useStoreState(state => state.status.value); const connected = ServerContext.useStoreState(state => state.socket.connected); const instance = ServerContext.useStoreState(state => state.socket.instance); + const limits = ServerContext.useStoreState(state => state.server.data!.limits); + const allocation = ServerContext.useStoreState(state => { + const match = state.server.data!.allocations.find(allocation => allocation.isDefault); - const statsListener = (data: string) => { + return !match ? 'n/a' : `${match.alias || formatIp(match.ip)}:${match.port}`; + }); + + useEffect(() => { + if (!connected || !instance) { + return; + } + + instance.send(SocketRequest.SEND_STATS); + }, [ instance, connected ]); + + useWebsocketEvent(SocketEvent.STATS, (data) => { let stats: any = {}; try { stats = JSON.parse(data); @@ -58,75 +71,92 @@ const ServerDetailsBlock = () => { rx: stats.network.rx_bytes, uptime: stats.uptime || 0, }); - }; - - useEffect(() => { - if (!connected || !instance) { - return; - } - - instance.addListener(SocketEvent.STATS, statsListener); - instance.send(SocketRequest.SEND_STATS); - - return () => { - instance.removeListener(SocketEvent.STATS, statsListener); - }; - }, [ instance, connected ]); - - const name = ServerContext.useStoreState(state => state.server.data!.name); - const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling); - const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring); - const limits = ServerContext.useStoreState(state => state.server.data!.limits); - const primaryAllocation = ServerContext.useStoreState(state => state.server.data!.allocations.filter(alloc => alloc.isDefault).map( - allocation => (allocation.alias || formatIp(allocation.ip)) + ':' + allocation.port, - )).toString(); - - const diskLimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited'; - const memoryLimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited'; - const cpuLimit = limits.cpu ? limits.cpu + '%' : 'Unlimited'; + }); return ( - -

- -  {!status ? 'Connecting...' : (isInstalling ? 'Installing' : (isTransferring) ? 'Transferring' : status)} - {stats.uptime > 0 && - - () - +

+ + {stats.uptime > 0 ? + + : + 'Offline' } -

- -

- - {primaryAllocation} -

-
-

- {stats.cpu.toFixed(2)}% - / {cpuLimit} -

-

- {bytesToHuman(stats.memory)} - / {memoryLimit} -

-

-  {bytesToHuman(stats.disk)} - / {diskLimit} -

-

- - {bytesToHuman(stats.tx)} - {bytesToHuman(stats.rx)} -

- +
+ + {status === 'offline' ? + Offline + : + `${stats.cpu.toFixed(2)}%` + } + + + {status === 'offline' ? + Offline + : + bytesToHuman(stats.memory) + } + + + {bytesToHuman(stats.disk)} + + + {status === 'offline' ? + Offline + : + bytesToHuman(stats.tx) + } + + + {status === 'offline' ? + Offline + : + bytesToHuman(stats.rx) + } + + + {allocation} + +
); }; diff --git a/resources/scripts/components/server/StopOrKillButton.tsx b/resources/scripts/components/server/StopOrKillButton.tsx deleted file mode 100644 index 605ca397e..000000000 --- a/resources/scripts/components/server/StopOrKillButton.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React, { memo, useEffect, useState } from 'react'; -import { ServerContext } from '@/state/server'; -import { PowerAction } from '@/components/server/ServerConsole'; -import Button from '@/components/elements/Button'; -import isEqual from 'react-fast-compare'; - -const StopOrKillButton = ({ onPress }: { onPress: (action: PowerAction) => void }) => { - const [ clicked, setClicked ] = useState(false); - const status = ServerContext.useStoreState(state => state.status.value); - - useEffect(() => { - setClicked(status === 'stopping'); - }, [ status ]); - - return ( - - ); -}; - -export default memo(StopOrKillButton, isEqual); diff --git a/resources/scripts/components/server/console/PowerButtons.tsx b/resources/scripts/components/server/console/PowerButtons.tsx new file mode 100644 index 000000000..169210a57 --- /dev/null +++ b/resources/scripts/components/server/console/PowerButtons.tsx @@ -0,0 +1,73 @@ +import React, { useState } from 'react'; +import { Button } from '@/components/elements/button/index'; +import Can from '@/components/elements/Can'; +import { ServerContext } from '@/state/server'; +import { PowerAction } from '@/components/server/ServerConsole'; +import { Dialog } from '@/components/elements/dialog'; + +interface PowerButtonProps { + className?: string; +} + +export default ({ className }: PowerButtonProps) => { + const [ open, setOpen ] = useState(false); + const status = ServerContext.useStoreState(state => state.status.value); + const instance = ServerContext.useStoreState(state => state.socket.instance); + + const killable = status === 'stopping'; + const onButtonClick = (action: PowerAction | 'kill-confirmed', e: React.MouseEvent): void => { + e.preventDefault(); + if (action === 'kill') { + return setOpen(true); + } + + if (instance) { + setOpen(false); + instance.send('set state', action === 'kill-confirmed' ? 'kill' : action); + } + }; + + return ( +
+ setOpen(false)} + title={'Forcibly Stop Process'} + confirm={'Continue'} + onConfirmed={onButtonClick.bind(this, 'kill-confirmed')} + > + Forcibly stopping a server can lead to data corruption. + + + + + + + Restart + + + + + {killable ? 'Kill' : 'Stop'} + + +
+ ); +}; diff --git a/resources/scripts/components/server/console/StatBlock.tsx b/resources/scripts/components/server/console/StatBlock.tsx new file mode 100644 index 000000000..4303b69b1 --- /dev/null +++ b/resources/scripts/components/server/console/StatBlock.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import Icon from '@/components/elements/Icon'; +import { IconDefinition } from '@fortawesome/free-solid-svg-icons'; +import classNames from 'classnames'; +import Tooltip from '@/components/elements/tooltip/Tooltip'; + +interface StatBlockProps { + title: string; + description?: string; + color?: string | undefined; + icon: IconDefinition; + children: React.ReactNode; +} + +export default ({ title, icon, color, description, children }: StatBlockProps) => { + return ( + +
+
+ +
+
+

{title}

+

+ {children} +

+
+
+
+ ); +};