Get server listing design working mostly correct

This commit is contained in:
Dane Everitt 2020-07-03 14:50:37 -07:00
parent 94e3acb9c4
commit d93e804a31
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
9 changed files with 170 additions and 188 deletions

View file

@ -0,0 +1,20 @@
import tw from 'twin.macro';
import { createGlobalStyle } from 'styled-components/macro';
export default createGlobalStyle`
@import url('//fonts.googleapis.com/css?family=Rubik:300,400,500&display=swap');
@import url('//fonts.googleapis.com/css?family=IBM+Plex+Mono|IBM+Plex+Sans:500&display=swap');
body {
${tw`font-sans bg-neutral-800 text-neutral-200`};
letter-spacing: 0.015em;
}
h1, h2, h3, h4, h5, h6 {
${tw`font-medium tracking-normal font-header`};
}
p {
${tw`text-neutral-200 leading-snug font-sans`};
}
`;

View file

@ -11,6 +11,7 @@ import { SiteSettings } from '@/state/settings';
import ProgressBar from '@/components/elements/ProgressBar'; import ProgressBar from '@/components/elements/ProgressBar';
import NotFound from '@/components/screens/NotFound'; import NotFound from '@/components/screens/NotFound';
import tw from 'twin.macro'; import tw from 'twin.macro';
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
interface ExtendedWindow extends Window { interface ExtendedWindow extends Window {
SiteConfiguration?: SiteSettings; SiteConfiguration?: SiteSettings;
@ -48,21 +49,24 @@ const App = () => {
} }
return ( return (
<StoreProvider store={store}> <>
<Provider store={store}> <GlobalStylesheet/>
<ProgressBar/> <StoreProvider store={store}>
<div css={tw`mx-auto w-auto`}> <Provider store={store}>
<BrowserRouter basename={'/'} key={'root-router'}> <ProgressBar/>
<Switch> <div css={tw`mx-auto w-auto`}>
<Route path="/server/:id" component={ServerRouter}/> <BrowserRouter basename={'/'} key={'root-router'}>
<Route path="/auth" component={AuthenticationRouter}/> <Switch>
<Route path="/" component={DashboardRouter}/> <Route path="/server/:id" component={ServerRouter}/>
<Route path={'*'} component={NotFound}/> <Route path="/auth" component={AuthenticationRouter}/>
</Switch> <Route path="/" component={DashboardRouter}/>
</BrowserRouter> <Route path={'*'} component={NotFound}/>
</div> </Switch>
</Provider> </BrowserRouter>
</StoreProvider> </div>
</Provider>
</StoreProvider>
</>
); );
}; };

View file

@ -1,51 +1,76 @@
import * as React from 'react'; import * as React from 'react';
import { Link, NavLink } from 'react-router-dom'; import { Link, NavLink } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLayerGroup } from '@fortawesome/free-solid-svg-icons/faLayerGroup'; import { faCogs, faLayerGroup, faSignOutAlt, faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons/faUserCircle';
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt';
import { faSwatchbook } from '@fortawesome/free-solid-svg-icons/faSwatchbook';
import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs';
import { useStoreState } from 'easy-peasy'; import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
import SearchContainer from '@/components/dashboard/search/SearchContainer'; import SearchContainer from '@/components/dashboard/search/SearchContainer';
import tw from 'twin.macro';
import styled from 'styled-components/macro';
import * as config from '@/../../tailwind.config.js';
const Navigation = styled.div`
${tw`w-full bg-neutral-900 shadow-md`};
& > div {
${tw`mx-auto w-full flex items-center`};
}
& #logo {
${tw`flex-1`};
& > a {
${tw`text-2xl font-header px-4 no-underline text-neutral-200 hover:text-neutral-100 transition-colors duration-150`};
}
}
`;
const RightNavigation = styled.div`
${tw`flex h-full items-center justify-center`};
& > a, & > .navigation-link {
${tw`flex items-center h-full no-underline text-neutral-300 px-6 cursor-pointer transition-all duration-150`};
&:active, &:hover {
${tw`text-neutral-100 bg-black`};
}
&:active, &:hover, &.active {
box-shadow: inset 0 -2px ${config.theme.colors.cyan['700']};
}
}
`;
export default () => { export default () => {
const user = useStoreState((state: ApplicationStore) => state.user.data!); const user = useStoreState((state: ApplicationStore) => state.user.data!);
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name); const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
return ( return (
<div id={'navigation'}> <Navigation>
<div className={'mx-auto w-full flex items-center'} style={{ maxWidth: '1200px', height: '3.5rem' }}> <div css={tw`mx-auto w-full flex items-center`} style={{ maxWidth: '1200px', height: '3.5rem' }}>
<div id={'logo'}> <div id={'logo'}>
<Link to={'/'}> <Link to={'/'}>
{name} {name}
</Link> </Link>
</div> </div>
<div className={'right-navigation'}> <RightNavigation>
<SearchContainer/> <SearchContainer/>
<NavLink to={'/'} exact={true}> <NavLink to={'/'} exact>
<FontAwesomeIcon icon={faLayerGroup}/> <FontAwesomeIcon icon={faLayerGroup}/>
</NavLink> </NavLink>
<NavLink to={'/account'}> <NavLink to={'/account'}>
<FontAwesomeIcon icon={faUserCircle}/> <FontAwesomeIcon icon={faUserCircle}/>
</NavLink> </NavLink>
{user.rootAdmin && {user.rootAdmin &&
<a href={'/admin'} target={'_blank'}> <a href={'/admin'} target={'_blank'} rel={'noreferrer'}>
<FontAwesomeIcon icon={faCogs}/> <FontAwesomeIcon icon={faCogs}/>
</a> </a>
} }
{process.env.NODE_ENV !== 'production' &&
<NavLink to={'/design'}>
<FontAwesomeIcon icon={faSwatchbook}/>
</NavLink>
}
<a href={'/auth/logout'}> <a href={'/auth/logout'}>
<FontAwesomeIcon icon={faSignOutAlt}/> <FontAwesomeIcon icon={faSignOutAlt}/>
</a> </a>
</div> </RightNavigation>
</div> </div>
</div> </Navigation>
); );
}; };

View file

@ -10,6 +10,7 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import { useStoreState } from 'easy-peasy'; import { useStoreState } from 'easy-peasy';
import { usePersistedState } from '@/plugins/usePersistedState'; import { usePersistedState } from '@/plugins/usePersistedState';
import Switch from '@/components/elements/Switch'; import Switch from '@/components/elements/Switch';
import tw from 'twin.macro';
export default () => { export default () => {
const { addError, clearFlashes } = useFlash(); const { addError, clearFlashes } = useFlash();
@ -37,10 +38,10 @@ export default () => {
return ( return (
<PageContentBlock> <PageContentBlock>
<FlashMessageRender className={'mb-4'}/> <FlashMessageRender css={tw`mb-4`}/>
{rootAdmin && {rootAdmin &&
<div className={'mb-2 flex justify-end items-center'}> <div css={tw`mb-2 flex justify-end items-center`}>
<p className={'uppercase text-xs text-neutral-400 mr-2'}> <p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
{showAdmin ? 'Showing all servers' : 'Showing your servers'} {showAdmin ? 'Showing all servers' : 'Showing your servers'}
</p> </p>
<Switch <Switch
@ -51,14 +52,16 @@ export default () => {
</div> </div>
} }
{loading ? {loading ?
<Spinner centered={true} size={'large'}/> <Spinner centered size={'large'}/>
: :
servers.length > 0 ? servers.length > 0 ?
servers.map(server => ( servers.map((server, index) => (
<ServerRow key={server.uuid} server={server} className={'mt-2'}/> <div key={server.uuid} css={index > 0 ? tw`mt-2` : undefined}>
<ServerRow server={server}/>
</div>
)) ))
: :
<p className={'text-center text-sm text-neutral-400'}> <p css={tw`text-center text-sm text-neutral-400`}>
There are no servers associated with your account. There are no servers associated with your account.
</p> </p>
} }

View file

@ -1,82 +0,0 @@
import * as React from 'react';
import { Link } from 'react-router-dom';
import ContentBox from '@/components/elements/ContentBox';
export default class DesignElementsContainer extends React.PureComponent {
render () {
return (
<React.Fragment>
<div className={'my-10'}>
<div className={'flex'}>
<ContentBox
className={'flex-1 mr-4'}
title={'A Special Announcement'}
borderColor={'border-primary-400'}
>
<p className={'text-neutral-200 text-sm'}>
Your demands have been received: Dark Mode will be default in Pterodactyl 0.8!
</p>
<p><Link to={'/'}>Back</Link></p>
</ContentBox>
<div className={'ml-4 flex-1'}>
<h2 className={'text-neutral-300 mb-2 px-4'}>Form Elements</h2>
<div className={'bg-neutral-700 p-4 rounded shadow-lg border-t-4 border-primary-400'}>
<label className={'uppercase text-neutral-200'}>Email</label>
<input type={'text'} className={'input-dark'}/>
<p className={'input-help'}>
This is some descriptive helper text to explain how things work.
</p>
<div className={'mt-6'}/>
<label className={'uppercase text-neutral-200'}>Username</label>
<input type={'text'} className={'input-dark error'}/>
<p className={'input-help'}>
This field has an error.
</p>
<div className={'mt-6'}/>
<label className={'uppercase text-neutral-200'}>Disabled Field</label>
<input type={'text'} className={'input-dark'} disabled={true}/>
<div className={'mt-6'}/>
<label className={'uppercase text-neutral-200'}>Select</label>
<select className={'input-dark'}>
<option>Option 1</option>
<option>Option 2</option>
<option>Option 3</option>
</select>
<div className={'mt-6'}/>
<label className={'uppercase text-neutral-200'}>Textarea</label>
<textarea className={'input-dark h-32'}></textarea>
<div className={'mt-6'}/>
<button className={'btn btn-primary btn-sm'}>
Blue
</button>
<button className={'btn btn-grey btn-sm ml-2'}>
Grey
</button>
<button className={'btn btn-green btn-sm ml-2'}>
Green
</button>
<button className={'btn btn-red btn-sm ml-2'}>
Red
</button>
<div className={'mt-6'}/>
<button className={'btn btn-secondary btn-sm'}>
Secondary
</button>
<button className={'btn btn-secondary btn-red btn-sm ml-2'}>
Secondary Danger
</button>
<div className={'mt-6'}/>
<button className={'btn btn-primary btn-lg'}>
Large
</button>
<button className={'btn btn-primary btn-xs ml-2'}>
Tiny
</button>
</div>
</div>
</div>
</div>
</React.Fragment>
);
}
}

View file

@ -11,6 +11,8 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage'; import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
import { bytesToHuman } from '@/helpers'; import { bytesToHuman } from '@/helpers';
import classNames from 'classnames'; import classNames from 'classnames';
import tw from 'twin.macro';
import GreyRowBox from '@/components/elements/GreyRowBox';
// Determines if the current value is in an alarm threshold so we can show it in red rather // Determines if the current value is in an alarm threshold so we can show it in red rather
// than the more faded default style. // than the more faded default style.
@ -20,7 +22,7 @@ const isAlarmState = (current: number, limit: number): boolean => {
return current / limitInBytes >= 0.90; return current / limitInBytes >= 0.90;
}; };
export default ({ server, className }: { server: Server; className: string | undefined }) => { export default ({ server }: { server: Server }) => {
const interval = useRef<number>(null); const interval = useRef<number>(null);
const [ stats, setStats ] = useState<ServerStats | null>(null); const [ stats, setStats ] = useState<ServerStats | null>(null);
const [ statsError, setStatsError ] = useState(false); const [ statsError, setStatsError ] = useState(false);
@ -37,7 +39,6 @@ export default ({ server, className }: { server: Server; className: string | und
useEffect(() => { useEffect(() => {
getStats().then(() => { getStats().then(() => {
// @ts-ignore
interval.current = setInterval(() => getStats(), 20000); interval.current = setInterval(() => getStats(), 20000);
}); });
@ -52,21 +53,21 @@ export default ({ server, className }: { server: Server; className: string | und
alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory); alarms.memory = isAlarmState(stats.memoryUsageInBytes, server.limits.memory);
alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk); alarms.disk = server.limits.disk === 0 ? false : isAlarmState(stats.diskUsageInBytes, server.limits.disk);
} }
const disklimit = server.limits.disk != 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : "Unlimited"; const disklimit = server.limits.disk !== 0 ? bytesToHuman(server.limits.disk * 1000 * 1000) : 'Unlimited';
const memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : "Unlimited"; const memorylimit = server.limits.memory !== 0 ? bytesToHuman(server.limits.memory * 1000 * 1000) : 'Unlimited';
return ( return (
<Link to={`/server/${server.id}`} className={`grey-row-box cursor-pointer ${className}`}> <GreyRowBox as={Link} to={`/server/${server.id}`}>
<div className={'icon'}> <div className={'icon'}>
<FontAwesomeIcon icon={faServer}/> <FontAwesomeIcon icon={faServer}/>
</div> </div>
<div className={'flex-1 ml-4'}> <div css={tw`flex-1 ml-4`}>
<p className={'text-lg'}>{server.name}</p> <p css={tw`text-lg`}>{server.name}</p>
</div> </div>
<div className={'w-1/4 overflow-hidden'}> <div css={tw`w-1/4 overflow-hidden`}>
<div className={'flex ml-4'}> <div css={tw`flex ml-4`}>
<FontAwesomeIcon icon={faEthernet} className={'text-neutral-500'}/> <FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
<p className={'text-sm text-neutral-400 ml-2'}> <p css={tw`text-sm text-neutral-400 ml-2`}>
{ {
server.allocations.filter(alloc => alloc.default).map(allocation => ( server.allocations.filter(alloc => alloc.default).map(allocation => (
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span> <span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
@ -75,85 +76,88 @@ export default ({ server, className }: { server: Server; className: string | und
</p> </p>
</div> </div>
</div> </div>
<div className={'w-1/3 flex items-baseline relative'}> <div css={tw`w-1/3 flex items-baseline relative`}>
{!stats ? {!stats ?
!statsError ? !statsError ?
<SpinnerOverlay size={'tiny'} visible={true} backgroundOpacity={0.25}/> <SpinnerOverlay size={'tiny'} visible={true} backgroundOpacity={0.25}/>
: :
server.isInstalling ? server.isInstalling ?
<div className={'flex-1 text-center'}> <div css={tw`flex-1 text-center`}>
<span className={'bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs'}> <span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
Installing Installing
</span> </span>
</div> </div>
: :
<div className={'flex-1 text-center'}> <div css={tw`flex-1 text-center`}>
<span className={'bg-red-500 rounded px-2 py-1 text-red-100 text-xs'}> <span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
{server.isSuspended ? 'Suspended' : 'Connection Error'} {server.isSuspended ? 'Suspended' : 'Connection Error'}
</span> </span>
</div> </div>
: :
<React.Fragment> <React.Fragment>
<div className={'flex-1 flex ml-4 justify-center'}> <div css={tw`flex-1 flex ml-4 justify-center`}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faMicrochip} icon={faMicrochip}
className={classNames({ css={[
'text-neutral-500': !alarms.cpu, !alarms.cpu && tw`text-neutral-500`,
'text-red-400': alarms.cpu, alarms.cpu && tw`text-red-400`,
})} ]}
/> />
<p <p
className={classNames('text-sm ml-2', { css={[
'text-neutral-400': !alarms.cpu, tw`text-sm ml-2`,
'text-white': alarms.cpu, !alarms.cpu && tw`text-neutral-400`,
})} alarms.cpu && tw`text-white`,
]}
> >
{stats.cpuUsagePercent} % {stats.cpuUsagePercent} %
</p> </p>
</div> </div>
<div className={'flex-1 ml-4'}> <div css={tw`flex-1 ml-4`}>
<div className={'flex justify-center'}> <div css={tw`flex justify-center`}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faMemory} icon={faMemory}
className={classNames({ css={[
'text-neutral-500': !alarms.memory, !alarms.memory && tw`text-neutral-500`,
'text-red-400': alarms.memory, alarms.memory && tw`text-red-400`,
})} ]}
/> />
<p <p
className={classNames('text-sm ml-2', { css={[
'text-neutral-400': !alarms.memory, tw`text-sm ml-2`,
'text-white': alarms.memory, !alarms.memory && tw`text-neutral-400`,
})} alarms.memory && tw`text-white`,
]}
> >
{bytesToHuman(stats.memoryUsageInBytes)} {bytesToHuman(stats.memoryUsageInBytes)}
</p> </p>
</div> </div>
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {memorylimit}</p> <p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {memorylimit}</p>
</div> </div>
<div className={'flex-1 ml-4'}> <div css={tw`flex-1 ml-4`}>
<div className={'flex justify-center'}> <div css={tw`flex justify-center`}>
<FontAwesomeIcon <FontAwesomeIcon
icon={faHdd} icon={faHdd}
className={classNames({ css={[
'text-neutral-500': !alarms.disk, !alarms.disk && tw`text-neutral-500`,
'text-red-400': alarms.disk, alarms.disk && tw`text-red-400`,
})} ]}
/> />
<p <p
className={classNames('text-sm ml-2', { css={[
'text-neutral-400': !alarms.disk, tw`text-sm ml-2`,
'text-white': alarms.disk, !alarms.disk && tw`text-neutral-400`,
})} alarms.disk && tw`text-white`,
]}
> >
{bytesToHuman(stats.diskUsageInBytes)} {bytesToHuman(stats.diskUsageInBytes)}
</p> </p>
</div> </div>
<p className={'text-xs text-neutral-600 text-center mt-1'}>of {disklimit}</p> <p css={tw`text-xs text-neutral-600 text-center mt-1`}>of {disklimit}</p>
</div> </div>
</React.Fragment> </React.Fragment>
} }
</div> </div>
</Link> </GreyRowBox>
); );
}; };

View file

@ -0,0 +1,14 @@
import styled from 'styled-components/macro';
import tw from 'twin.macro';
export default styled.div`
${tw`flex rounded no-underline text-neutral-200 items-center bg-neutral-700 p-4 border border-transparent transition-colors duration-150`};
&:not(.no-hover):hover {
${tw`border-neutral-500`};
}
& > div.icon {
${tw`rounded-full bg-neutral-500 p-3`};
}
`;

View file

@ -1,26 +1,22 @@
import React from 'react'; import React from 'react';
import ContentContainer from '@/components/elements/ContentContainer'; import ContentContainer from '@/components/elements/ContentContainer';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import tw from 'twin.macro';
interface Props { export default ({ children }): React.FC => (
children: React.ReactNode; <CSSTransition timeout={250} classNames={'fade'} appear in>
className?: string;
}
export default ({ className, children }: Props) => (
<CSSTransition timeout={250} classNames={'fade'} appear={true} in={true}>
<> <>
<ContentContainer className={`my-10 ${className}`}> <ContentContainer css={tw`my-10`}>
{children} {children}
</ContentContainer> </ContentContainer>
<ContentContainer className={'mb-4'}> <ContentContainer css={tw`mb-4`}>
<p className={'text-center text-neutral-500 text-xs'}> <p css={tw`text-center text-neutral-500 text-xs`}>
&copy; 2015 - 2020&nbsp; &copy; 2015 - 2020&nbsp;
<a <a
rel={'noopener nofollow'} rel={'noopener nofollow noreferrer'}
href={'https://pterodactyl.io'} href={'https://pterodactyl.io'}
target={'_blank'} target={'_blank'}
className={'no-underline text-neutral-500 hover:text-neutral-300'} css={tw`no-underline text-neutral-500 hover:text-neutral-300`}
> >
Pterodactyl Software Pterodactyl Software
</a> </a>

View file

@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { NavLink, Route, RouteComponentProps, Switch } from 'react-router-dom';
import DesignElementsContainer from '@/components/dashboard/DesignElementsContainer';
import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer'; import AccountOverviewContainer from '@/components/dashboard/AccountOverviewContainer';
import NavigationBar from '@/components/NavigationBar'; import NavigationBar from '@/components/NavigationBar';
import DashboardContainer from '@/components/dashboard/DashboardContainer'; import DashboardContainer from '@/components/dashboard/DashboardContainer';
@ -9,13 +8,13 @@ import AccountApiContainer from '@/components/dashboard/AccountApiContainer';
import NotFound from '@/components/screens/NotFound'; import NotFound from '@/components/screens/NotFound';
export default ({ location }: RouteComponentProps) => ( export default ({ location }: RouteComponentProps) => (
<React.Fragment> <>
<NavigationBar/> <NavigationBar/>
{location.pathname.startsWith('/account') && {location.pathname.startsWith('/account') &&
<div id={'sub-navigation'}> <div id={'sub-navigation'}>
<div className={'items'}> <div className={'items'}>
<NavLink to={`/account`} exact>Settings</NavLink> <NavLink to={'/account'} exact>Settings</NavLink>
<NavLink to={`/account/api`}>API Credentials</NavLink> <NavLink to={'/account/api'}>API Credentials</NavLink>
</div> </div>
</div> </div>
} }
@ -24,9 +23,8 @@ export default ({ location }: RouteComponentProps) => (
<Route path={'/'} component={DashboardContainer} exact/> <Route path={'/'} component={DashboardContainer} exact/>
<Route path={'/account'} component={AccountOverviewContainer} exact/> <Route path={'/account'} component={AccountOverviewContainer} exact/>
<Route path={'/account/api'} component={AccountApiContainer} exact/> <Route path={'/account/api'} component={AccountApiContainer} exact/>
<Route path={'/design'} component={DesignElementsContainer}/>
<Route path={'*'} component={NotFound}/> <Route path={'*'} component={NotFound}/>
</Switch> </Switch>
</TransitionRouter> </TransitionRouter>
</React.Fragment> </>
); );