1
1
mirror of https://github.com/pterodactyl/panel.git synced 2024-11-23 01:22:30 +01:00

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

View File

@ -1,51 +1,76 @@
import * as React from 'react';
import { Link, NavLink } from 'react-router-dom';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLayerGroup } from '@fortawesome/free-solid-svg-icons/faLayerGroup';
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 { faCogs, faLayerGroup, faSignOutAlt, faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
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 () => {
const user = useStoreState((state: ApplicationStore) => state.user.data!);
const name = useStoreState((state: ApplicationStore) => state.settings.data!.name);
return (
<div id={'navigation'}>
<div className={'mx-auto w-full flex items-center'} style={{ maxWidth: '1200px', height: '3.5rem' }}>
<Navigation>
<div css={tw`mx-auto w-full flex items-center`} style={{ maxWidth: '1200px', height: '3.5rem' }}>
<div id={'logo'}>
<Link to={'/'}>
{name}
</Link>
</div>
<div className={'right-navigation'}>
<RightNavigation>
<SearchContainer/>
<NavLink to={'/'} exact={true}>
<NavLink to={'/'} exact>
<FontAwesomeIcon icon={faLayerGroup}/>
</NavLink>
<NavLink to={'/account'}>
<FontAwesomeIcon icon={faUserCircle}/>
</NavLink>
{user.rootAdmin &&
<a href={'/admin'} target={'_blank'}>
<a href={'/admin'} target={'_blank'} rel={'noreferrer'}>
<FontAwesomeIcon icon={faCogs}/>
</a>
}
{process.env.NODE_ENV !== 'production' &&
<NavLink to={'/design'}>
<FontAwesomeIcon icon={faSwatchbook}/>
</NavLink>
}
<a href={'/auth/logout'}>
<FontAwesomeIcon icon={faSignOutAlt}/>
</a>
</div>
</RightNavigation>
</div>
</div>
</Navigation>
);
};

View File

@ -10,6 +10,7 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import { useStoreState } from 'easy-peasy';
import { usePersistedState } from '@/plugins/usePersistedState';
import Switch from '@/components/elements/Switch';
import tw from 'twin.macro';
export default () => {
const { addError, clearFlashes } = useFlash();
@ -37,10 +38,10 @@ export default () => {
return (
<PageContentBlock>
<FlashMessageRender className={'mb-4'}/>
<FlashMessageRender css={tw`mb-4`}/>
{rootAdmin &&
<div className={'mb-2 flex justify-end items-center'}>
<p className={'uppercase text-xs text-neutral-400 mr-2'}>
<div css={tw`mb-2 flex justify-end items-center`}>
<p css={tw`uppercase text-xs text-neutral-400 mr-2`}>
{showAdmin ? 'Showing all servers' : 'Showing your servers'}
</p>
<Switch
@ -51,14 +52,16 @@ export default () => {
</div>
}
{loading ?
<Spinner centered={true} size={'large'}/>
<Spinner centered size={'large'}/>
:
servers.length > 0 ?
servers.map(server => (
<ServerRow key={server.uuid} server={server} className={'mt-2'}/>
servers.map((server, index) => (
<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.
</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 { bytesToHuman } from '@/helpers';
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
// than the more faded default style.
@ -20,7 +22,7 @@ const isAlarmState = (current: number, limit: number): boolean => {
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 [ stats, setStats ] = useState<ServerStats | null>(null);
const [ statsError, setStatsError ] = useState(false);
@ -37,7 +39,6 @@ export default ({ server, className }: { server: Server; className: string | und
useEffect(() => {
getStats().then(() => {
// @ts-ignore
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.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 memorylimit = server.limits.memory != 0 ? bytesToHuman(server.limits.memory * 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';
return (
<Link to={`/server/${server.id}`} className={`grey-row-box cursor-pointer ${className}`}>
<GreyRowBox as={Link} to={`/server/${server.id}`}>
<div className={'icon'}>
<FontAwesomeIcon icon={faServer}/>
</div>
<div className={'flex-1 ml-4'}>
<p className={'text-lg'}>{server.name}</p>
<div css={tw`flex-1 ml-4`}>
<p css={tw`text-lg`}>{server.name}</p>
</div>
<div className={'w-1/4 overflow-hidden'}>
<div className={'flex ml-4'}>
<FontAwesomeIcon icon={faEthernet} className={'text-neutral-500'}/>
<p className={'text-sm text-neutral-400 ml-2'}>
<div css={tw`w-1/4 overflow-hidden`}>
<div css={tw`flex ml-4`}>
<FontAwesomeIcon icon={faEthernet} css={tw`text-neutral-500`}/>
<p css={tw`text-sm text-neutral-400 ml-2`}>
{
server.allocations.filter(alloc => alloc.default).map(allocation => (
<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>
</div>
</div>
<div className={'w-1/3 flex items-baseline relative'}>
<div css={tw`w-1/3 flex items-baseline relative`}>
{!stats ?
!statsError ?
<SpinnerOverlay size={'tiny'} visible={true} backgroundOpacity={0.25}/>
:
server.isInstalling ?
<div className={'flex-1 text-center'}>
<span className={'bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs'}>
<div css={tw`flex-1 text-center`}>
<span css={tw`bg-neutral-500 rounded px-2 py-1 text-neutral-100 text-xs`}>
Installing
</span>
</div>
:
<div className={'flex-1 text-center'}>
<span className={'bg-red-500 rounded px-2 py-1 text-red-100 text-xs'}>
<div css={tw`flex-1 text-center`}>
<span css={tw`bg-red-500 rounded px-2 py-1 text-red-100 text-xs`}>
{server.isSuspended ? 'Suspended' : 'Connection Error'}
</span>
</div>
:
<React.Fragment>
<div className={'flex-1 flex ml-4 justify-center'}>
<div css={tw`flex-1 flex ml-4 justify-center`}>
<FontAwesomeIcon
icon={faMicrochip}
className={classNames({
'text-neutral-500': !alarms.cpu,
'text-red-400': alarms.cpu,
})}
css={[
!alarms.cpu && tw`text-neutral-500`,
alarms.cpu && tw`text-red-400`,
]}
/>
<p
className={classNames('text-sm ml-2', {
'text-neutral-400': !alarms.cpu,
'text-white': alarms.cpu,
})}
css={[
tw`text-sm ml-2`,
!alarms.cpu && tw`text-neutral-400`,
alarms.cpu && tw`text-white`,
]}
>
{stats.cpuUsagePercent} %
</p>
</div>
<div className={'flex-1 ml-4'}>
<div className={'flex justify-center'}>
<div css={tw`flex-1 ml-4`}>
<div css={tw`flex justify-center`}>
<FontAwesomeIcon
icon={faMemory}
className={classNames({
'text-neutral-500': !alarms.memory,
'text-red-400': alarms.memory,
})}
css={[
!alarms.memory && tw`text-neutral-500`,
alarms.memory && tw`text-red-400`,
]}
/>
<p
className={classNames('text-sm ml-2', {
'text-neutral-400': !alarms.memory,
'text-white': alarms.memory,
})}
css={[
tw`text-sm ml-2`,
!alarms.memory && tw`text-neutral-400`,
alarms.memory && tw`text-white`,
]}
>
{bytesToHuman(stats.memoryUsageInBytes)}
</p>
</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 className={'flex-1 ml-4'}>
<div className={'flex justify-center'}>
<div css={tw`flex-1 ml-4`}>
<div css={tw`flex justify-center`}>
<FontAwesomeIcon
icon={faHdd}
className={classNames({
'text-neutral-500': !alarms.disk,
'text-red-400': alarms.disk,
})}
css={[
!alarms.disk && tw`text-neutral-500`,
alarms.disk && tw`text-red-400`,
]}
/>
<p
className={classNames('text-sm ml-2', {
'text-neutral-400': !alarms.disk,
'text-white': alarms.disk,
})}
css={[
tw`text-sm ml-2`,
!alarms.disk && tw`text-neutral-400`,
alarms.disk && tw`text-white`,
]}
>
{bytesToHuman(stats.diskUsageInBytes)}
</p>
</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>
</React.Fragment>
}
</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 ContentContainer from '@/components/elements/ContentContainer';
import { CSSTransition } from 'react-transition-group';
import tw from 'twin.macro';
interface Props {
children: React.ReactNode;
className?: string;
}
export default ({ className, children }: Props) => (
<CSSTransition timeout={250} classNames={'fade'} appear={true} in={true}>
export default ({ children }): React.FC => (
<CSSTransition timeout={250} classNames={'fade'} appear in>
<>
<ContentContainer className={`my-10 ${className}`}>
<ContentContainer css={tw`my-10`}>
{children}
</ContentContainer>
<ContentContainer className={'mb-4'}>
<p className={'text-center text-neutral-500 text-xs'}>
<ContentContainer css={tw`mb-4`}>
<p css={tw`text-center text-neutral-500 text-xs`}>
&copy; 2015 - 2020&nbsp;
<a
rel={'noopener nofollow'}
rel={'noopener nofollow noreferrer'}
href={'https://pterodactyl.io'}
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
</a>

View File

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