Merge branch 'develop' into fix/2071

This commit is contained in:
Matthew Penner 2020-07-11 12:29:04 -06:00 committed by GitHub
commit a9bb692112
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
194 changed files with 5396 additions and 5416 deletions

View file

@ -4,16 +4,17 @@ import CreateApiKeyForm from '@/components/dashboard/forms/CreateApiKeyForm';
import getApiKeys, { ApiKey } from '@/api/account/getApiKeys';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faKey } from '@fortawesome/free-solid-svg-icons/faKey';
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
import { faKey, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
import ConfirmationModal from '@/components/elements/ConfirmationModal';
import deleteApiKey from '@/api/account/deleteApiKey';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import FlashMessageRender from '@/components/FlashMessageRender';
import { httpErrorToHuman } from '@/api/http';
import format from 'date-fns/format';
import { format } from 'date-fns';
import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import GreyRowBox from '@/components/elements/GreyRowBox';
export default () => {
const [ deleteIdentifier, setDeleteIdentifier ] = useState('');
@ -48,18 +49,18 @@ export default () => {
return (
<PageContentBlock>
<FlashMessageRender byKey={'account'} className={'mb-4'}/>
<div className={'flex'}>
<ContentBox title={'Create API Key'} className={'flex-1'}>
<FlashMessageRender byKey={'account'} css={tw`mb-4`}/>
<div css={tw`flex`}>
<ContentBox title={'Create API Key'} css={tw`flex-1`}>
<CreateApiKeyForm onKeyCreated={key => setKeys(s => ([ ...s!, key ]))}/>
</ContentBox>
<ContentBox title={'API Keys'} className={'ml-10 flex-1'}>
<ContentBox title={'API Keys'} css={tw`ml-10 flex-1`}>
<SpinnerOverlay visible={loading}/>
{deleteIdentifier &&
<ConfirmationModal
visible
title={'Confirm key deletion'}
buttonText={'Yes, delete key'}
visible={true}
onConfirmed={() => {
doDeletion(deleteIdentifier);
setDeleteIdentifier('');
@ -72,38 +73,38 @@ export default () => {
}
{
keys.length === 0 ?
<p className={'text-center text-sm'}>
<p css={tw`text-center text-sm`}>
{loading ? 'Loading...' : 'No API keys exist for this account.'}
</p>
:
keys.map(key => (
<div
keys.map((key, index) => (
<GreyRowBox
key={key.identifier}
className={'grey-row-box bg-neutral-600 mb-2 flex items-center'}
css={[ tw`bg-neutral-600 flex items-center`, index > 0 && tw`mt-2` ]}
>
<FontAwesomeIcon icon={faKey} className={'text-neutral-300'}/>
<div className={'ml-4 flex-1'}>
<p className={'text-sm'}>{key.description}</p>
<p className={'text-2xs text-neutral-300 uppercase'}>
Last
used: {key.lastUsedAt ? format(key.lastUsedAt, 'MMM Do, YYYY HH:mm') : 'Never'}
<FontAwesomeIcon icon={faKey} css={tw`text-neutral-300`}/>
<div css={tw`ml-4 flex-1`}>
<p css={tw`text-sm`}>{key.description}</p>
<p css={tw`text-2xs text-neutral-300 uppercase`}>
Last used:&nbsp;
{key.lastUsedAt ? format(key.lastUsedAt, 'MMM do, yyyy HH:mm') : 'Never'}
</p>
</div>
<p className={'text-sm ml-4'}>
<code className={'font-mono py-1 px-2 bg-neutral-900 rounded'}>
<p css={tw`text-sm ml-4`}>
<code css={tw`font-mono py-1 px-2 bg-neutral-900 rounded`}>
{key.identifier}
</code>
</p>
<button
className={'ml-4 p-2 text-sm'}
css={tw`ml-4 p-2 text-sm`}
onClick={() => setDeleteIdentifier(key.identifier)}
>
<FontAwesomeIcon
icon={faTrashAlt}
className={'text-neutral-400 hover:text-red-400 transition-colors duration-150'}
css={tw`text-neutral-400 hover:text-red-400 transition-colors duration-150`}
/>
</button>
</div>
</GreyRowBox>
))
}
</ContentBox>

View file

@ -3,9 +3,10 @@ import ContentBox from '@/components/elements/ContentBox';
import UpdatePasswordForm from '@/components/dashboard/forms/UpdatePasswordForm';
import UpdateEmailAddressForm from '@/components/dashboard/forms/UpdateEmailAddressForm';
import ConfigureTwoFactorForm from '@/components/dashboard/forms/ConfigureTwoFactorForm';
import styled from 'styled-components';
import { breakpoint } from 'styled-components-breakpoint';
import PageContentBlock from '@/components/elements/PageContentBlock';
import tw from 'twin.macro';
import { breakpoint } from '@/theme';
import styled from 'styled-components/macro';
const Container = styled.div`
${tw`flex flex-wrap my-10`};
@ -31,13 +32,13 @@ export default () => {
<UpdatePasswordForm/>
</ContentBox>
<ContentBox
className={'mt-8 md:mt-0 md:ml-8'}
css={tw`mt-8 md:mt-0 md:ml-8`}
title={'Update Email Address'}
showFlashes={'account:email'}
>
<UpdateEmailAddressForm/>
</ContentBox>
<ContentBox className={'xl:ml-8 mt-8 xl:mt-0'} title={'Configure Two Factor'}>
<ContentBox css={tw`xl:ml-8 mt-8 xl:mt-0`} title={'Configure Two Factor'}>
<ConfigureTwoFactorForm/>
</ContentBox>
</Container>

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

@ -1,16 +1,14 @@
import React, { useEffect, useRef, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faServer } from '@fortawesome/free-solid-svg-icons/faServer';
import { faEthernet } from '@fortawesome/free-solid-svg-icons/faEthernet';
import { faMicrochip } from '@fortawesome/free-solid-svg-icons/faMicrochip';
import { faMemory } from '@fortawesome/free-solid-svg-icons/faMemory';
import { faHdd } from '@fortawesome/free-solid-svg-icons/faHdd';
import { faServer, faEthernet, faMicrochip, faMemory, faHdd } from '@fortawesome/free-solid-svg-icons';
import { Link } from 'react-router-dom';
import { Server } from '@/api/server/getServer';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import getServerResourceUsage, { ServerStats } from '@/api/server/getServerResourceUsage';
import { bytesToHuman, megabytesToHuman } 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 +18,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);
@ -52,110 +50,114 @@ 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 ? megabytesToHuman(server.limits.disk) : "Unlimited";
const memorylimit = server.limits.memory != 0 ? megabytesToHuman(server.limits.memory) : "Unlimited";
const disklimit = server.limits.disk !== 0 ? megabytesToHuman(server.limits.disk) : "Unlimited";
const memorylimit = server.limits.memory !== 0 ? megabytesToHuman(server.limits.memory) : "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 => (
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
))
}
</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}/>
<SpinnerOverlay size={'small'} visible 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

@ -3,6 +3,8 @@ import { useStoreState } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import SetupTwoFactorModal from '@/components/dashboard/forms/SetupTwoFactorModal';
import DisableTwoFactorModal from '@/components/dashboard/forms/DisableTwoFactorModal';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
export default () => {
const user = useStoreState((state: ApplicationStore) => state.user.data!);
@ -12,43 +14,45 @@ export default () => {
<div>
{visible &&
<DisableTwoFactorModal
appear={true}
appear
visible={visible}
onDismissed={() => setVisible(false)}
/>
}
<p className={'text-sm'}>
<p css={tw`text-sm`}>
Two-factor authentication is currently enabled on your account.
</p>
<div className={'mt-6'}>
<button
<div css={tw`mt-6`}>
<Button
color={'red'}
isSecondary
onClick={() => setVisible(true)}
className={'btn btn-red btn-secondary btn-sm'}
>
Disable
</button>
</Button>
</div>
</div>
:
<div>
{visible &&
<SetupTwoFactorModal
appear={true}
appear
visible={visible}
onDismissed={() => setVisible(false)}
/>
}
<p className={'text-sm'}>
<p css={tw`text-sm`}>
You do not currently have two-factor authentication enabled on your account. Click
the button below to begin configuring it.
</p>
<div className={'mt-6'}>
<button
<div css={tw`mt-6`}>
<Button
color={'green'}
isSecondary
onClick={() => setVisible(true)}
className={'btn btn-green btn-secondary btn-sm'}
>
Begin Setup
</button>
</Button>
</div>
</div>
;

View file

@ -9,6 +9,9 @@ import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import { ApiKey } from '@/api/account/getApiKeys';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
import Input, { Textarea } from '@/components/elements/Input';
interface Values {
description: string;
@ -44,22 +47,21 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
closeOnEscape={false}
closeOnBackground={false}
>
<h3 className={'mb-6'}>Your API Key</h3>
<p className={'text-sm mb-6'}>
<h3 css={tw`mb-6`}>Your API Key</h3>
<p css={tw`text-sm mb-6`}>
The API key you have requested is shown below. Please store this in a safe location, it will not be
shown again.
</p>
<pre className={'text-sm bg-neutral-900 rounded py-2 px-4 font-mono'}>
<code className={'font-mono'}>{apiKey}</code>
<pre css={tw`text-sm bg-neutral-900 rounded py-2 px-4 font-mono`}>
<code css={tw`font-mono`}>{apiKey}</code>
</pre>
<div className={'flex justify-end mt-6'}>
<button
<div css={tw`flex justify-end mt-6`}>
<Button
type={'button'}
className={'btn btn-secondary btn-sm'}
onClick={() => setApiKey('')}
>
Close
</button>
</Button>
</div>
</Modal>
<Formik
@ -80,25 +82,19 @@ export default ({ onKeyCreated }: { onKeyCreated: (key: ApiKey) => void }) => {
label={'Description'}
name={'description'}
description={'A description of this API key.'}
className={'mb-6'}
css={tw`mb-6`}
>
<Field name={'description'} className={'input-dark'}/>
<Field name={'description'} as={Input}/>
</FormikFieldWrapper>
<FormikFieldWrapper
label={'Allowed IPs'}
name={'allowedIps'}
description={'Leave blank to allow any IP address to use this API key, otherwise provide each IP address on a new line.'}
>
<Field
as={'textarea'}
name={'allowedIps'}
className={'input-dark h-32'}
/>
<Field as={Textarea} name={'allowedIps'} css={tw`h-32`}/>
</FormikFieldWrapper>
<div className={'flex justify-end mt-6'}>
<button className={'btn btn-primary btn-sm'}>
Create
</button>
<div css={tw`flex justify-end mt-6`}>
<Button>Create</Button>
</div>
</Form>
)}

View file

@ -8,6 +8,8 @@ import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import disableAccountTwoFactor from '@/api/account/disableAccountTwoFactor';
import { httpErrorToHuman } from '@/api/http';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
interface Values {
password: string;
@ -45,19 +47,19 @@ export default ({ ...props }: RequiredModalProps) => {
{({ isSubmitting, isValid }) => (
<Modal {...props} dismissable={!isSubmitting} showSpinnerOverlay={isSubmitting}>
<Form className={'mb-0'}>
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'}/>
<Field
id={'password'}
name={'password'}
type={'password'}
label={'Current Password'}
description={'In order to disable two-factor authentication you will need to provide your account password.'}
autoFocus={true}
autoFocus
/>
<div className={'mt-6 text-right'}>
<button className={'btn btn-red btn-sm'} disabled={!isValid}>
<div css={tw`mt-6 text-right`}>
<Button disabled={!isValid}>
Disable Two-Factor
</button>
</Button>
</div>
</Form>
</Modal>

View file

@ -9,6 +9,8 @@ import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender';
import Field from '@/components/elements/Field';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
interface Values {
code: string;
@ -64,7 +66,7 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
.matches(/^(\d){6}$/, 'Authenticator code must be 6 digits.'),
})}
>
{({ isSubmitting, isValid }) => (
{({ isSubmitting }) => (
<Modal
{...props}
onDismissed={dismiss}
@ -75,47 +77,47 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
>
{recoveryTokens.length > 0 ?
<>
<h2 className={'mb-4'}>Two-factor authentication enabled</h2>
<p className={'text-neutral-300'}>
<h2 css={tw`text-2xl mb-4`}>Two-factor authentication enabled</h2>
<p css={tw`text-neutral-300`}>
Two-factor authentication has been enabled on your account. Should you loose access to
this device you'll need to use on of the codes displayed below in order to access your
this device you&apos;ll need to use on of the codes displayed below in order to access your
account.
</p>
<p className={'text-neutral-300 mt-4'}>
<p css={tw`text-neutral-300 mt-4`}>
<strong>These codes will not be displayed again.</strong> Please take note of them now
by storing them in a secure repository such as a password manager.
</p>
<pre className={'mt-4 rounded font-mono bg-neutral-900 p-4'}>
{recoveryTokens.map(token => <code key={token} className={'block mb-1'}>{token}</code>)}
<pre css={tw`text-sm mt-4 rounded font-mono bg-neutral-900 p-4`}>
{recoveryTokens.map(token => <code key={token} css={tw`block mb-1`}>{token}</code>)}
</pre>
<div className={'text-right'}>
<button className={'mt-6 btn btn-lg btn-primary'} onClick={dismiss}>
<div css={tw`text-right`}>
<Button css={tw`mt-6`} size={'large'} onClick={dismiss}>
Close
</button>
</Button>
</div>
</>
:
<Form className={'mb-0'}>
<FlashMessageRender className={'mb-6'} byKey={'account:two-factor'}/>
<div className={'flex flex-wrap'}>
<div className={'w-full md:flex-1'}>
<div className={'w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto'}>
<Form css={tw`mb-0`}>
<FlashMessageRender css={tw`mb-6`} byKey={'account:two-factor'}/>
<div css={tw`flex flex-wrap`}>
<div css={tw`w-full md:flex-1`}>
<div css={tw`w-32 h-32 md:w-64 md:h-64 bg-neutral-600 p-2 rounded mx-auto`}>
{!token || !token.length ?
<img
src={'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII='}
className={'w-64 h-64 rounded'}
css={tw`w-64 h-64 rounded`}
/>
:
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=500x500&data=${token}`}
onLoad={() => setLoading(false)}
className={'w-full h-full shadow-none rounded-0'}
css={tw`w-full h-full shadow-none rounded-none`}
/>
}
</div>
</div>
<div className={'w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col'}>
<div className={'flex-1'}>
<div css={tw`w-full mt-6 md:mt-0 md:flex-1 md:flex md:flex-col`}>
<div css={tw`flex-1`}>
<Field
id={'code'}
name={'code'}
@ -125,10 +127,10 @@ export default ({ onDismissed, ...props }: RequiredModalProps) => {
autoFocus={!loading}
/>
</div>
<div className={'mt-6 md:mt-0 text-right'}>
<button className={'btn btn-primary btn-sm'} disabled={!isValid}>
<div css={tw`mt-6 md:mt-0 text-right`}>
<Button>
Setup
</button>
</Button>
</div>
</div>
</div>

View file

@ -6,6 +6,8 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import Field from '@/components/elements/Field';
import { httpErrorToHuman } from '@/api/http';
import { ApplicationStore } from '@/state';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
interface Values {
email: string;
@ -54,14 +56,14 @@ export default () => {
({ isSubmitting, isValid }) => (
<React.Fragment>
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
<Form className={'m-0'}>
<Form css={tw`m-0`}>
<Field
id={'current_email'}
type={'email'}
name={'email'}
label={'Email'}
/>
<div className={'mt-6'}>
<div css={tw`mt-6`}>
<Field
id={'confirm_password'}
type={'password'}
@ -69,10 +71,10 @@ export default () => {
label={'Confirm Password'}
/>
</div>
<div className={'mt-6'}>
<button className={'btn btn-sm btn-primary'} disabled={isSubmitting || !isValid}>
<div css={tw`mt-6`}>
<Button size={'small'} disabled={isSubmitting || !isValid}>
Update Email
</button>
</Button>
</div>
</Form>
</React.Fragment>

View file

@ -7,6 +7,8 @@ import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
import updateAccountPassword from '@/api/account/updateAccountPassword';
import { httpErrorToHuman } from '@/api/http';
import { ApplicationStore } from '@/state';
import tw from 'twin.macro';
import Button from '@/components/elements/Button';
interface Values {
current: string;
@ -30,7 +32,7 @@ export default () => {
return null;
}
const submit = (values: Values, { resetForm, setSubmitting }: FormikHelpers<Values>) => {
const submit = (values: Values, { setSubmitting }: FormikHelpers<Values>) => {
clearFlashes('account:password');
updateAccountPassword({ ...values })
.then(() => {
@ -57,14 +59,14 @@ export default () => {
({ isSubmitting, isValid }) => (
<React.Fragment>
<SpinnerOverlay size={'large'} visible={isSubmitting}/>
<Form className={'m-0'}>
<Form css={tw`m-0`}>
<Field
id={'current_password'}
type={'password'}
name={'current'}
label={'Current Password'}
/>
<div className={'mt-6'}>
<div css={tw`mt-6`}>
<Field
id={'new_password'}
type={'password'}
@ -73,7 +75,7 @@ export default () => {
description={'Your new password should be at least 8 characters in length and unique to this website.'}
/>
</div>
<div className={'mt-6'}>
<div css={tw`mt-6`}>
<Field
id={'confirm_password'}
type={'password'}
@ -81,10 +83,10 @@ export default () => {
label={'Confirm New Password'}
/>
</div>
<div className={'mt-6'}>
<button className={'btn btn-primary btn-sm'} disabled={isSubmitting || !isValid}>
<div css={tw`mt-6`}>
<Button size={'small'} disabled={isSubmitting || !isValid}>
Update Password
</button>
</Button>
</div>
</Form>
</React.Fragment>

View file

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
import { faSearch } from '@fortawesome/free-solid-svg-icons';
import useEventListener from '@/plugins/useEventListener';
import SearchModal from '@/components/dashboard/search/SearchModal';
@ -19,7 +19,7 @@ export default () => {
<>
{visible &&
<SearchModal
appear={true}
appear
visible={visible}
onDismissed={() => setVisible(false)}
/>

View file

@ -3,7 +3,7 @@ import Modal, { RequiredModalProps } from '@/components/elements/Modal';
import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik';
import { Actions, useStoreActions, useStoreState } from 'easy-peasy';
import { object, string } from 'yup';
import { debounce } from 'lodash-es';
import debounce from 'debounce';
import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper';
import InputSpinner from '@/components/elements/InputSpinner';
import getServers from '@/api/getServers';
@ -11,7 +11,9 @@ import { Server } from '@/api/server/getServer';
import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import styled from 'styled-components/macro';
import tw from 'twin.macro';
import Input from '@/components/elements/Input';
type Props = RequiredModalProps;
@ -20,8 +22,7 @@ interface Values {
}
const ServerResult = styled(Link)`
${tw`flex items-center bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline`};
transition: all 250ms linear;
${tw`flex items-center bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline transition-all duration-150`};
&:hover {
${tw`shadow border-cyan-500`};
@ -55,6 +56,7 @@ export default ({ ...props }: Props) => {
setLoading(true);
setSubmitting(false);
clearFlashes('search');
getServers(term)
.then(servers => setServers(servers.items.filter((_, index) => index < 5)))
.catch(error => {
@ -93,16 +95,12 @@ export default ({ ...props }: Props) => {
>
<SearchWatcher/>
<InputSpinner visible={loading}>
<Field
innerRef={ref}
name={'term'}
className={'input-dark'}
/>
<Field as={Input} innerRef={ref} name={'term'}/>
</InputSpinner>
</FormikFieldWrapper>
</Form>
{servers.length > 0 &&
<div className={'mt-6'}>
<div css={tw`mt-6`}>
{
servers.map(server => (
<ServerResult
@ -111,17 +109,17 @@ export default ({ ...props }: Props) => {
onClick={() => props.onDismissed()}
>
<div>
<p className={'text-sm'}>{server.name}</p>
<p className={'mt-1 text-xs text-neutral-400'}>
<p css={tw`text-sm`}>{server.name}</p>
<p css={tw`mt-1 text-xs text-neutral-400`}>
{
server.allocations.filter(alloc => alloc.default).map(allocation => (
server.allocations.filter(alloc => alloc.isDefault).map(allocation => (
<span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span>
))
}
</p>
</div>
<div className={'flex-1 text-right'}>
<span className={'text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded'}>
<div css={tw`flex-1 text-right`}>
<span css={tw`text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded`}>
{server.node}
</span>
</div>