Update use of server error blocks

This commit is contained in:
Dane Everitt 2021-01-30 18:01:32 -08:00
parent e30a765071
commit 32fb21d0b7
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
20 changed files with 132 additions and 116 deletions

View file

@ -0,0 +1,5 @@
import http from '@/api/http';
export const restoreServerBackup = async (uuid: string, backup: string): Promise<void> => {
await http.post(`/api/client/servers/${uuid}/backups/${backup}/restore`);
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -9,7 +9,7 @@ import ServerRouter from '@/routers/ServerRouter';
import AuthenticationRouter from '@/routers/AuthenticationRouter'; import AuthenticationRouter from '@/routers/AuthenticationRouter';
import { SiteSettings } from '@/state/settings'; 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/elements/ScreenBlock';
import tw, { GlobalStyles as TailwindGlobalStyles } from 'twin.macro'; import tw, { GlobalStyles as TailwindGlobalStyles } from 'twin.macro';
import GlobalStylesheet from '@/assets/css/GlobalStylesheet'; import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
import { history } from '@/components/history'; import { history } from '@/components/history';

View file

@ -5,6 +5,8 @@ import { faArrowLeft, faSyncAlt } from '@fortawesome/free-solid-svg-icons';
import styled, { keyframes } from 'styled-components/macro'; import styled, { keyframes } from 'styled-components/macro';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import NotFoundSvg from '@/assets/images/not_found.svg';
import ServerErrorSvg from '@/assets/images/server_error.svg';
interface BaseProps { interface BaseProps {
title: string; title: string;
@ -16,15 +18,15 @@ interface BaseProps {
interface PropsWithRetry extends BaseProps { interface PropsWithRetry extends BaseProps {
onRetry?: () => void; onRetry?: () => void;
onBack?: never | undefined; onBack?: never;
} }
interface PropsWithBack extends BaseProps { interface PropsWithBack extends BaseProps {
onBack?: () => void; onBack?: () => void;
onRetry?: never | undefined; onRetry?: never;
} }
type Props = PropsWithBack | PropsWithRetry; export type ScreenBlockProps = PropsWithBack | PropsWithRetry;
const spin = keyframes` const spin = keyframes`
to { transform: rotate(360deg) } to { transform: rotate(360deg) }
@ -38,7 +40,7 @@ const ActionButton = styled(Button)`
} }
`; `;
export default ({ title, image, message, onBack, onRetry }: Props) => ( const ScreenBlock = ({ title, image, message, onBack, onRetry }: ScreenBlockProps) => (
<PageContentBlock> <PageContentBlock>
<div css={tw`flex justify-center`}> <div css={tw`flex justify-center`}>
<div css={tw`w-full sm:w-3/4 md:w-1/2 p-12 md:p-20 bg-neutral-100 rounded-lg shadow-lg text-center relative`}> <div css={tw`w-full sm:w-3/4 md:w-1/2 p-12 md:p-20 bg-neutral-100 rounded-lg shadow-lg text-center relative`}>
@ -61,3 +63,23 @@ export default ({ title, image, message, onBack, onRetry }: Props) => (
</div> </div>
</PageContentBlock> </PageContentBlock>
); );
type ServerErrorProps = (Omit<PropsWithBack, 'image' | 'title'> | Omit<PropsWithRetry, 'image' | 'title'>) & {
title?: string;
}
const ServerError = ({ title, ...props }: ServerErrorProps) => (
<ScreenBlock title={title || 'Something went wrong'} image={ServerErrorSvg} {...props}/>
);
const NotFound = ({ title, message, onBack }: Partial<Pick<ScreenBlockProps, 'title' | 'message' | 'onBack'>>) => (
<ScreenBlock
title={title || '404'}
image={NotFoundSvg}
message={message || 'The requested resource was not found.'}
onBack={onBack}
/>
);
export { ServerError, NotFound };
export default ScreenBlock;

View file

@ -1,17 +0,0 @@
import React from 'react';
import ScreenBlock from '@/components/screens/ScreenBlock';
interface Props {
title?: string;
message?: string;
onBack?: () => void;
}
export default ({ title, message, onBack }: Props) => (
<ScreenBlock
title={title || '404'}
image={'/assets/svgs/not_found.svg'}
message={message || 'The requested resource was not found.'}
onBack={onBack}
/>
);

View file

@ -1,20 +0,0 @@
import React from 'react';
import ScreenBlock from '@/components/screens/ScreenBlock';
interface Props {
title?: string;
message: string;
onRetry?: () => void;
onBack?: () => void;
}
export default ({ title, message, onBack, onRetry }: Props) => (
// @ts-ignore
<ScreenBlock
title={title || 'Something went wrong'}
image={'/assets/svgs/server_error.svg'}
message={message}
onBack={onBack}
onRetry={onRetry}
/>
);

View file

@ -14,6 +14,7 @@ import getServerBackups from '@/api/swr/getServerBackups';
import { ServerBackup } from '@/api/server/types'; import { ServerBackup } from '@/api/server/types';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import Input from '@/components/elements/Input'; import Input from '@/components/elements/Input';
import { restoreServerBackup } from '@/api/server/backups';
interface Props { interface Props {
backup: ServerBackup; backup: ServerBackup;
@ -21,10 +22,9 @@ interface Props {
export default ({ backup }: Props) => { export default ({ backup }: Props) => {
const uuid = ServerContext.useStoreState(state => state.server.data!.uuid); const uuid = ServerContext.useStoreState(state => state.server.data!.uuid);
const setServerFromState = ServerContext.useStoreActions(actions => actions.server.setServerFromState);
const [ modal, setModal ] = useState('');
const [ loading, setLoading ] = useState(false); const [ loading, setLoading ] = useState(false);
const [ visible, setVisible ] = useState(false);
const [ deleteVisible, setDeleteVisible ] = useState(false);
const [ restoreVisible, setRestoreVisible ] = useState(false);
const { clearFlashes, clearAndAddHttpError } = useFlash(); const { clearFlashes, clearAndAddHttpError } = useFlash();
const { mutate } = getServerBackups(); const { mutate } = getServerBackups();
@ -47,36 +47,47 @@ export default ({ backup }: Props) => {
setLoading(true); setLoading(true);
clearFlashes('backups'); clearFlashes('backups');
deleteBackup(uuid, backup.uuid) deleteBackup(uuid, backup.uuid)
.then(() => { .then(() => mutate(data => ({
mutate(data => ({ ...data,
...data, items: data.items.filter(b => b.uuid !== backup.uuid),
items: data.items.filter(b => b.uuid !== backup.uuid), }), false))
}), false);
})
.catch(error => { .catch(error => {
console.error(error); console.error(error);
clearAndAddHttpError({ key: 'backups', error }); clearAndAddHttpError({ key: 'backups', error });
setLoading(false); setLoading(false);
setDeleteVisible(false); setModal('');
}); });
}; };
const doRestorationAction = () => {
setLoading(true);
clearFlashes('backups');
restoreServerBackup(uuid, backup.uuid)
.then(() => setServerFromState(s => ({
...s,
status: 'restoring_backup',
})))
.catch(error => {
console.error(error);
clearAndAddHttpError({ key: 'backups', error });
})
.then(() => setLoading(false));
};
return ( return (
<> <>
{visible &&
<ChecksumModal <ChecksumModal
appear appear
visible={visible} visible={modal === 'checksum'}
onDismissed={() => setVisible(false)} onDismissed={() => setModal('')}
checksum={backup.checksum} checksum={backup.checksum}
/> />
}
<ConfirmationModal <ConfirmationModal
visible={restoreVisible} visible={modal === 'restore'}
title={'Restore this backup?'} title={'Restore this backup?'}
buttonText={'Restore backup'} buttonText={'Restore backup'}
onConfirmed={() => null} onConfirmed={() => doRestorationAction()}
onModalDismissed={() => setRestoreVisible(false)} onModalDismissed={() => setModal('')}
> >
<p css={tw`text-neutral-300`}> <p css={tw`text-neutral-300`}>
This server will be stopped in order to restore the backup. Once the backup has started you will This server will be stopped in order to restore the backup. Once the backup has started you will
@ -87,7 +98,10 @@ export default ({ backup }: Props) => {
Are you sure you want to continue? Are you sure you want to continue?
</p> </p>
<p css={tw`mt-4 -mb-2 bg-neutral-900 p-3 rounded`}> <p css={tw`mt-4 -mb-2 bg-neutral-900 p-3 rounded`}>
<label htmlFor={'restore_truncate'} css={tw`text-base text-neutral-200 flex items-center cursor-pointer`}> <label
htmlFor={'restore_truncate'}
css={tw`text-base text-neutral-200 flex items-center cursor-pointer`}
>
<Input <Input
type={'checkbox'} type={'checkbox'}
css={tw`text-red-500! w-5! h-5! mr-2`} css={tw`text-red-500! w-5! h-5! mr-2`}
@ -99,11 +113,11 @@ export default ({ backup }: Props) => {
</p> </p>
</ConfirmationModal> </ConfirmationModal>
<ConfirmationModal <ConfirmationModal
visible={deleteVisible} visible={modal === 'delete'}
title={'Delete this backup?'} title={'Delete this backup?'}
buttonText={'Yes, delete backup'} buttonText={'Yes, delete backup'}
onConfirmed={() => doDeletion()} onConfirmed={() => doDeletion()}
onModalDismissed={() => setDeleteVisible(false)} onModalDismissed={() => setModal('')}
> >
Are you sure you wish to delete this backup? This is a permanent operation and the backup cannot Are you sure you wish to delete this backup? This is a permanent operation and the backup cannot
be recovered once deleted. be recovered once deleted.
@ -122,23 +136,23 @@ export default ({ backup }: Props) => {
> >
<div css={tw`text-sm`}> <div css={tw`text-sm`}>
<Can action={'backup.download'}> <Can action={'backup.download'}>
<DropdownButtonRow onClick={() => doDownload()}> <DropdownButtonRow onClick={doDownload}>
<FontAwesomeIcon fixedWidth icon={faCloudDownloadAlt} css={tw`text-xs`}/> <FontAwesomeIcon fixedWidth icon={faCloudDownloadAlt} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Download</span> <span css={tw`ml-2`}>Download</span>
</DropdownButtonRow> </DropdownButtonRow>
</Can> </Can>
<Can action={'backup.restore'}> <Can action={'backup.restore'}>
<DropdownButtonRow onClick={() => setRestoreVisible(true)}> <DropdownButtonRow onClick={() => setModal('restore')}>
<FontAwesomeIcon fixedWidth icon={faBoxOpen} css={tw`text-xs`}/> <FontAwesomeIcon fixedWidth icon={faBoxOpen} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Restore</span> <span css={tw`ml-2`}>Restore</span>
</DropdownButtonRow> </DropdownButtonRow>
</Can> </Can>
<DropdownButtonRow onClick={() => setVisible(true)}> <DropdownButtonRow onClick={() => setModal('checksum')}>
<FontAwesomeIcon fixedWidth icon={faLock} css={tw`text-xs`}/> <FontAwesomeIcon fixedWidth icon={faLock} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Checksum</span> <span css={tw`ml-2`}>Checksum</span>
</DropdownButtonRow> </DropdownButtonRow>
<Can action={'backup.delete'}> <Can action={'backup.delete'}>
<DropdownButtonRow danger onClick={() => setDeleteVisible(true)}> <DropdownButtonRow danger onClick={() => setModal('delete')}>
<FontAwesomeIcon fixedWidth icon={faTrashAlt} css={tw`text-xs`}/> <FontAwesomeIcon fixedWidth icon={faTrashAlt} css={tw`text-xs`}/>
<span css={tw`ml-2`}>Delete</span> <span css={tw`ml-2`}>Delete</span>
</DropdownButtonRow> </DropdownButtonRow>
@ -147,7 +161,7 @@ export default ({ backup }: Props) => {
</DropdownMenu> </DropdownMenu>
: :
<button <button
onClick={() => setDeleteVisible(true)} onClick={() => setModal('delete')}
css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`} css={tw`text-neutral-200 transition-colors duration-150 hover:text-neutral-100 p-2`}
> >
<FontAwesomeIcon icon={faTrashAlt}/> <FontAwesomeIcon icon={faTrashAlt}/>

View file

@ -9,7 +9,7 @@ import FileNameModal from '@/components/server/files/FileNameModal';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import PageContentBlock from '@/components/elements/PageContentBlock'; import PageContentBlock from '@/components/elements/PageContentBlock';
import ServerError from '@/components/screens/ServerError'; import { ServerError } from '@/components/elements/ScreenBlock';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import Select from '@/components/elements/Select'; import Select from '@/components/elements/Select';

View file

@ -8,7 +8,7 @@ import { FileObject } from '@/api/server/files/loadDirectory';
import NewDirectoryButton from '@/components/server/files/NewDirectoryButton'; import NewDirectoryButton from '@/components/server/files/NewDirectoryButton';
import { NavLink, useLocation } from 'react-router-dom'; import { NavLink, useLocation } from 'react-router-dom';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import ServerError from '@/components/screens/ServerError'; import { ServerError } from '@/components/elements/ScreenBlock';
import tw from 'twin.macro'; import tw from 'twin.macro';
import Button from '@/components/elements/Button'; import Button from '@/components/elements/Button';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';

View file

@ -5,7 +5,7 @@ import VariableBox from '@/components/server/startup/VariableBox';
import ServerContentBlock from '@/components/elements/ServerContentBlock'; import ServerContentBlock from '@/components/elements/ServerContentBlock';
import getServerStartup from '@/api/swr/getServerStartup'; import getServerStartup from '@/api/swr/getServerStartup';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import ServerError from '@/components/screens/ServerError'; import { ServerError } from '@/components/elements/ScreenBlock';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect'; import { useDeepCompareEffect } from '@/plugins/useDeepCompareEffect';

3
resources/scripts/globals.d.ts vendored Normal file
View file

@ -0,0 +1,3 @@
declare module '*.jpg';
declare module '*.png';
declare module '*.svg';

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import ScreenBlock from '@/components/screens/ScreenBlock'; import { ServerError } from '@/components/elements/ScreenBlock';
export interface RequireServerPermissionProps { export interface RequireServerPermissionProps {
permissions: string | string[] permissions: string | string[]
} }
@ -10,8 +11,7 @@ const RequireServerPermission: React.FC<RequireServerPermissionProps> = ({ child
<Can <Can
action={permissions} action={permissions}
renderOnError={ renderOnError={
<ScreenBlock <ServerError
image={'/assets/svgs/server_error.svg'}
title={'Access Denied'} title={'Access Denied'}
message={'You do not have permission to access this page.'} message={'You do not have permission to access this page.'}
/> />

View file

@ -4,7 +4,7 @@ import LoginContainer from '@/components/auth/LoginContainer';
import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer'; import ForgotPasswordContainer from '@/components/auth/ForgotPasswordContainer';
import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer'; import ResetPasswordContainer from '@/components/auth/ResetPasswordContainer';
import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer'; import LoginCheckpointContainer from '@/components/auth/LoginCheckpointContainer';
import NotFound from '@/components/screens/NotFound'; import { NotFound } from '@/components/elements/ScreenBlock';
export default ({ location, history, match }: RouteComponentProps) => ( export default ({ location, history, match }: RouteComponentProps) => (
<div className={'pt-8 xl:pt-32'}> <div className={'pt-8 xl:pt-32'}>

View file

@ -4,7 +4,7 @@ import AccountOverviewContainer from '@/components/dashboard/AccountOverviewCont
import NavigationBar from '@/components/NavigationBar'; import NavigationBar from '@/components/NavigationBar';
import DashboardContainer from '@/components/dashboard/DashboardContainer'; import DashboardContainer from '@/components/dashboard/DashboardContainer';
import AccountApiContainer from '@/components/dashboard/AccountApiContainer'; import AccountApiContainer from '@/components/dashboard/AccountApiContainer';
import NotFound from '@/components/screens/NotFound'; import { NotFound } from '@/components/elements/ScreenBlock';
import TransitionRouter from '@/TransitionRouter'; import TransitionRouter from '@/TransitionRouter';
import SubNavigation from '@/components/elements/SubNavigation'; import SubNavigation from '@/components/elements/SubNavigation';

View file

@ -18,11 +18,9 @@ import UsersContainer from '@/components/server/users/UsersContainer';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import BackupContainer from '@/components/server/backups/BackupContainer'; import BackupContainer from '@/components/server/backups/BackupContainer';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import ServerError from '@/components/screens/ServerError'; import ScreenBlock, { NotFound, ServerError } from '@/components/elements/ScreenBlock';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import NotFound from '@/components/screens/NotFound';
import { useStoreState } from 'easy-peasy'; import { useStoreState } from 'easy-peasy';
import ScreenBlock from '@/components/screens/ScreenBlock';
import SubNavigation from '@/components/elements/SubNavigation'; import SubNavigation from '@/components/elements/SubNavigation';
import NetworkContainer from '@/components/server/network/NetworkContainer'; import NetworkContainer from '@/components/server/network/NetworkContainer';
import InstallListener from '@/components/server/InstallListener'; import InstallListener from '@/components/server/InstallListener';
@ -31,17 +29,36 @@ import ErrorBoundary from '@/components/elements/ErrorBoundary';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
import RequireServerPermission from '@/hoc/RequireServerPermission'; import RequireServerPermission from '@/hoc/RequireServerPermission';
import ServerInstallSvg from '@/assets/images/server_installing.svg';
import ServerRestoreSvg from '@/assets/images/server_restore.svg';
const ConflictStateRenderer = () => {
const status = ServerContext.useStoreState(state => state.server.data?.status || null);
const isTransferring = ServerContext.useStoreState(state => state.server.data?.isTransferring || false);
return (
status === 'installing' || status === 'install_failed' ?
<ScreenBlock
title={'Running Installer'}
image={ServerInstallSvg}
message={'Your server should be ready soon, please try again in a few minutes.'}
/>
:
<ScreenBlock
title={isTransferring ? 'Transferring' : 'Restoring from Backup'}
image={ServerRestoreSvg}
message={isTransferring ? 'Your server is being transfered to a new node, please check back later.' : 'Your server is currently being restored from a backup, please check back in a few minutes.'}
/>
);
};
const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => { const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>) => {
const rootAdmin = useStoreState(state => state.user.data!.rootAdmin); const rootAdmin = useStoreState(state => state.user.data!.rootAdmin);
const [ error, setError ] = useState(''); const [ error, setError ] = useState('');
const [ installing, setInstalling ] = useState(false);
const [ transferring, setTransferring ] = useState(false);
const id = ServerContext.useStoreState(state => state.server.data?.id); const id = ServerContext.useStoreState(state => state.server.data?.id);
const uuid = ServerContext.useStoreState(state => state.server.data?.uuid); const uuid = ServerContext.useStoreState(state => state.server.data?.uuid);
const isInstalling = ServerContext.useStoreState(state => state.server.data?.isInstalling); const inConflictState = ServerContext.useStoreState(state => state.server.inConflictState);
const isTransferring = ServerContext.useStoreState(state => state.server.data?.isTransferring);
const serverId = ServerContext.useStoreState(state => state.server.data?.internalId); const serverId = ServerContext.useStoreState(state => state.server.data?.internalId);
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer); const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState); const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
@ -50,31 +67,13 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
clearServerState(); clearServerState();
}, []); }, []);
useEffect(() => {
setInstalling(!!isInstalling);
}, [ isInstalling ]);
useEffect(() => {
setTransferring(!!isTransferring);
}, [ isTransferring ]);
useEffect(() => { useEffect(() => {
setError(''); setError('');
setInstalling(false);
setTransferring(false);
getServer(match.params.id) getServer(match.params.id)
.catch(error => { .catch(error => {
if (error.response?.status === 409) { console.error(error);
if (error.response.data?.errors[0]?.code === 'ServerTransferringException') { setError(httpErrorToHuman(error));
setTransferring(true);
} else {
setInstalling(true);
}
} else {
console.error(error);
setError(httpErrorToHuman(error));
}
}); });
return () => { return () => {
@ -131,12 +130,8 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
<InstallListener/> <InstallListener/>
<TransferListener/> <TransferListener/>
<WebsocketHandler/> <WebsocketHandler/>
{((installing || transferring) && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ? {(inConflictState && (!rootAdmin || (rootAdmin && !location.pathname.endsWith(`/server/${id}`)))) ?
<ScreenBlock <ConflictStateRenderer/>
title={installing ? 'Your server is installing.' : 'Your server is currently being transferred.'}
image={'/assets/svgs/server_installing.svg'}
message={'Please check back in a few minutes.'}
/>
: :
<ErrorBoundary> <ErrorBoundary>
<TransitionRouter> <TransitionRouter>
@ -144,22 +139,22 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
<Route path={`${match.path}`} component={ServerConsole} exact/> <Route path={`${match.path}`} component={ServerConsole} exact/>
<Route path={`${match.path}/files`} exact> <Route path={`${match.path}/files`} exact>
<RequireServerPermission permissions={'file.*'}> <RequireServerPermission permissions={'file.*'}>
<FileManagerContainer /> <FileManagerContainer/>
</RequireServerPermission> </RequireServerPermission>
</Route> </Route>
<Route path={`${match.path}/files/:action(edit|new)`} exact> <Route path={`${match.path}/files/:action(edit|new)`} exact>
<SuspenseSpinner> <SuspenseSpinner>
<FileEditContainer /> <FileEditContainer/>
</SuspenseSpinner> </SuspenseSpinner>
</Route> </Route>
<Route path={`${match.path}/databases`} exact> <Route path={`${match.path}/databases`} exact>
<RequireServerPermission permissions={'database.*'}> <RequireServerPermission permissions={'database.*'}>
<DatabasesContainer /> <DatabasesContainer/>
</RequireServerPermission> </RequireServerPermission>
</Route> </Route>
<Route path={`${match.path}/schedules`} exact> <Route path={`${match.path}/schedules`} exact>
<RequireServerPermission permissions={'schedule.*'}> <RequireServerPermission permissions={'schedule.*'}>
<ScheduleContainer /> <ScheduleContainer/>
</RequireServerPermission> </RequireServerPermission>
</Route> </Route>
<Route path={`${match.path}/schedules/:id`} exact> <Route path={`${match.path}/schedules/:id`} exact>
@ -167,17 +162,17 @@ const ServerRouter = ({ match, location }: RouteComponentProps<{ id: string }>)
</Route> </Route>
<Route path={`${match.path}/users`} exact> <Route path={`${match.path}/users`} exact>
<RequireServerPermission permissions={'user.*'}> <RequireServerPermission permissions={'user.*'}>
<UsersContainer /> <UsersContainer/>
</RequireServerPermission> </RequireServerPermission>
</Route> </Route>
<Route path={`${match.path}/backups`} exact> <Route path={`${match.path}/backups`} exact>
<RequireServerPermission permissions={'backup.*'}> <RequireServerPermission permissions={'backup.*'}>
<BackupContainer /> <BackupContainer/>
</RequireServerPermission> </RequireServerPermission>
</Route> </Route>
<Route path={`${match.path}/network`} exact> <Route path={`${match.path}/network`} exact>
<RequireServerPermission permissions={'allocation.*'}> <RequireServerPermission permissions={'allocation.*'}>
<NetworkContainer /> <NetworkContainer/>
</RequireServerPermission> </RequireServerPermission>
</Route> </Route>
<Route path={`${match.path}/startup`} component={StartupContainer} exact/> <Route path={`${match.path}/startup`} component={StartupContainer} exact/>

View file

@ -1,5 +1,5 @@
import getServer, { Server } from '@/api/server/getServer'; import getServer, { Server } from '@/api/server/getServer';
import { action, Action, createContextStore, thunk, Thunk } from 'easy-peasy'; import { action, Action, computed, Computed, createContextStore, thunk, Thunk } from 'easy-peasy';
import socket, { SocketStore } from './socket'; import socket, { SocketStore } from './socket';
import files, { ServerFileStore } from '@/state/server/files'; import files, { ServerFileStore } from '@/state/server/files';
import subusers, { ServerSubuserStore } from '@/state/server/subusers'; import subusers, { ServerSubuserStore } from '@/state/server/subusers';
@ -12,6 +12,7 @@ export type ServerStatus = 'offline' | 'starting' | 'stopping' | 'running' | nul
interface ServerDataStore { interface ServerDataStore {
data?: Server; data?: Server;
inConflictState: Computed<ServerDataStore, boolean>;
permissions: string[]; permissions: string[];
getServer: Thunk<ServerDataStore, string, Record<string, unknown>, ServerStore, Promise<void>>; getServer: Thunk<ServerDataStore, string, Record<string, unknown>, ServerStore, Promise<void>>;
@ -23,6 +24,14 @@ interface ServerDataStore {
const server: ServerDataStore = { const server: ServerDataStore = {
permissions: [], permissions: [],
inConflictState: computed(state => {
if (!state.data) {
return false;
}
return state.data.status !== null || state.data.isTransferring;
}),
getServer: thunk(async (actions, payload) => { getServer: thunk(async (actions, payload) => {
const [ server, permissions ] = await getServer(payload); const [ server, permissions ] = await getServer(payload);