Some mobile improvements for the UI; make console fill space better
This commit is contained in:
parent
faff263f17
commit
54c619e6ba
9 changed files with 127 additions and 93 deletions
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
}
|
|
@ -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')}
|
||||
|
|
|
@ -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);
|
|
@ -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'}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
59
resources/scripts/components/server/console/style.module.css
Normal file
59
resources/scripts/components/server/console/style.module.css
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue