React 18 and Vite (#4510)

This commit is contained in:
Matthew Penner 2022-11-25 13:25:03 -07:00 committed by GitHub
parent 1bb1b13f6d
commit 21613fa602
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
244 changed files with 4547 additions and 8933 deletions

View file

@ -1,4 +1,4 @@
import React from 'react';
import * as React from 'react';
import classNames from 'classnames';
import styles from '@/components/server/console/style.module.css';

View file

@ -1,25 +1,28 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { ITerminalOptions, Terminal } from 'xterm';
import { ChevronDoubleRightIcon } from '@heroicons/react/solid';
import classNames from 'classnames';
import { debounce } from 'debounce';
import type { KeyboardEvent as ReactKeyboardEvent } from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import type { ITerminalInitOnlyOptions, ITerminalOptions, ITheme } from 'xterm';
import { Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import { SearchBarAddon } from 'xterm-addon-search-bar';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { ScrollDownHelperAddon } from '@/plugins/XtermScrollDownHelperAddon';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { ServerContext } from '@/state/server';
import { usePermissions } from '@/plugins/usePermissions';
import { theme as th } from 'twin.macro';
import useEventListener from '@/plugins/useEventListener';
import { debounce } from 'debounce';
import { usePersistedState } from '@/plugins/usePersistedState';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { SocketEvent, SocketRequest } from '@/components/server/events';
import classNames from 'classnames';
import { ChevronDoubleRightIcon } from '@heroicons/react/solid';
import { ScrollDownHelperAddon } from '@/plugins/XtermScrollDownHelperAddon';
import useEventListener from '@/plugins/useEventListener';
import { usePermissions } from '@/plugins/usePermissions';
import { usePersistedState } from '@/plugins/usePersistedState';
import { ServerContext } from '@/state/server';
import 'xterm/css/xterm.css';
import styles from './style.module.css';
const theme = {
const theme: ITheme = {
background: th`colors.black`.toString(),
cursor: 'transparent',
black: th`colors.black`.toString(),
@ -38,7 +41,7 @@ const theme = {
brightMagenta: '#C792EA',
brightCyan: '#89DDFF',
brightWhite: '#ffffff',
selection: '#FAF089',
selectionBackground: '#FAF089',
};
const terminalProps: ITerminalOptions = {
@ -47,23 +50,27 @@ const terminalProps: ITerminalOptions = {
allowTransparency: true,
fontSize: 12,
fontFamily: th('fontFamily.mono'),
rows: 30,
theme: theme,
allowProposedApi: true,
};
const terminalInitOnlyProps: ITerminalInitOnlyOptions = {
rows: 30,
};
export default () => {
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m';
const ref = useRef<HTMLDivElement>(null);
const terminal = useMemo(() => new Terminal({ ...terminalProps }), []);
const terminal = useMemo(() => new Terminal({ ...terminalProps, ...terminalInitOnlyProps }), []);
const fitAddon = new FitAddon();
const searchAddon = new SearchAddon();
const searchBar = new SearchBarAddon({ searchAddon });
const webLinksAddon = new WebLinksAddon();
const scrollDownHelperAddon = new ScrollDownHelperAddon();
const { connected, instance } = ServerContext.useStoreState((state) => state.socket);
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
const [canSendCommands] = usePermissions(['control.console']);
const serverId = ServerContext.useStoreState((state) => state.server.data!.id);
const isTransferring = ServerContext.useStoreState((state) => state.server.data!.isTransferring);
const serverId = ServerContext.useStoreState(state => state.server.data!.id);
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
const [history, setHistory] = usePersistedState<string[]>(`${serverId}:command_history`, []);
const [historyIndex, setHistoryIndex] = useState(-1);
@ -81,20 +88,20 @@ export default () => {
const handleDaemonErrorOutput = (line: string) =>
terminal.writeln(
TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m'
TERMINAL_PRELUDE + '\u001b[1m\u001b[41m' + line.replace(/(?:\r\n|\r|\n)$/im, '') + '\u001b[0m',
);
const handlePowerChangeEvent = (state: string) =>
terminal.writeln(TERMINAL_PRELUDE + 'Server marked as ' + state + '...\u001b[0m');
const handleCommandKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
const handleCommandKeyDown = (e: ReactKeyboardEvent<HTMLInputElement>) => {
if (e.key === 'ArrowUp') {
const newIndex = Math.min(historyIndex + 1, history!.length - 1);
setHistoryIndex(newIndex);
e.currentTarget.value = history![newIndex] || '';
// By default up arrow will also bring the cursor to the start of the line,
// By default, up arrow will also bring the cursor to the start of the line,
// so we'll preventDefault to keep it at the end.
e.preventDefault();
}
@ -108,7 +115,7 @@ export default () => {
const command = e.currentTarget.value;
if (e.key === 'Enter' && command.length > 0) {
setHistory((prevHistory) => [command, ...prevHistory!].slice(0, 32));
setHistory(prevHistory => [command, ...prevHistory!].slice(0, 32));
setHistoryIndex(-1);
instance && instance.send('send command', command);
@ -150,7 +157,7 @@ export default () => {
if (terminal.element) {
fitAddon.fit();
}
}, 100)
}, 100),
);
useEffect(() => {
@ -160,7 +167,7 @@ export default () => {
[SocketEvent.INSTALL_OUTPUT]: handleConsoleOutput,
[SocketEvent.TRANSFER_LOGS]: handleConsoleOutput,
[SocketEvent.TRANSFER_STATUS]: handleTransferStatus,
[SocketEvent.DAEMON_MESSAGE]: (line) => handleConsoleOutput(line, true),
[SocketEvent.DAEMON_MESSAGE]: line => handleConsoleOutput(line, true),
[SocketEvent.DAEMON_ERROR]: handleDaemonErrorOutput,
};
@ -171,7 +178,12 @@ export default () => {
}
Object.keys(listeners).forEach((key: string) => {
instance.addListener(key, listeners[key]);
const listener = listeners[key];
if (listener === undefined) {
return;
}
instance.addListener(key, listener);
});
instance.send(SocketRequest.SEND_LOGS);
}
@ -179,7 +191,12 @@ export default () => {
return () => {
if (instance) {
Object.keys(listeners).forEach((key: string) => {
instance.removeListener(key, listeners[key]);
const listener = listeners[key];
if (listener === undefined) {
return;
}
instance.removeListener(key, listener);
});
}
};
@ -210,7 +227,7 @@ export default () => {
<div
className={classNames(
'text-gray-100 peer-focus:text-gray-50 peer-focus:animate-pulse',
styles.command_icon
styles.command_icon,
)}
>
<ChevronDoubleRightIcon className={'w-4 h-4'} />

View file

@ -1,4 +1,5 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import * as React from 'react';
import { Button } from '@/components/elements/button/index';
import Can from '@/components/elements/Can';
import { ServerContext } from '@/state/server';
@ -11,13 +12,13 @@ interface PowerButtonProps {
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 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<HTMLButtonElement, MouseEvent>
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
): void => {
e.preventDefault();
if (action === 'kill') {

View file

@ -1,25 +1,26 @@
import React, { memo } from 'react';
import { ServerContext } from '@/state/server';
import { memo } from 'react';
import isEqual from 'react-fast-compare';
import { Alert } from '@/components/elements/alert';
import Can from '@/components/elements/Can';
import ServerContentBlock from '@/components/elements/ServerContentBlock';
import isEqual from 'react-fast-compare';
import Spinner from '@/components/elements/Spinner';
import Features from '@feature/Features';
import Console from '@/components/server/console/Console';
import StatGraphs from '@/components/server/console/StatGraphs';
import PowerButtons from '@/components/server/console/PowerButtons';
import ServerDetailsBlock from '@/components/server/console/ServerDetailsBlock';
import { Alert } from '@/components/elements/alert';
import StatGraphs from '@/components/server/console/StatGraphs';
import Features from '@feature/Features';
import { ServerContext } from '@/state/server';
export type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
const ServerConsoleContainer = () => {
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.isInstalling);
const isTransferring = ServerContext.useStoreState((state) => state.server.data!.isTransferring);
const eggFeatures = ServerContext.useStoreState((state) => state.server.data!.eggFeatures, isEqual);
const isNodeUnderMaintenance = ServerContext.useStoreState((state) => state.server.data!.isNodeUnderMaintenance);
function ServerConsoleContainer() {
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.isInstalling);
const isTransferring = ServerContext.useStoreState(state => state.server.data!.isTransferring);
const eggFeatures = ServerContext.useStoreState(state => state.server.data!.eggFeatures, isEqual);
const isNodeUnderMaintenance = ServerContext.useStoreState(state => state.server.data!.isNodeUnderMaintenance);
return (
<ServerContentBlock title={'Console'}>
@ -59,6 +60,6 @@ const ServerConsoleContainer = () => {
<Features enabled={eggFeatures} />
</ServerContentBlock>
);
};
}
export default memo(ServerConsoleContainer, isEqual);

View file

@ -1,4 +1,3 @@
import React, { useEffect, useMemo, useState } from 'react';
import {
faClock,
faCloudDownloadAlt,
@ -8,18 +7,21 @@ import {
faMicrochip,
faWifi,
} from '@fortawesome/free-solid-svg-icons';
import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
import { ServerContext } from '@/state/server';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useEffect, useMemo, useState } from 'react';
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';
import classNames from 'classnames';
import { bytesToString, ip, mbToBytes } from '@/lib/formatters';
import { capitalize } from '@/lib/strings';
import { ServerContext } from '@/state/server';
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
type Stats = Record<'memory' | 'cpu' | 'disk' | 'uptime' | 'rx' | 'tx', number>;
const getBackgroundColor = (value: number, max: number | null): string | undefined => {
function getBackgroundColor(value: number, max: number | null): string | undefined {
const delta = !max ? 0 : value / max;
if (delta > 0.8) {
@ -30,22 +32,24 @@ const getBackgroundColor = (value: number, max: number | null): string | undefin
}
return undefined;
};
}
const Limit = ({ limit, children }: { limit: string | null; children: React.ReactNode }) => (
<>
{children}
<span className={'ml-1 text-gray-300 text-[70%] select-none'}>/ {limit || <>&infin;</>}</span>
</>
);
function Limit({ limit, children }: { limit: string | null; children: ReactNode }) {
return (
<>
{children}
<span className={'ml-1 text-gray-300 text-[70%] select-none'}>/ {limit || <>&infin;</>}</span>
</>
);
}
const ServerDetailsBlock = ({ className }: { className?: string }) => {
function ServerDetailsBlock({ className }: { className?: string }) {
const [stats, setStats] = useState<Stats>({ 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 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 textLimits = useMemo(
() => ({
@ -53,11 +57,11 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
memory: limits?.memory ? bytesToString(mbToBytes(limits.memory)) : null,
disk: limits?.disk ? bytesToString(mbToBytes(limits.disk)) : null,
}),
[limits]
[limits],
);
const allocation = ServerContext.useStoreState((state) => {
const match = state.server.data!.allocations.find((allocation) => allocation.isDefault);
const allocation = ServerContext.useStoreState(state => {
const match = state.server.data!.allocations.find(allocation => allocation.isDefault);
return !match ? 'n/a' : `${match.alias || ip(match.ip)}:${match.port}`;
});
@ -70,7 +74,7 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
instance.send(SocketRequest.SEND_STATS);
}, [instance, connected]);
useWebsocketEvent(SocketEvent.STATS, (data) => {
useWebsocketEvent(SocketEvent.STATS, data => {
let stats: any = {};
try {
stats = JSON.parse(data);
@ -135,6 +139,6 @@ const ServerDetailsBlock = ({ className }: { className?: string }) => {
</StatBlock>
</div>
);
};
}
export default ServerDetailsBlock;

View file

@ -1,21 +1,23 @@
import React from 'react';
import Icon from '@/components/elements/Icon';
import { IconDefinition } from '@fortawesome/free-solid-svg-icons';
import type { IconDefinition } from '@fortawesome/free-solid-svg-icons';
import classNames from 'classnames';
import styles from './style.module.css';
import useFitText from 'use-fit-text';
import type { ReactNode } from 'react';
import { useFitText } from '@flyyer/use-fit-text';
import CopyOnClick from '@/components/elements/CopyOnClick';
import Icon from '@/components/elements/Icon';
import styles from './style.module.css';
interface StatBlockProps {
title: string;
copyOnClick?: string;
color?: string | undefined;
icon: IconDefinition;
children: React.ReactNode;
children: ReactNode;
className?: string;
}
export default ({ title, copyOnClick, icon, color, className, children }: StatBlockProps) => {
function StatBlock({ title, copyOnClick, icon, color, className, children }: StatBlockProps) {
const { fontSize, ref } = useFitText({ minFontSize: 8, maxFontSize: 500 });
return (
@ -44,4 +46,6 @@ export default ({ title, copyOnClick, icon, color, className, children }: StatBl
</div>
</CopyOnClick>
);
};
}
export default StatBlock;

View file

@ -1,4 +1,4 @@
import React, { useEffect, useRef } from 'react';
import { useEffect, useRef } from 'react';
import { ServerContext } from '@/state/server';
import { SocketEvent } from '@/components/server/events';
import useWebsocketEvent from '@/plugins/useWebsocketEvent';
@ -12,8 +12,8 @@ import ChartBlock from '@/components/server/console/ChartBlock';
import Tooltip from '@/components/elements/tooltip/Tooltip';
export default () => {
const status = ServerContext.useStoreState((state) => state.status.value);
const limits = ServerContext.useStoreState((state) => state.server.data!.limits);
const status = ServerContext.useStoreState(state => state.status.value);
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
const previous = useRef<Record<'tx' | 'rx', number>>({ tx: -1, rx: -1 });
const cpu = useChartTickLabel('CPU', limits.cpu, '%', 2);

View file

@ -71,13 +71,14 @@ const options: ChartOptions<'line'> = {
};
function getOptions(opts?: DeepPartial<ChartOptions<'line'>> | undefined): ChartOptions<'line'> {
return deepmerge(options, opts || {});
// @ts-expect-error go away
return deepmerge(options, opts ?? {});
}
type ChartDatasetCallback = (value: ChartDataset<'line'>, index: number) => ChartDataset<'line'>;
function getEmptyData(label: string, sets = 1, callback?: ChartDatasetCallback | undefined): ChartData<'line'> {
const next = callback || ((value) => value);
const next = callback || (value => value);
return {
labels: Array(20)
@ -94,8 +95,8 @@ function getEmptyData(label: string, sets = 1, callback?: ChartDatasetCallback |
borderColor: theme('colors.cyan.400'),
backgroundColor: hexToRgba(theme('colors.cyan.700'), 0.5),
},
index
)
index,
),
),
};
}
@ -110,30 +111,31 @@ interface UseChartOptions {
function useChart(label: string, opts?: UseChartOptions) {
const options = getOptions(
typeof opts?.options === 'number' ? { scales: { y: { min: 0, suggestedMax: opts.options } } } : opts?.options
typeof opts?.options === 'number' ? { scales: { y: { min: 0, suggestedMax: opts.options } } } : opts?.options,
);
const [data, setData] = useState(getEmptyData(label, opts?.sets || 1, opts?.callback));
const push = (items: number | null | (number | null)[]) =>
setData((state) =>
setData(state =>
merge(state, {
datasets: (Array.isArray(items) ? items : [items]).map((item, index) => ({
...state.datasets[index],
data: state.datasets[index].data
.slice(1)
.concat(typeof item === 'number' ? Number(item.toFixed(2)) : item),
data:
state.datasets[index]?.data
?.slice(1)
?.concat(typeof item === 'number' ? Number(item.toFixed(2)) : item) ?? [],
})),
})
}),
);
const clear = () =>
setData((state) =>
setData(state =>
merge(state, {
datasets: state.datasets.map((value) => ({
datasets: state.datasets.map(value => ({
...value,
data: Array(20).fill(-5),
})),
})
}),
);
return { props: { data, options }, push, clear };