Merge branch 'develop' into develop
This commit is contained in:
commit
ea778e9345
159 changed files with 3400 additions and 3896 deletions
|
@ -9,7 +9,6 @@ interface Response {
|
|||
}
|
||||
|
||||
export default (uuid: string, initialData?: Response) => useSWR([ uuid, '/startup' ], async (): Promise<Response> => {
|
||||
console.log('firing getServerStartup');
|
||||
const { data } = await http.get(`/api/client/servers/${uuid}/startup`);
|
||||
|
||||
const variables = ((data as FractalResponseList).data || []).map(rawDataToServerEggVariable);
|
||||
|
|
|
@ -94,7 +94,6 @@ export default () => {
|
|||
}
|
||||
<div css={tw`mt-6 text-center`}>
|
||||
<Link
|
||||
type={'button'}
|
||||
to={'/auth/login'}
|
||||
css={tw`text-xs text-neutral-500 tracking-wide uppercase no-underline hover:text-neutral-700`}
|
||||
>
|
||||
|
|
|
@ -14,26 +14,8 @@ import { httpErrorToHuman } from '@/api/http';
|
|||
import { format } from 'date-fns';
|
||||
import PageContentBlock from '@/components/elements/PageContentBlock';
|
||||
import tw from 'twin.macro';
|
||||
import { breakpoint } from '@/theme';
|
||||
import styled from 'styled-components/macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
|
||||
const Container = styled.div`
|
||||
${tw`flex flex-wrap my-10`};
|
||||
|
||||
& > div {
|
||||
${tw`w-full`};
|
||||
|
||||
${breakpoint('md')`
|
||||
width: calc(50% - 1rem);
|
||||
`}
|
||||
|
||||
${breakpoint('xl')`
|
||||
${tw`w-auto flex-1`};
|
||||
`}
|
||||
}
|
||||
`;
|
||||
|
||||
export default () => {
|
||||
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
|
||||
const [ keys, setKeys ] = useState<ApiKey[]>([]);
|
||||
|
@ -67,12 +49,12 @@ export default () => {
|
|||
|
||||
return (
|
||||
<PageContentBlock title={'Account API'}>
|
||||
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
|
||||
<Container>
|
||||
<ContentBox title={'Create API Key'}>
|
||||
<FlashMessageRender byKey={'account'}/>
|
||||
<div css={tw`md:flex flex-no-wrap my-10`}>
|
||||
<ContentBox title={'Create API Key'} css={tw`flex-none w-full md:w-1/2`}>
|
||||
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
|
||||
</ContentBox>
|
||||
<ContentBox title={'API Keys'} css={tw`mt-8 md:mt-0 md:ml-8`}>
|
||||
<ContentBox title={'API Keys'} css={tw`flex-1 overflow-hidden mt-8 md:mt-0 md:ml-8`}>
|
||||
<SpinnerOverlay visible={loading}/>
|
||||
<ConfirmationModal
|
||||
visible={!!deleteIdentifier}
|
||||
|
@ -99,14 +81,14 @@ export default () => {
|
|||
css={[ tw`bg-neutral-600 flex items-center`, index > 0 && tw`mt-2` ]}
|
||||
>
|
||||
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`}/>
|
||||
<div css={tw`ml-4 flex-1`}>
|
||||
<p css={tw`text-sm`}>{key.description}</p>
|
||||
<div css={tw`ml-4 flex-1 overflow-hidden`}>
|
||||
<p css={tw`text-sm break-words`}>{key.description}</p>
|
||||
<p css={tw`text-2xs text-neutral-300 uppercase`}>
|
||||
Last used:
|
||||
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
|
||||
</p>
|
||||
</div>
|
||||
<p css={tw`text-sm ml-4`}>
|
||||
<p css={tw`text-sm ml-4 hidden md:block`}>
|
||||
<code css={tw`font-mono py-1 px-2 bg-neutral-900 rounded`}>
|
||||
{key.identifier}
|
||||
</code>
|
||||
|
@ -124,7 +106,7 @@ export default () => {
|
|||
))
|
||||
}
|
||||
</ContentBox>
|
||||
</Container>
|
||||
</div>
|
||||
</PageContentBlock>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,21 +1,43 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { memo, useEffect, useRef, useState } from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faEthernet, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Server } from '@/api/server/getServer';
|
||||
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
|
||||
import getServerResourceUsage, { ServerPowerState, ServerStats } from '@/api/server/getServerResourceUsage';
|
||||
import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
||||
import tw from 'twin.macro';
|
||||
import GreyRowBox from '@/components/elements/GreyRowBox';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import styled from 'styled-components/macro';
|
||||
import isEqual from 'react-fast-compare';
|
||||
|
||||
// Determines if the current value is in an alarm threshold so we can show it in red rather
|
||||
// than the more faded default style.
|
||||
const isAlarmState = (current: number, limit: number): boolean => {
|
||||
const limitInBytes = limit * 1024 * 1024;
|
||||
const isAlarmState = (current: number, limit: number): boolean => limit > 0 && (current / (limit * 1024 * 1024) >= 0.90);
|
||||
|
||||
return current / limitInBytes >= 0.90;
|
||||
};
|
||||
const Icon = memo(styled(FontAwesomeIcon)<{ $alarm: boolean }>`
|
||||
${props => props.$alarm ? tw`text-red-400` : tw`text-neutral-500`};
|
||||
`, isEqual);
|
||||
|
||||
const IconDescription = styled.p<{ $alarm: boolean }>`
|
||||
${tw`text-sm ml-2`};
|
||||
${props => props.$alarm ? tw`text-white` : tw`text-neutral-400`};
|
||||
`;
|
||||
|
||||
const StatusIndicatorBox = styled(GreyRowBox)<{ $status: ServerPowerState | undefined }>`
|
||||
${tw`grid grid-cols-12 gap-4 relative`};
|
||||
|
||||
& .status-bar {
|
||||
${tw`w-2 bg-red-500 absolute right-0 z-20 rounded-full m-1 opacity-50 transition-all duration-150`};
|
||||
height: calc(100% - 0.5rem);
|
||||
|
||||
${({ $status }) => (!$status || $status === 'offline') ? tw`bg-red-500` : ($status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`)};
|
||||
}
|
||||
|
||||
&:hover .status-bar {
|
||||
${tw`opacity-75`};
|
||||
}
|
||||
`;
|
||||
|
||||
export default ({ server, className }: { server: Server; className?: string }) => {
|
||||
const interval = useRef<number>(null);
|
||||
|
@ -54,29 +76,31 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
const memorylimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<GreyRowBox as={Link} to={`/server/${server.id}`} className={className}>
|
||||
<div className={'icon'} css={tw`hidden md:block`}>
|
||||
<FontAwesomeIcon icon={faServer}/>
|
||||
</div>
|
||||
<div css={tw`flex-1 md:ml-4`}>
|
||||
<p css={tw`text-lg break-all`}>{server.name}</p>
|
||||
{!!server.description &&
|
||||
<p css={tw`text-sm text-neutral-300 break-all`}>{server.description}</p>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`w-48 overflow-hidden self-start hidden lg:block`}>
|
||||
<div css={tw`flex ml-4 justify-end`}>
|
||||
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
|
||||
<p css={tw`text-sm text-neutral-400 ml-2`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
<StatusIndicatorBox as={Link} to={`/server/${server.id}`} className={className} $status={stats?.status}>
|
||||
<div css={tw`flex items-center col-span-12 sm:col-span-5 lg:col-span-6`}>
|
||||
<div className={'icon'} css={tw`mr-4`}>
|
||||
<FontAwesomeIcon icon={faServer}/>
|
||||
</div>
|
||||
<div>
|
||||
<p css={tw`text-lg break-words`}>{server.name}</p>
|
||||
{!!server.description &&
|
||||
<p css={tw`text-sm text-neutral-300 break-words`}>{server.description}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`w-1/3 sm:w-1/2 lg:w-1/3 flex items-baseline justify-center relative`}>
|
||||
<div css={tw`hidden lg:col-span-2 lg:flex ml-4 justify-end h-full`}>
|
||||
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
|
||||
<p css={tw`text-sm text-neutral-400 ml-2`}>
|
||||
{
|
||||
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
|
||||
<React.Fragment key={allocation.ip + allocation.port.toString()}>
|
||||
{allocation.alias || allocation.ip}:{allocation.port}
|
||||
</React.Fragment>
|
||||
))
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div css={tw`hidden col-span-7 lg:col-span-4 sm:flex items-baseline justify-center`}>
|
||||
{!stats ?
|
||||
!statsError ?
|
||||
<Spinner size={'small'}/>
|
||||
|
@ -96,80 +120,33 @@ export default ({ server, className }: { server: Server; className?: string }) =
|
|||
:
|
||||
<React.Fragment>
|
||||
<div css={tw`flex-1 flex md:ml-4 sm:flex hidden justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faMicrochip}
|
||||
css={[
|
||||
!alarms.cpu && tw`text-neutral-500`,
|
||||
alarms.cpu && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.cpu && tw`text-neutral-400`,
|
||||
alarms.cpu && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
<Icon icon={faMicrochip} $alarm={alarms.cpu}/>
|
||||
<IconDescription $alarm={alarms.cpu}>
|
||||
{stats.cpuUsagePercent} %
|
||||
</p>
|
||||
</IconDescription>
|
||||
</div>
|
||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||
<div css={tw`flex justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faMemory}
|
||||
css={[
|
||||
!alarms.memory && tw`text-neutral-500`,
|
||||
alarms.memory && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.memory && tw`text-neutral-400`,
|
||||
alarms.memory && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
<Icon icon={faMemory} $alarm={alarms.memory}/>
|
||||
<IconDescription $alarm={alarms.memory}>
|
||||
{bytesToHuman(stats.memoryUsageInBytes)}
|
||||
</p>
|
||||
</IconDescription>
|
||||
</div>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memorylimit}</p>
|
||||
</div>
|
||||
<div css={tw`flex-1 ml-4 sm:block hidden`}>
|
||||
<div css={tw`flex justify-center`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faHdd}
|
||||
css={[
|
||||
!alarms.disk && tw`text-neutral-500`,
|
||||
alarms.disk && tw`text-red-400`,
|
||||
]}
|
||||
/>
|
||||
<p
|
||||
css={[
|
||||
tw`text-sm ml-2`,
|
||||
!alarms.disk && tw`text-neutral-400`,
|
||||
alarms.disk && tw`text-white`,
|
||||
]}
|
||||
>
|
||||
<Icon icon={faHdd} $alarm={alarms.disk}/>
|
||||
<IconDescription $alarm={alarms.disk}>
|
||||
{bytesToHuman(stats.diskUsageInBytes)}
|
||||
</p>
|
||||
</IconDescription>
|
||||
</div>
|
||||
<p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {disklimit}</p>
|
||||
</div>
|
||||
<div css={tw`flex-1 flex justify-end sm:hidden`}>
|
||||
<div css={tw`flex items-end text-right`}>
|
||||
<div
|
||||
css={[
|
||||
tw`w-3 h-3 rounded-full`,
|
||||
(!stats?.status || stats?.status === 'offline')
|
||||
? tw`bg-red-500`
|
||||
: (stats?.status === 'running' ? tw`bg-green-500` : tw`bg-yellow-500`),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
}
|
||||
</div>
|
||||
</GreyRowBox>
|
||||
<div className={'status-bar'}/>
|
||||
</StatusIndicatorBox>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -69,6 +69,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
|
|||
{({ isSubmitting }) => (
|
||||
<Modal
|
||||
{...props}
|
||||
top={false}
|
||||
onDismissed={dismiss}
|
||||
dismissable={!isSubmitting}
|
||||
showSpinnerOverlay={loading || isSubmitting}
|
||||
|
|
|
@ -2,11 +2,11 @@ import styled from 'styled-components/macro';
|
|||
import tw from 'twin.macro';
|
||||
|
||||
export default styled.div<{ $hoverable?: boolean }>`
|
||||
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150`};
|
||||
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150 overflow-hidden`};
|
||||
|
||||
${props => props.$hoverable !== false && tw`hover:border-neutral-500`};
|
||||
|
||||
& > div.icon {
|
||||
& .icon {
|
||||
${tw`rounded-full bg-neutral-500 p-3`};
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -35,6 +35,8 @@ const ModalContainer = styled.div<{ alignTop?: boolean }>`
|
|||
margin-top: 20%;
|
||||
${breakpoint('md')`margin-top: 10%`};
|
||||
`};
|
||||
|
||||
margin-bottom: auto;
|
||||
|
||||
& > .close-icon {
|
||||
${tw`absolute right-0 p-2 text-white cursor-pointer opacity-50 transition-all duration-150 ease-linear hover:opacity-100`};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { ITerminalOptions, Terminal } from 'xterm';
|
||||
import * as TerminalFit from 'xterm/lib/addons/fit/fit';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
|
@ -7,6 +7,8 @@ import styled from 'styled-components/macro';
|
|||
import { usePermissions } from '@/plugins/usePermissions';
|
||||
import tw from 'twin.macro';
|
||||
import 'xterm/dist/xterm.css';
|
||||
import useEventListener from '@/plugins/useEventListener';
|
||||
import { debounce } from 'debounce';
|
||||
|
||||
const theme = {
|
||||
background: 'transparent',
|
||||
|
@ -51,8 +53,7 @@ const TerminalDiv = styled.div`
|
|||
|
||||
export default () => {
|
||||
const TERMINAL_PRELUDE = '\u001b[1m\u001b[33mcontainer@pterodactyl~ \u001b[0m';
|
||||
const [ terminalElement, setTerminalElement ] = useState<HTMLDivElement | null>(null);
|
||||
const useRef = useCallback(node => setTerminalElement(node), []);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const terminal = useMemo(() => new Terminal({ ...terminalProps }), []);
|
||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
const [ canSendCommands ] = usePermissions([ 'control.console' ]);
|
||||
|
@ -79,8 +80,8 @@ export default () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (connected && terminalElement && !terminal.element) {
|
||||
terminal.open(terminalElement);
|
||||
if (connected && ref.current && !terminal.element) {
|
||||
terminal.open(ref.current);
|
||||
|
||||
// @see https://github.com/xtermjs/xterm.js/issues/2265
|
||||
// @see https://github.com/xtermjs/xterm.js/issues/2230
|
||||
|
@ -97,7 +98,13 @@ export default () => {
|
|||
return true;
|
||||
});
|
||||
}
|
||||
}, [ terminal, connected, terminalElement ]);
|
||||
}, [ terminal, connected ]);
|
||||
|
||||
const fit = debounce(() => {
|
||||
TerminalFit.fit(terminal);
|
||||
}, 100);
|
||||
|
||||
useEventListener('resize', () => fit());
|
||||
|
||||
useEffect(() => {
|
||||
if (connected && instance) {
|
||||
|
@ -134,7 +141,7 @@ export default () => {
|
|||
maxHeight: '32rem',
|
||||
}}
|
||||
>
|
||||
<TerminalDiv id={'terminal'} ref={useRef}/>
|
||||
<TerminalDiv id={'terminal'} ref={ref}/>
|
||||
</div>
|
||||
{canSendCommands &&
|
||||
<div css={tw`rounded-b bg-neutral-900 text-neutral-100 flex`}>
|
||||
|
|
55
resources/scripts/components/server/PowerControls.tsx
Normal file
55
resources/scripts/components/server/PowerControls.tsx
Normal file
|
@ -0,0 +1,55 @@
|
|||
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 (
|
||||
<div css={tw`shadow-md bg-neutral-700 rounded p-3 flex text-xs mt-4 justify-center`}>
|
||||
<Can action={'control.start'}>
|
||||
<Button
|
||||
size={'xsmall'}
|
||||
color={'green'}
|
||||
isSecondary
|
||||
css={tw`mr-2`}
|
||||
disabled={status !== 'offline'}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
sendPowerCommand('start');
|
||||
}}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</Can>
|
||||
<Can action={'control.restart'}>
|
||||
<Button
|
||||
size={'xsmall'}
|
||||
isSecondary
|
||||
css={tw`mr-2`}
|
||||
disabled={!status}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
sendPowerCommand('restart');
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
</Can>
|
||||
<Can action={'control.stop'}>
|
||||
<StopOrKillButton onPress={action => sendPowerCommand(action)}/>
|
||||
</Can>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PowerControls;
|
|
@ -1,130 +1,29 @@
|
|||
import React, { lazy, useEffect, useState } from 'react';
|
||||
import React, { lazy, memo } from 'react';
|
||||
import { ServerContext } from '@/state/server';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
||||
import SuspenseSpinner from '@/components/elements/SuspenseSpinner';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import Can from '@/components/elements/Can';
|
||||
import ContentContainer from '@/components/elements/ContentContainer';
|
||||
import tw from 'twin.macro';
|
||||
import Button from '@/components/elements/Button';
|
||||
import StopOrKillButton from '@/components/server/StopOrKillButton';
|
||||
import ServerContentBlock from '@/components/elements/ServerContentBlock';
|
||||
import ServerDetailsBlock from '@/components/server/ServerDetailsBlock';
|
||||
import isEqual from 'react-fast-compare';
|
||||
import PowerControls from '@/components/server/PowerControls';
|
||||
|
||||
export type PowerAction = 'start' | 'stop' | 'restart' | 'kill';
|
||||
|
||||
const ChunkedConsole = lazy(() => import(/* webpackChunkName: "console" */'@/components/server/Console'));
|
||||
const ChunkedStatGraphs = lazy(() => import(/* webpackChunkName: "graphs" */'@/components/server/StatGraphs'));
|
||||
|
||||
export default () => {
|
||||
const [ memory, setMemory ] = useState(0);
|
||||
const [ cpu, setCpu ] = useState(0);
|
||||
const [ disk, setDisk ] = useState(0);
|
||||
|
||||
const name = ServerContext.useStoreState(state => state.server.data!.name);
|
||||
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
|
||||
const ServerConsole = () => {
|
||||
const isInstalling = ServerContext.useStoreState(state => state.server.data!.isInstalling);
|
||||
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 statsListener = (data: string) => {
|
||||
let stats: any = {};
|
||||
try {
|
||||
stats = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
setMemory(stats.memory_bytes);
|
||||
setCpu(stats.cpu_absolute);
|
||||
setDisk(stats.disk_bytes);
|
||||
};
|
||||
|
||||
const sendPowerCommand = (command: PowerAction) => {
|
||||
instance && instance.send('set state', command);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.addListener('stats', statsListener);
|
||||
|
||||
return () => {
|
||||
instance.removeListener('stats', statsListener);
|
||||
};
|
||||
}, [ instance, connected ]);
|
||||
|
||||
const disklimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||
const memorylimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<ServerContentBlock title={'Console'} css={tw`flex flex-wrap`}>
|
||||
<div css={tw`w-full md:w-1/4`}>
|
||||
<TitledGreyBox css={tw`break-all`} title={name} icon={faServer}>
|
||||
<p css={tw`text-xs uppercase`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faCircle}
|
||||
fixedWidth
|
||||
css={[
|
||||
tw`mr-1`,
|
||||
status === 'offline' ? tw`text-red-500` : (status === 'running' ? tw`text-green-500` : tw`text-yellow-500`),
|
||||
]}
|
||||
/>
|
||||
{!status ? 'Connecting...' : status}
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMicrochip} fixedWidth css={tw`mr-1`}/> {cpu.toFixed(2)}%
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMemory} fixedWidth css={tw`mr-1`}/> {bytesToHuman(memory)}
|
||||
<span css={tw`text-neutral-500`}> / {memorylimit}</span>
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faHdd} fixedWidth css={tw`mr-1`}/> {bytesToHuman(disk)}
|
||||
<span css={tw`text-neutral-500`}> / {disklimit}</span>
|
||||
</p>
|
||||
</TitledGreyBox>
|
||||
<div css={tw`w-full lg:w-1/4`}>
|
||||
<ServerDetailsBlock/>
|
||||
{!isInstalling ?
|
||||
<Can action={[ 'control.start', 'control.stop', 'control.restart' ]} matchAny>
|
||||
<div css={tw`shadow-md bg-neutral-700 rounded p-3 flex text-xs mt-4 justify-center`}>
|
||||
<Can action={'control.start'}>
|
||||
<Button
|
||||
size={'xsmall'}
|
||||
color={'green'}
|
||||
isSecondary
|
||||
css={tw`mr-2`}
|
||||
disabled={status !== 'offline'}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
sendPowerCommand('start');
|
||||
}}
|
||||
>
|
||||
Start
|
||||
</Button>
|
||||
</Can>
|
||||
<Can action={'control.restart'}>
|
||||
<Button
|
||||
size={'xsmall'}
|
||||
isSecondary
|
||||
css={tw`mr-2`}
|
||||
disabled={!status}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
sendPowerCommand('restart');
|
||||
}}
|
||||
>
|
||||
Restart
|
||||
</Button>
|
||||
</Can>
|
||||
<Can action={'control.stop'}>
|
||||
<StopOrKillButton onPress={action => sendPowerCommand(action)}/>
|
||||
</Can>
|
||||
</div>
|
||||
<PowerControls/>
|
||||
</Can>
|
||||
:
|
||||
<div css={tw`mt-4 rounded bg-yellow-500 p-3`}>
|
||||
|
@ -137,7 +36,7 @@ export default () => {
|
|||
</div>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`w-full md:flex-1 md:ml-4 mt-4 md:mt-0`}>
|
||||
<div css={tw`w-full lg:w-3/4 mt-4 lg:mt-0 lg:pl-4`}>
|
||||
<SuspenseSpinner>
|
||||
<ChunkedConsole/>
|
||||
<ChunkedStatGraphs/>
|
||||
|
@ -146,3 +45,5 @@ export default () => {
|
|||
</ServerContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ServerConsole, isEqual);
|
||||
|
|
84
resources/scripts/components/server/ServerDetailsBlock.tsx
Normal file
84
resources/scripts/components/server/ServerDetailsBlock.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { faCircle, faHdd, faMemory, faMicrochip, faServer } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { bytesToHuman, megabytesToHuman } from '@/helpers';
|
||||
import TitledGreyBox from '@/components/elements/TitledGreyBox';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
interface Stats {
|
||||
memory: number;
|
||||
cpu: number;
|
||||
disk: number;
|
||||
}
|
||||
|
||||
const ServerDetailsBlock = () => {
|
||||
const [ stats, setStats ] = useState<Stats>({ memory: 0, cpu: 0, disk: 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 statsListener = (data: string) => {
|
||||
let stats: any = {};
|
||||
try {
|
||||
stats = JSON.parse(data);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
setStats({
|
||||
memory: stats.memory_bytes,
|
||||
cpu: stats.cpu_absolute,
|
||||
disk: stats.disk_bytes,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!connected || !instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
instance.addListener('stats', statsListener);
|
||||
instance.send('send stats');
|
||||
|
||||
return () => {
|
||||
instance.removeListener('stats', statsListener);
|
||||
};
|
||||
}, [ instance, connected ]);
|
||||
|
||||
const name = ServerContext.useStoreState(state => state.server.data!.name);
|
||||
const limits = ServerContext.useStoreState(state => state.server.data!.limits);
|
||||
|
||||
const disklimit = limits.disk ? megabytesToHuman(limits.disk) : 'Unlimited';
|
||||
const memorylimit = limits.memory ? megabytesToHuman(limits.memory) : 'Unlimited';
|
||||
|
||||
return (
|
||||
<TitledGreyBox css={tw`break-words`} title={name} icon={faServer}>
|
||||
<p css={tw`text-xs uppercase`}>
|
||||
<FontAwesomeIcon
|
||||
icon={faCircle}
|
||||
fixedWidth
|
||||
css={[
|
||||
tw`mr-1`,
|
||||
status === 'offline' ? tw`text-red-500` : (status === 'running' ? tw`text-green-500` : tw`text-yellow-500`),
|
||||
]}
|
||||
/>
|
||||
{!status ? 'Connecting...' : status}
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMicrochip} fixedWidth css={tw`mr-1`}/> {stats.cpu.toFixed(2)}%
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faMemory} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.memory)}
|
||||
<span css={tw`text-neutral-500`}> / {memorylimit}</span>
|
||||
</p>
|
||||
<p css={tw`text-xs mt-2`}>
|
||||
<FontAwesomeIcon icon={faHdd} fixedWidth css={tw`mr-1`}/> {bytesToHuman(stats.disk)}
|
||||
<span css={tw`text-neutral-500`}> / {disklimit}</span>
|
||||
</p>
|
||||
</TitledGreyBox>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServerDetailsBlock;
|
|
@ -142,24 +142,28 @@ export default () => {
|
|||
|
||||
return (
|
||||
<div css={tw`flex flex-wrap mt-4`}>
|
||||
<TitledGreyBox title={'Memory usage'} icon={faMemory} css={tw`md:flex-1 w-full md:w-1/2 md:mr-2`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'memory_chart'} ref={memoryRef} aria-label={'Server Memory Usage Graph'} role={'img'}/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
</p>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
<TitledGreyBox title={'CPU usage'} icon={faMicrochip} css={tw`md:flex-1 w-full md:w-1/2 md:ml-2 mt-4 md:mt-0`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'cpu_chart'} ref={cpuRef} aria-label={'Server CPU Usage Graph'} role={'img'}/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
</p>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
<div css={tw`w-full sm:w-1/2`}>
|
||||
<TitledGreyBox title={'Memory usage'} icon={faMemory} css={tw`mr-0 sm:mr-4`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'memory_chart'} ref={memoryRef} aria-label={'Server Memory Usage Graph'} role={'img'}/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
</p>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
</div>
|
||||
<div css={tw`w-full sm:w-1/2 mt-4 sm:mt-0`}>
|
||||
<TitledGreyBox title={'CPU usage'} icon={faMicrochip} css={tw`ml-0 sm:ml-4`}>
|
||||
{status !== 'offline' ?
|
||||
<canvas id={'cpu_chart'} ref={cpuRef} aria-label={'Server CPU Usage Graph'} role={'img'}/>
|
||||
:
|
||||
<p css={tw`text-xs text-neutral-400 text-center p-3`}>
|
||||
Server is offline.
|
||||
</p>
|
||||
}
|
||||
</TitledGreyBox>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,20 +8,27 @@ import Spinner from '@/components/elements/Spinner';
|
|||
import tw from 'twin.macro';
|
||||
|
||||
export default () => {
|
||||
const server = ServerContext.useStoreState(state => state.server.data);
|
||||
const [ error, setError ] = useState(false);
|
||||
let updatingToken = false;
|
||||
const [ error, setError ] = useState<'connecting' | string>('');
|
||||
const { connected, instance } = ServerContext.useStoreState(state => state.socket);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
|
||||
const setServerStatus = ServerContext.useStoreActions(actions => actions.status.setServerStatus);
|
||||
const { setInstance, setConnectionState } = ServerContext.useStoreActions(actions => actions.socket);
|
||||
|
||||
const updateToken = (uuid: string, socket: Websocket) => {
|
||||
if (updatingToken) return;
|
||||
|
||||
updatingToken = true;
|
||||
getWebsocketToken(uuid)
|
||||
.then(data => socket.setToken(data.token, true))
|
||||
.catch(error => console.error(error));
|
||||
.catch(error => console.error(error))
|
||||
.then(() => {
|
||||
updatingToken = false;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
connected && setError(false);
|
||||
connected && setError('');
|
||||
}, [ connected ]);
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -33,7 +40,7 @@ export default () => {
|
|||
useEffect(() => {
|
||||
// If there is already an instance or there is no server, just exit out of this process
|
||||
// since we don't need to make a new connection.
|
||||
if (instance || !server) {
|
||||
if (instance || !uuid) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -42,7 +49,7 @@ export default () => {
|
|||
socket.on('auth success', () => setConnectionState(true));
|
||||
socket.on('SOCKET_CLOSE', () => setConnectionState(false));
|
||||
socket.on('SOCKET_ERROR', () => {
|
||||
setError(true);
|
||||
setError('connecting');
|
||||
setConnectionState(false);
|
||||
});
|
||||
socket.on('status', (status) => setServerStatus(status));
|
||||
|
@ -51,10 +58,20 @@ export default () => {
|
|||
console.warn('Got error message from daemon socket:', message);
|
||||
});
|
||||
|
||||
socket.on('token expiring', () => updateToken(server.uuid, socket));
|
||||
socket.on('token expired', () => updateToken(server.uuid, socket));
|
||||
socket.on('token expiring', () => updateToken(uuid, socket));
|
||||
socket.on('token expired', () => updateToken(uuid, socket));
|
||||
socket.on('jwt error', (error: string) => {
|
||||
setConnectionState(false);
|
||||
console.warn('JWT validation error from wings:', error);
|
||||
|
||||
getWebsocketToken(server.uuid)
|
||||
if (error === 'jwt: exp claim is invalid') {
|
||||
updateToken(uuid, socket);
|
||||
} else {
|
||||
setError('There was an error validating the credentials provided for the websocket. Please refresh the page.');
|
||||
}
|
||||
});
|
||||
|
||||
getWebsocketToken(uuid)
|
||||
.then(data => {
|
||||
// Connect and then set the authentication token.
|
||||
socket.setToken(data.token).connect(data.socket);
|
||||
|
@ -63,17 +80,25 @@ export default () => {
|
|||
setInstance(socket);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
}, [ server ]);
|
||||
}, [ uuid ]);
|
||||
|
||||
return (
|
||||
error ?
|
||||
<CSSTransition timeout={150} in appear classNames={'fade'}>
|
||||
<div css={tw`bg-red-500 py-2`}>
|
||||
<ContentContainer css={tw`flex items-center justify-center`}>
|
||||
<Spinner size={'small'}/>
|
||||
<p css={tw`ml-2 text-sm text-red-100`}>
|
||||
We're having some trouble connecting to your server, please wait...
|
||||
</p>
|
||||
{error === 'connecting' ?
|
||||
<>
|
||||
<Spinner size={'small'}/>
|
||||
<p css={tw`ml-2 text-sm text-red-100`}>
|
||||
We're having some trouble connecting to your server, please wait...
|
||||
</p>
|
||||
</>
|
||||
:
|
||||
<p css={tw`ml-2 text-sm text-red-100`}>
|
||||
{error}
|
||||
</p>
|
||||
}
|
||||
</ContentContainer>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
|
|
|
@ -40,31 +40,35 @@ export default ({ backup, className }: Props) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<GreyRowBox css={tw`flex items-center`} className={className}>
|
||||
<div css={tw`mr-4`}>
|
||||
{backup.completedAt ?
|
||||
<FontAwesomeIcon icon={faArchive} css={tw`text-neutral-300 hidden md:block`}/>
|
||||
:
|
||||
<Spinner size={'small'}/>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`flex-1`}>
|
||||
<p css={tw`text-sm mb-1`}>
|
||||
{!backup.isSuccessful &&
|
||||
<span css={tw`bg-red-500 py-px px-2 rounded-full text-white text-xs uppercase border border-red-600 mr-2`}>
|
||||
Failed
|
||||
</span>
|
||||
<GreyRowBox css={tw`flex-wrap md:flex-no-wrap items-center`} className={className}>
|
||||
<div css={tw`flex items-center truncate w-full md:flex-1`}>
|
||||
<div css={tw`mr-4`}>
|
||||
{backup.completedAt ?
|
||||
<FontAwesomeIcon icon={faArchive} css={tw`text-neutral-300`}/>
|
||||
:
|
||||
<Spinner size={'small'}/>
|
||||
}
|
||||
{backup.name}
|
||||
{(backup.completedAt && backup.isSuccessful) &&
|
||||
<span css={tw`ml-3 text-neutral-300 text-xs font-thin hidden sm:inline`}>{bytesToHuman(backup.bytes)}</span>
|
||||
}
|
||||
</p>
|
||||
<p css={tw`text-xs text-neutral-400 font-mono hidden md:block`}>
|
||||
{backup.uuid}
|
||||
</p>
|
||||
</div>
|
||||
<div css={tw`flex flex-col truncate`}>
|
||||
<div css={tw`flex items-center text-sm mb-1`}>
|
||||
{!backup.isSuccessful &&
|
||||
<span css={tw`bg-red-500 py-px px-2 rounded-full text-white text-xs uppercase border border-red-600 mr-2`}>
|
||||
Failed
|
||||
</span>
|
||||
}
|
||||
<p css={tw`break-words truncate`}>
|
||||
{backup.name}
|
||||
</p>
|
||||
{(backup.completedAt && backup.isSuccessful) &&
|
||||
<span css={tw`ml-3 text-neutral-300 text-xs font-thin hidden sm:inline`}>{bytesToHuman(backup.bytes)}</span>
|
||||
}
|
||||
</div>
|
||||
<p css={tw`mt-1 md:mt-0 text-xs text-neutral-400 font-mono truncate`}>
|
||||
{backup.uuid}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`ml-8 text-center`}>
|
||||
<div css={tw`flex-1 md:flex-none md:w-48 mt-4 md:mt-0 md:ml-8 md:text-center`}>
|
||||
<p
|
||||
title={format(backup.createdAt, 'ddd, MMMM do, yyyy HH:mm:ss')}
|
||||
css={tw`text-sm`}
|
||||
|
@ -74,7 +78,7 @@ export default ({ backup, className }: Props) => {
|
|||
<p css={tw`text-2xs text-neutral-500 uppercase mt-1`}>Created</p>
|
||||
</div>
|
||||
<Can action={'backup.download'}>
|
||||
<div css={tw`ml-6`} style={{ marginRight: '-0.5rem' }}>
|
||||
<div css={tw`mt-4 md:mt-0 ml-6`} style={{ marginRight: '-0.5rem' }}>
|
||||
{!backup.completedAt ?
|
||||
<div css={tw`p-2 invisible`}>
|
||||
<FontAwesomeIcon icon={faEllipsisH}/>
|
||||
|
|
|
@ -87,14 +87,14 @@ export default () => {
|
|||
onSubmit={submit}
|
||||
initialValues={{ name: '', ignored: '' }}
|
||||
validationSchema={object().shape({
|
||||
name: string().max(255),
|
||||
name: string().max(191),
|
||||
ignored: string(),
|
||||
})}
|
||||
>
|
||||
<ModalContent appear visible={visible} onDismissed={() => setVisible(false)}/>
|
||||
</Formik>
|
||||
}
|
||||
<Button onClick={() => setVisible(true)}>
|
||||
<Button css={tw`w-full sm:w-auto`} onClick={() => setVisible(true)}>
|
||||
Create backup
|
||||
</Button>
|
||||
</>
|
||||
|
|
|
@ -32,6 +32,7 @@ export default () => {
|
|||
|
||||
const id = ServerContext.useStoreState(state => state.server.data!.id);
|
||||
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
|
||||
const setDirectory = ServerContext.useStoreActions(actions => actions.files.setDirectory);
|
||||
const { addError, clearFlashes } = useFlash();
|
||||
|
||||
let fetchFileContent: null | (() => Promise<string>) = null;
|
||||
|
@ -39,8 +40,9 @@ export default () => {
|
|||
useEffect(() => {
|
||||
if (action === 'new') return;
|
||||
|
||||
setLoading(true);
|
||||
setError('');
|
||||
setLoading(true);
|
||||
setDirectory(hash.replace(/^#/, '').split('/').filter(v => !!v).slice(0, -1).join('/'));
|
||||
getFileContents(uuid, hash.replace(/^#/, ''))
|
||||
.then(setContent)
|
||||
.catch(error => {
|
||||
|
@ -116,7 +118,13 @@ export default () => {
|
|||
fetchContent={value => {
|
||||
fetchFileContent = value;
|
||||
}}
|
||||
onContentSaved={save}
|
||||
onContentSaved={() => {
|
||||
if (action !== 'edit') {
|
||||
setModalVisible(true);
|
||||
} else {
|
||||
save();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`flex justify-end mt-4`}>
|
||||
|
|
|
@ -45,13 +45,15 @@ export default ({ withinFileEditor, isNewFile }: Props) => {
|
|||
|
||||
return (
|
||||
<div css={tw`flex items-center text-sm mb-4 text-neutral-500`}>
|
||||
{(files && files.length > 0 && !params?.action) &&
|
||||
<FileActionCheckbox
|
||||
type={'checkbox'}
|
||||
css={tw`mx-4`}
|
||||
checked={selectedFilesLength === (files ? files.length : -1)}
|
||||
onChange={onSelectAllClick}
|
||||
/>
|
||||
{(files && files.length > 0 && !params?.action) ?
|
||||
<FileActionCheckbox
|
||||
type={'checkbox'}
|
||||
css={tw`mx-4`}
|
||||
checked={selectedFilesLength === (files ? files.length : -1)}
|
||||
onChange={onSelectAllClick}
|
||||
/>
|
||||
:
|
||||
<div css={tw`w-12`}/>
|
||||
}
|
||||
/<span css={tw`px-1 text-neutral-300`}>home</span>/
|
||||
<NavLink
|
||||
|
|
|
@ -38,13 +38,13 @@ const Clickable: React.FC<{ file: FileObject }> = memo(({ file, children }) => {
|
|||
|
||||
return (
|
||||
(!canReadContents || (file.isFile && !file.isEditable())) ?
|
||||
<div css={tw`flex flex-1 text-neutral-300 no-underline p-3 cursor-default`}>
|
||||
<div css={tw`flex flex-1 text-neutral-300 no-underline p-3 cursor-default overflow-hidden truncate`}>
|
||||
{children}
|
||||
</div>
|
||||
:
|
||||
<NavLink
|
||||
to={`${match.url}/${file.isFile ? 'edit/' : ''}#${cleanDirectoryPath(`${directory}/${file.name}`)}`}
|
||||
css={tw`flex flex-1 text-neutral-300 no-underline p-3`}
|
||||
css={tw`flex flex-1 text-neutral-300 no-underline p-3 overflow-hidden truncate`}
|
||||
onClick={onRowClick}
|
||||
>
|
||||
{children}
|
||||
|
@ -69,7 +69,7 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
|
|||
<FontAwesomeIcon icon={faFolder}/>
|
||||
}
|
||||
</div>
|
||||
<div css={tw`flex-1`}>
|
||||
<div css={tw`flex-1 truncate`}>
|
||||
{file.name}
|
||||
</div>
|
||||
{file.isFile &&
|
||||
|
@ -92,4 +92,11 @@ const FileObjectRow = ({ file }: { file: FileObject }) => (
|
|||
</Row>
|
||||
);
|
||||
|
||||
export default memo(FileObjectRow, isEqual);
|
||||
export default memo(FileObjectRow, (prevProps, nextProps) => {
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
const { isArchiveType, isEditable, ...prevFile } = prevProps.file;
|
||||
const { isArchiveType: nextIsArchiveType, isEditable: nextIsEditable, ...nextFile } = nextProps.file;
|
||||
/* eslint-enable @typescript-eslint/no-unused-vars */
|
||||
|
||||
return isEqual(prevFile, nextFile);
|
||||
});
|
||||
|
|
|
@ -24,7 +24,7 @@ const schema = object().shape({
|
|||
|
||||
const generateDirectoryData = (name: string): FileObject => ({
|
||||
key: `dir_${name.split('/', 1)[0] ?? name}`,
|
||||
name: name.split('/', 1)[0] ?? name,
|
||||
name: name.replace(/^(\/*)/, '').split('/', 1)[0] ?? name,
|
||||
mode: '0644',
|
||||
size: 0,
|
||||
isFile: false,
|
||||
|
@ -88,7 +88,7 @@ export default ({ className }: WithClassname) => {
|
|||
name={'directoryName'}
|
||||
label={'Directory Name'}
|
||||
/>
|
||||
<p css={tw`text-xs mt-2 text-neutral-400`}>
|
||||
<p css={tw`text-xs mt-2 text-neutral-400 break-all`}>
|
||||
<span css={tw`text-neutral-200`}>This directory will be created as</span>
|
||||
/home/container/
|
||||
<span css={tw`text-cyan-200`}>
|
||||
|
|
|
@ -15,7 +15,7 @@ import setServerAllocationNotes from '@/api/server/network/setServerAllocationNo
|
|||
import useFlash from '@/plugins/useFlash';
|
||||
import { ServerContext } from '@/state/server';
|
||||
|
||||
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm block`}`;
|
||||
const Code = styled.code`${tw`font-mono py-1 px-2 bg-neutral-900 rounded text-sm inline-block`}`;
|
||||
const Label = styled.label`${tw`uppercase text-xs mt-1 text-neutral-400 block px-1 select-none transition-colors duration-150`}`;
|
||||
|
||||
interface Props {
|
||||
|
@ -40,22 +40,21 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
|
|||
}, 750);
|
||||
|
||||
return (
|
||||
<GreyRowBox
|
||||
$hoverable={false}
|
||||
css={tw`mt-2 overflow-x-auto`}
|
||||
>
|
||||
<div css={tw`hidden md:block pl-4 pr-6 text-neutral-400`}>
|
||||
<FontAwesomeIcon icon={faNetworkWired}/>
|
||||
<GreyRowBox $hoverable={false} css={tw`flex-wrap md:flex-no-wrap mt-2`}>
|
||||
<div css={tw`flex items-center w-full md:w-auto`}>
|
||||
<div css={tw`pl-4 pr-6 text-neutral-400`}>
|
||||
<FontAwesomeIcon icon={faNetworkWired}/>
|
||||
</div>
|
||||
<div css={tw`mr-4 flex-1 md:w-40`}>
|
||||
<Code>{allocation.alias || allocation.ip}</Code>
|
||||
<Label>IP Address</Label>
|
||||
</div>
|
||||
<div css={tw`w-16 md:w-24 overflow-hidden`}>
|
||||
<Code>{allocation.port}</Code>
|
||||
<Label>Port</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div css={tw`mr-4`}>
|
||||
<Code>{allocation.alias || allocation.ip}</Code>
|
||||
<Label>IP Address</Label>
|
||||
</div>
|
||||
<div>
|
||||
<Code>{allocation.port}</Code>
|
||||
<Label>Port</Label>
|
||||
</div>
|
||||
<div css={tw`px-8 flex-none sm:flex-1 self-start`}>
|
||||
<div css={tw`mt-4 w-full md:mt-0 md:flex-1 md:w-auto`}>
|
||||
<InputSpinner visible={loading}>
|
||||
<Textarea
|
||||
css={tw`bg-neutral-800 hover:border-neutral-600 border-transparent`}
|
||||
|
@ -65,7 +64,7 @@ const AllocationRow = ({ allocation, onSetPrimary, onNotesChanged }: Props) => {
|
|||
/>
|
||||
</InputSpinner>
|
||||
</div>
|
||||
<div css={tw`w-32 text-right pr-4 sm:pr-0`}>
|
||||
<div css={tw`w-full md:flex-none md:w-32 md:text-center mt-4 md:mt-0 text-right ml-4`}>
|
||||
{allocation.isDefault ?
|
||||
<span css={tw`bg-green-500 py-1 px-2 rounded text-green-50 text-xs`}>Primary</span>
|
||||
:
|
||||
|
|
|
@ -81,7 +81,7 @@ export default ({ schedule, task }: Props) => {
|
|||
<div css={tw`md:ml-6 mt-2`}>
|
||||
{task.action === 'backup' &&
|
||||
<p css={tw`text-xs uppercase text-neutral-400 mb-1`}>Ignoring files & folders:</p>}
|
||||
<div css={tw`font-mono bg-neutral-800 rounded py-1 px-2 text-sm w-auto whitespace-pre inline-block break-all`}>
|
||||
<div css={tw`font-mono bg-neutral-800 rounded py-1 px-2 text-sm w-auto inline-block whitespace-pre-wrap break-all`}>
|
||||
{task.payload}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -250,7 +250,10 @@ export default ({ subuser, ...props }: Props) => {
|
|||
permissions: subuser?.permissions || [],
|
||||
} as Values}
|
||||
validationSchema={object().shape({
|
||||
email: string().email('A valid email address must be provided.').required('A valid email address must be provided.'),
|
||||
email: string()
|
||||
.max(191, 'Email addresses must not exceed 191 characters.')
|
||||
.email('A valid email address must be provided.')
|
||||
.required('A valid email address must be provided.'),
|
||||
permissions: array().of(string()),
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -30,8 +30,8 @@ export default ({ subuser }: Props) => {
|
|||
<div css={tw`w-10 h-10 rounded-full bg-white border-2 border-neutral-800 overflow-hidden hidden md:block`}>
|
||||
<img css={tw`w-full h-full`} src={`${subuser.image}?s=400`}/>
|
||||
</div>
|
||||
<div css={tw`ml-4 flex-1`}>
|
||||
<p css={tw`text-sm break-all`}>{subuser.email}</p>
|
||||
<div css={tw`ml-4 flex-1 overflow-hidden`}>
|
||||
<p css={tw`text-sm truncate`}>{subuser.email}</p>
|
||||
</div>
|
||||
<div css={tw`ml-4`}>
|
||||
<p css={tw`font-medium text-center`}>
|
||||
|
|
|
@ -30,7 +30,7 @@ import StartupContainer from '@/components/server/startup/StartupContainer';
|
|||
import requireServerPermission from '@/hoc/requireServerPermission';
|
||||
|
||||
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
|
||||
const { rootAdmin } = useStoreState(state => state.user.data!);
|
||||
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
|
||||
const [ error, setError ] = useState('');
|
||||
const [ installing, setInstalling ] = useState(false);
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
<div class="col-md-12">
|
||||
<label for="pLongModal" class="form-label">Description</label>
|
||||
<textarea name="long" id="pLongModal" class="form-control" rows="4"></textarea>
|
||||
<p class="text-muted small">A longer description of this location. Must be less than 255 characters.</p>
|
||||
<p class="text-muted small">A longer description of this location. Must be less than 191 characters.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -84,7 +84,7 @@
|
|||
<div class="col-md-12">
|
||||
<label for="pDescription" class="form-label">Description</label>
|
||||
<textarea id="pDescription" name="description" class="form-control" rows="4"></textarea>
|
||||
<p class="text-muted small">A longer description for this mount, must be less than 255 characters.</p>
|
||||
<p class="text-muted small">A longer description for this mount, must be less than 191 characters.</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
|
|
|
@ -158,7 +158,7 @@
|
|||
</div>
|
||||
|
||||
<div class="form-group col-xs-6">
|
||||
<label for="pThreads">CPU Threads</label>
|
||||
<label for="pThreads">CPU Pinning</label>
|
||||
|
||||
<div>
|
||||
<input type="text" id="pThreads" name="threads" class="form-control" value="{{ old('threads') }}" />
|
||||
|
@ -327,7 +327,7 @@
|
|||
// END Persist 'Service Variables'
|
||||
</script>
|
||||
|
||||
{!! Theme::js('js/admin/new-server.js?v=20200913') !!}
|
||||
{!! Theme::js('js/admin/new-server.js?v=20201003') !!}
|
||||
|
||||
<script type="application/javascript">
|
||||
$(document).ready(function() {
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
<p class="text-muted small">Each <em>physical</em> core on the system is considered to be <code>100%</code>. Setting this value to <code>0</code> will allow a server to use CPU time without restrictions.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="threads" class="control-label">CPU Threads</label>
|
||||
<label for="threads" class="control-label">CPU Pinning</label>
|
||||
<div>
|
||||
<input type="text" name="threads" class="form-control" value="{{ old('threads', $server->threads) }}"/>
|
||||
</div>
|
||||
|
|
|
@ -66,6 +66,12 @@
|
|||
@section('footer-scripts')
|
||||
@parent
|
||||
<script>
|
||||
function escapeHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.appendChild(document.createTextNode(str));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
$('#pUserId').select2({
|
||||
ajax: {
|
||||
url: '/admin/users/accounts.json',
|
||||
|
@ -85,14 +91,14 @@
|
|||
escapeMarkup: function (markup) { return markup; },
|
||||
minimumInputLength: 2,
|
||||
templateResult: function (data) {
|
||||
if (data.loading) return data.text;
|
||||
if (data.loading) return escapeHtml(data.text);
|
||||
|
||||
return '<div class="user-block"> \
|
||||
<img class="img-circle img-bordered-xs" src="https://www.gravatar.com/avatar/' + data.md5 + '?s=120" alt="User Image"> \
|
||||
<img class="img-circle img-bordered-xs" src="https://www.gravatar.com/avatar/' + escapeHtml(data.md5) + '?s=120" alt="User Image"> \
|
||||
<span class="username"> \
|
||||
<a href="#">' + data.name_first + ' ' + data.name_last +'</a> \
|
||||
<a href="#">' + escapeHtml(data.name_first) + ' ' + escapeHtml(data.name_last) +'</a> \
|
||||
</span> \
|
||||
<span class="description"><strong>' + data.email + '</strong> - ' + data.username + '</span> \
|
||||
<span class="description"><strong>' + escapeHtml(data.email) + '</strong> - ' + escapeHtml(data.username) + '</span> \
|
||||
</div>';
|
||||
},
|
||||
templateSelection: function (data) {
|
||||
|
@ -108,10 +114,10 @@
|
|||
|
||||
return '<div> \
|
||||
<span> \
|
||||
<img class="img-rounded img-bordered-xs" src="https://www.gravatar.com/avatar/' + data.md5 + '?s=120" style="height:28px;margin-top:-4px;" alt="User Image"> \
|
||||
<img class="img-rounded img-bordered-xs" src="https://www.gravatar.com/avatar/' + escapeHtml(data.md5) + '?s=120" style="height:28px;margin-top:-4px;" alt="User Image"> \
|
||||
</span> \
|
||||
<span style="padding-left:5px;"> \
|
||||
' + data.name_first + ' ' + data.name_last + ' (<strong>' + data.email + '</strong>) \
|
||||
' + escapeHtml(data.name_first) + ' ' + escapeHtml(data.name_last) + ' (<strong>' + escapeHtml(data.email) + '</strong>) \
|
||||
</span> \
|
||||
</div>';
|
||||
}
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>CPU Threads</td>
|
||||
<td>CPU Pinning</td>
|
||||
<td>
|
||||
@if($server->threads != null)
|
||||
<code>{{ $server->threads }}</code>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue