Some mobile improvements for the UI; make console fill space better

This commit is contained in:
DaneEveritt 2022-06-21 18:43:59 -04:00
parent faff263f17
commit 54c619e6ba
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
9 changed files with 127 additions and 93 deletions

View file

@ -1,6 +1,7 @@
import React, { Suspense } from 'react';
import styled, { css, keyframes } from 'styled-components/macro';
import tw from 'twin.macro';
import ErrorBoundary from '@/components/elements/ErrorBoundary';
export type SpinnerSize = 'small' | 'base' | 'large';
@ -25,12 +26,12 @@ const SpinnerComponent = styled.div<Props>`
border-width: 3px;
border-radius: 50%;
animation: ${spin} 1s cubic-bezier(0.55, 0.25, 0.25, 0.70) infinite;
${props => props.size === 'small' ? tw`w-4 h-4 border-2` : (props.size === 'large' ? css`
${tw`w-16 h-16`};
border-width: 6px;
` : null)};
border-color: ${props => !props.isBlue ? 'rgba(255, 255, 255, 0.2)' : 'hsla(212, 92%, 43%, 0.2)'};
border-top-color: ${props => !props.isBlue ? 'rgb(255, 255, 255)' : 'hsl(212, 92%, 43%)'};
`;
@ -58,7 +59,9 @@ Spinner.Size = {
Spinner.Suspense = ({ children, centered = true, size = Spinner.Size.LARGE, ...props }) => (
<Suspense fallback={<Spinner centered={centered} size={size} {...props}/>}>
{children}
<ErrorBoundary>
{children}
</ErrorBoundary>
</Suspense>
);
Spinner.Suspense.displayName = 'Spinner.Suspense';

View file

@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { Terminal, ITerminalOptions } from 'xterm';
import { ITerminalOptions, Terminal } from 'xterm';
import { FitAddon } from 'xterm-addon-fit';
import { SearchAddon } from 'xterm-addon-search';
import { SearchBarAddon } from 'xterm-addon-search-bar';
@ -7,14 +7,16 @@ import { WebLinksAddon } from 'xterm-addon-web-links';
import { ScrollDownHelperAddon } from '@/plugins/XtermScrollDownHelperAddon';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { ServerContext } from '@/state/server';
import styled from 'styled-components/macro';
import { usePermissions } from '@/plugins/usePermissions';
import tw, { theme as th } from 'twin.macro';
import { theme as th } from 'twin.macro';
import 'xterm/css/xterm.css';
import useEventListener from '@/plugins/useEventListener';
import { debounce } from 'debounce';
import { usePersistedState } from '@/plugins/usePersistedState';
import { SocketEvent, SocketRequest } from '@/components/server/events';
import classNames from 'classnames';
import styles from './style.module.css';
import { ChevronDoubleRightIcon } from '@heroicons/react/solid';
const theme = {
background: th`colors.black`.toString(),
@ -48,23 +50,6 @@ const terminalProps: ITerminalOptions = {
theme: theme,
};
const TerminalDiv = styled.div`
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-thumb {
${tw`bg-neutral-900`};
}
`;
const CommandInput = styled.input`
${tw`text-sm transition-colors duration-150 px-2 bg-transparent border-0 border-b-2 border-transparent text-neutral-100 p-2 pl-0 w-full focus:ring-0`}
&:focus {
${tw`border-cyan-700`};
}
`;
export default () => {
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m';
const ref = useRef<HTMLDivElement>(null);
@ -202,30 +187,25 @@ export default () => {
}, [ connected, instance ]);
return (
<div css={tw`text-xs font-mono relative`}>
<SpinnerOverlay visible={!connected} size={'large'} />
<div
css={[
tw`rounded-t p-2 bg-black w-full`,
!canSendCommands && tw`rounded-b`,
]}
style={{ minHeight: '16rem' }}
>
<TerminalDiv id={'terminal'} ref={ref} />
<div className={styles.terminal}>
<SpinnerOverlay visible={!connected} size={'large'}/>
<div className={classNames(styles.container, styles.overflows_container, { 'rounded-b': !canSendCommands })}>
<div id={styles.terminal} ref={ref}/>
</div>
{canSendCommands &&
<div css={tw`rounded-b bg-neutral-900 text-neutral-100 flex items-baseline`}>
<div css={tw`flex-shrink-0 p-2 font-bold`}>$</div>
<div css={tw`w-full`}>
<CommandInput
type={'text'}
placeholder={'Type a command...'}
aria-label={'Console command input.'}
disabled={!instance || !connected}
onKeyDown={handleCommandKeyDown}
autoCorrect={'off'}
autoCapitalize={'none'}
/>
<div className={classNames('relative', styles.overflows_container)}>
<input
className={classNames('peer', styles.command_input)}
type={'text'}
placeholder={'Type a command...'}
aria-label={'Console command input.'}
disabled={!instance || !connected}
onKeyDown={handleCommandKeyDown}
autoCorrect={'off'}
autoCapitalize={'none'}
/>
<div className={classNames('text-gray-100 peer-focus:text-gray-50 peer-focus:animate-pulse', styles.command_icon)}>
<ChevronDoubleRightIcon className={'w-4 h-4'}/>
</div>
</div>
}

View file

@ -2,7 +2,7 @@ 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 { PowerAction } from '@/components/server/console/ServerConsoleContainer';
import { Dialog } from '@/components/elements/dialog';
interface PowerButtonProps {
@ -41,7 +41,7 @@ export default ({ className }: PowerButtonProps) => {
</Dialog.Confirm>
<Can action={'control.start'}>
<Button
className={'w-24'}
className={'w-full sm:w-24'}
disabled={status !== 'offline'}
onClick={onButtonClick.bind(this, 'start')}
>
@ -50,7 +50,7 @@ export default ({ className }: PowerButtonProps) => {
</Can>
<Can action={'control.restart'}>
<Button.Text
className={'w-24'}
className={'w-full sm:w-24'}
variant={Button.Variants.Secondary}
disabled={!status}
onClick={onButtonClick.bind(this, 'restart')}
@ -60,7 +60,7 @@ export default ({ className }: PowerButtonProps) => {
</Can>
<Can action={'control.stop'}>
<Button.Danger
className={'w-24'}
className={'w-full sm:w-24'}
variant={killable ? undefined : Button.Variants.Secondary}
disabled={status === 'offline'}
onClick={onButtonClick.bind(this, killable ? 'kill' : 'stop')}

View file

@ -5,17 +5,16 @@ import ContentContainer from '@/components/elements/ContentContainer';
import tw from 'twin.macro';
import ServerContentBlock from '@/components/elements/ServerContentBlock';
import isEqual from 'react-fast-compare';
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 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/ServerDetailsBlock';
import ServerDetailsBlock from '@/components/server/console/ServerDetailsBlock';
export type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
const ServerConsole = () => {
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.data!.isInstalling);
@ -23,20 +22,25 @@ const ServerConsole = () => {
const eggFeatures = ServerContext.useStoreState(state => state.server.data!.eggFeatures, isEqual);
return (
<ServerContentBlock title={'Console'} className={'grid grid-cols-4 gap-4'}>
<div className={'flex space-x-4 items-end col-span-4'}>
<div className={'flex-1'}>
<ServerContentBlock title={'Console'} className={'flex flex-col gap-2 sm:gap-4'}>
<div className={'flex gap-4 items-end'}>
<div className={'hidden sm:block flex-1'}>
<h1 className={'font-header text-2xl text-gray-50 leading-relaxed line-clamp-1'}>{name}</h1>
<p className={'text-sm line-clamp-2'}>{description}</p>
</div>
<div className={'flex-1'}>
<Can action={[ 'control.start', 'control.stop', 'control.restart' ]} matchAny>
<PowerButtons className={'flex justify-end space-x-2'}/>
<PowerButtons className={'flex sm:justify-end space-x-2'}/>
</Can>
</div>
</div>
<div className={'col-span-4 lg:col-span-1'}>
<ServerDetailsBlock className={'flex flex-col space-y-4'}/>
<div className={'grid grid-cols-4 gap-2 sm:gap-4'}>
<ServerDetailsBlock className={'col-span-4 lg:col-span-1 order-last lg:order-none'}/>
<div className={'col-span-4 lg:col-span-3'}>
<Spinner.Suspense>
<Console/>
</Spinner.Suspense>
</div>
{isInstalling ?
<div css={tw`mt-4 rounded bg-yellow-500 p-3`}>
<ContentContainer>
@ -60,17 +64,14 @@ const ServerConsole = () => {
null
}
</div>
<div className={'col-span-3'}>
<div className={'grid grid-cols-1 md:grid-cols-3 gap-2 sm:gap-4'}>
<Spinner.Suspense>
<ErrorBoundary>
<Console/>
</ErrorBoundary>
<StatGraphs/>
</Spinner.Suspense>
<Features enabled={eggFeatures}/>
</div>
<Features enabled={eggFeatures}/>
</ServerContentBlock>
);
};
export default memo(ServerConsole, isEqual);
export default memo(ServerConsoleContainer, isEqual);

View file

@ -14,13 +14,10 @@ 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';
type Stats = Record<'memory' | 'cpu' | 'disk' | 'uptime' | 'rx' | 'tx', number>;
interface DetailBlockProps {
className?: string;
}
const getBackgroundColor = (value: number, max: number | null): string | undefined => {
const delta = !max ? 0 : (value / max);
@ -34,7 +31,7 @@ const getBackgroundColor = (value: number, max: number | null): string | undefin
return undefined;
};
const ServerDetailsBlock = ({ className }: DetailBlockProps) => {
const 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);
@ -74,7 +71,7 @@ const ServerDetailsBlock = ({ className }: DetailBlockProps) => {
});
return (
<div className={className}>
<div className={classNames('grid grid-cols-6 gap-2 md:gap-4', className)}>
<StatBlock
icon={faClock}
title={'Uptime'}

View file

@ -3,6 +3,7 @@ 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';
import styles from './style.module.css';
interface StatBlockProps {
title: string;
@ -10,33 +11,26 @@ interface StatBlockProps {
color?: string | undefined;
icon: IconDefinition;
children: React.ReactNode;
className?: string;
}
export default ({ title, icon, color, description, children }: StatBlockProps) => {
export default ({ title, icon, color, description, className, children }: StatBlockProps) => {
return (
<Tooltip arrow placement={'top'} disabled={!description} content={description || ''}>
<div className={'flex items-center space-x-4 bg-gray-600 rounded p-4 shadow-lg'}>
<div
className={classNames(
'transition-colors duration-500',
'flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-lg shadow-md',
color || 'bg-gray-700',
)}
>
<div className={classNames(styles.stat_block, 'bg-gray-600', className)}>
<div className={classNames(styles.status_bar, color || 'bg-gray-700')}/>
<div className={classNames(styles.icon, color || 'bg-gray-700')}>
<Icon
icon={icon}
className={classNames(
'w-6 h-6 m-auto',
{
'text-gray-100': !color || color === 'bg-gray-700',
'text-gray-50': color && color !== 'bg-gray-700',
},
)}
className={classNames({
'text-gray-100': !color || color === 'bg-gray-700',
'text-gray-50': color && color !== 'bg-gray-700',
})}
/>
</div>
<div className={'flex flex-col justify-center overflow-hidden'}>
<p className={'font-header leading-tight text-sm text-gray-200'}>{title}</p>
<p className={'text-xl font-semibold text-gray-50 truncate'}>
<p className={'font-header leading-tight text-xs md:text-sm text-gray-200'}>{title}</p>
<p className={'text-base md:text-xl font-semibold text-gray-50 truncate'}>
{children}
</p>
</div>

View file

@ -123,7 +123,7 @@ export default () => {
});
return (
<div css={tw`mt-4 grid grid-cols-1 sm:grid-cols-2 gap-4`}>
<>
<TitledGreyBox title={'Memory usage'} icon={faMemory}>
{status !== 'offline' ?
<canvas
@ -165,6 +165,6 @@ export default () => {
</p>
}
</TitledGreyBox>
</div>
</>
);
};

View file

@ -0,0 +1,59 @@
.stat_block {
@apply flex items-center rounded shadow-lg relative;
@apply col-span-3 md:col-span-2 lg:col-span-6;
@apply px-3 py-2 md:p-3 lg:p-4;
& > .status_bar {
@apply w-1 h-full absolute left-0 top-0 rounded-l sm:hidden;
}
& > .icon {
@apply hidden flex-shrink-0 items-center justify-center rounded-lg shadow-md w-12 h-12;
@apply transition-colors duration-500;
@apply sm:flex sm:mr-4;
& > svg {
@apply w-6 h-6 m-auto;
}
}
}
.terminal {
@apply relative h-full flex flex-col;
& .overflows_container {
@apply -ml-4 sm:ml-0;
width: calc(100% + 2rem);
@screen sm {
@apply w-full;
}
}
& > .container {
@apply rounded-t p-1 sm:p-2 bg-black min-h-[16rem] flex-1 font-mono text-sm;
& #terminal {
@apply h-full;
&::-webkit-scrollbar-track {
@apply w-2;
}
&::-webkit-scrollbar-thumb {
@apply bg-gray-900;
}
}
}
& .command_icon {
@apply flex items-center top-0 left-0 absolute z-10 select-none h-full px-3 transition-colors duration-100;
}
& .command_input {
@apply relative bg-gray-900 px-2 text-gray-100 pl-10 pr-4 py-2 w-full font-mono text-sm sm:rounded-b;
@apply focus:ring-0 outline-none focus-visible:outline-none;
@apply border-0 border-b-2 border-transparent transition-colors duration-100;
@apply active:border-cyan-500 focus:border-cyan-500;
}
}

View file

@ -1,5 +1,5 @@
import React, { lazy } from 'react';
import ServerConsole from '@/components/server/ServerConsole';
import ServerConsole from '@/components/server/console/ServerConsoleContainer';
import DatabasesContainer from '@/components/server/databases/DatabasesContainer';
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
import UsersContainer from '@/components/server/users/UsersContainer';