Make it easier for plugins to extend the navigation and add routes
This commit is contained in:
parent
88a7bd7578
commit
04e97cc67e
5 changed files with 198 additions and 116 deletions
30
resources/scripts/components/elements/PermissionRoute.tsx
Normal file
30
resources/scripts/components/elements/PermissionRoute.tsx
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Route } from 'react-router-dom';
|
||||||
|
import { RouteProps } from 'react-router';
|
||||||
|
import Can from '@/components/elements/Can';
|
||||||
|
import { ServerError } from '@/components/elements/ScreenBlock';
|
||||||
|
|
||||||
|
interface Props extends Omit<RouteProps, 'path'> {
|
||||||
|
path: string;
|
||||||
|
permission: string | string[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ({ permission, children, ...props }: Props) => (
|
||||||
|
<Route {...props}>
|
||||||
|
{!permission ?
|
||||||
|
children
|
||||||
|
:
|
||||||
|
<Can
|
||||||
|
action={permission}
|
||||||
|
renderOnError={
|
||||||
|
<ServerError
|
||||||
|
title={'Access Denied'}
|
||||||
|
message={'You do not have permission to access this page.'}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Can>
|
||||||
|
}
|
||||||
|
</Route>
|
||||||
|
);
|
|
@ -0,0 +1,33 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { ServerContext } from '@/state/server';
|
||||||
|
import ScreenBlock from '@/components/elements/ScreenBlock';
|
||||||
|
import ServerInstallSvg from '@/assets/images/server_installing.svg';
|
||||||
|
import ServerErrorSvg from '@/assets/images/server_error.svg';
|
||||||
|
import ServerRestoreSvg from '@/assets/images/server_restore.svg';
|
||||||
|
|
||||||
|
export default () => {
|
||||||
|
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.'}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
status === 'suspended' ?
|
||||||
|
<ScreenBlock
|
||||||
|
title={'Server Suspended'}
|
||||||
|
image={ServerErrorSvg}
|
||||||
|
message={'This server is suspended and cannot be accessed.'}
|
||||||
|
/>
|
||||||
|
:
|
||||||
|
<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.'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { lazy, useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import getFileContents from '@/api/server/files/getFileContents';
|
import getFileContents from '@/api/server/files/getFileContents';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||||
|
@ -19,8 +19,7 @@ import { ServerContext } from '@/state/server';
|
||||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
||||||
import { encodePathSegments, hashToPath } from '@/helpers';
|
import { encodePathSegments, hashToPath } from '@/helpers';
|
||||||
import { dirname } from 'path';
|
import { dirname } from 'path';
|
||||||
|
import CodemirrorEditor from '@/components/elements/CodemirrorEditor';
|
||||||
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [ error, setError ] = useState('');
|
const [ error, setError ] = useState('');
|
||||||
|
@ -116,7 +115,7 @@ export default () => {
|
||||||
/>
|
/>
|
||||||
<div css={tw`relative`}>
|
<div css={tw`relative`}>
|
||||||
<SpinnerOverlay visible={loading}/>
|
<SpinnerOverlay visible={loading}/>
|
||||||
<LazyCodemirrorEditor
|
<CodemirrorEditor
|
||||||
mode={mode}
|
mode={mode}
|
||||||
filename={hash.replace(/^#/, '')}
|
filename={hash.replace(/^#/, '')}
|
||||||
onModeChanged={setMode}
|
onModeChanged={setMode}
|
||||||
|
|
|
@ -2,63 +2,24 @@ import TransferListener from '@/components/server/TransferListener';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { NavLink, Route, Switch, useRouteMatch } from 'react-router-dom';
|
import { NavLink, Route, Switch, useRouteMatch } from 'react-router-dom';
|
||||||
import NavigationBar from '@/components/NavigationBar';
|
import NavigationBar from '@/components/NavigationBar';
|
||||||
import ServerConsole from '@/components/server/ServerConsole';
|
|
||||||
import TransitionRouter from '@/TransitionRouter';
|
import TransitionRouter from '@/TransitionRouter';
|
||||||
import WebsocketHandler from '@/components/server/WebsocketHandler';
|
import WebsocketHandler from '@/components/server/WebsocketHandler';
|
||||||
import { ServerContext } from '@/state/server';
|
import { ServerContext } from '@/state/server';
|
||||||
import DatabasesContainer from '@/components/server/databases/DatabasesContainer';
|
|
||||||
import FileManagerContainer from '@/components/server/files/FileManagerContainer';
|
|
||||||
import { CSSTransition } from 'react-transition-group';
|
import { CSSTransition } from 'react-transition-group';
|
||||||
import FileEditContainer from '@/components/server/files/FileEditContainer';
|
|
||||||
import SettingsContainer from '@/components/server/settings/SettingsContainer';
|
|
||||||
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
|
|
||||||
import ScheduleEditContainer from '@/components/server/schedules/ScheduleEditContainer';
|
|
||||||
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 Spinner from '@/components/elements/Spinner';
|
import Spinner from '@/components/elements/Spinner';
|
||||||
import ScreenBlock, { NotFound, ServerError } from '@/components/elements/ScreenBlock';
|
import { NotFound, ServerError } from '@/components/elements/ScreenBlock';
|
||||||
import { httpErrorToHuman } from '@/api/http';
|
import { httpErrorToHuman } from '@/api/http';
|
||||||
import { useStoreState } from 'easy-peasy';
|
import { useStoreState } from 'easy-peasy';
|
||||||
import SubNavigation from '@/components/elements/SubNavigation';
|
import SubNavigation from '@/components/elements/SubNavigation';
|
||||||
import NetworkContainer from '@/components/server/network/NetworkContainer';
|
|
||||||
import InstallListener from '@/components/server/InstallListener';
|
import InstallListener from '@/components/server/InstallListener';
|
||||||
import StartupContainer from '@/components/server/startup/StartupContainer';
|
|
||||||
import ErrorBoundary from '@/components/elements/ErrorBoundary';
|
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 ServerInstallSvg from '@/assets/images/server_installing.svg';
|
|
||||||
import ServerRestoreSvg from '@/assets/images/server_restore.svg';
|
|
||||||
import ServerErrorSvg from '@/assets/images/server_error.svg';
|
|
||||||
import { useLocation } from 'react-router';
|
import { useLocation } from 'react-router';
|
||||||
|
import ConflictStateRenderer from '@/components/server/ConflictStateRenderer';
|
||||||
const ConflictStateRenderer = () => {
|
import PermissionRoute from '@/components/elements/PermissionRoute';
|
||||||
const status = ServerContext.useStoreState(state => state.server.data?.status || null);
|
import routes from '@/routers/routes';
|
||||||
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.'}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
status === 'suspended' ?
|
|
||||||
<ScreenBlock
|
|
||||||
title={'Server Suspended'}
|
|
||||||
image={ServerErrorSvg}
|
|
||||||
message={'This server is suspended and cannot be accessed.'}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<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.'}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const match = useRouteMatch<{ id: string }>();
|
const match = useRouteMatch<{ id: string }>();
|
||||||
|
@ -74,6 +35,10 @@ export default () => {
|
||||||
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);
|
||||||
|
|
||||||
|
const to = (value: string, url = false) => {
|
||||||
|
return `${(url ? match.url : match.path).replace(/\/*$/, '')}/${value.replace(/^\/+/, '')}`;
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
clearServerState();
|
clearServerState();
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -105,33 +70,21 @@ export default () => {
|
||||||
<CSSTransition timeout={150} classNames={'fade'} appear in>
|
<CSSTransition timeout={150} classNames={'fade'} appear in>
|
||||||
<SubNavigation>
|
<SubNavigation>
|
||||||
<div>
|
<div>
|
||||||
<NavLink to={`${match.url}`} exact>Console</NavLink>
|
{routes.server.filter(route => !!route.name).map((route) => (
|
||||||
<Can action={'file.*'}>
|
route.permission ?
|
||||||
<NavLink to={`${match.url}/files`}>File Manager</NavLink>
|
<Can key={route.path} action={route.permission} matchAny>
|
||||||
</Can>
|
<NavLink to={to(route.path, true)} exact={route.exact}>
|
||||||
<Can action={'database.*'}>
|
{route.name}
|
||||||
<NavLink to={`${match.url}/databases`}>Databases</NavLink>
|
</NavLink>
|
||||||
</Can>
|
|
||||||
<Can action={'schedule.*'}>
|
|
||||||
<NavLink to={`${match.url}/schedules`}>Schedules</NavLink>
|
|
||||||
</Can>
|
|
||||||
<Can action={'user.*'}>
|
|
||||||
<NavLink to={`${match.url}/users`}>Users</NavLink>
|
|
||||||
</Can>
|
|
||||||
<Can action={'backup.*'}>
|
|
||||||
<NavLink to={`${match.url}/backups`}>Backups</NavLink>
|
|
||||||
</Can>
|
|
||||||
<Can action={'allocation.*'}>
|
|
||||||
<NavLink to={`${match.url}/network`}>Network</NavLink>
|
|
||||||
</Can>
|
|
||||||
<Can action={'startup.*'}>
|
|
||||||
<NavLink to={`${match.url}/startup`}>Startup</NavLink>
|
|
||||||
</Can>
|
|
||||||
<Can action={[ 'settings.*', 'file.sftp' ]} matchAny>
|
|
||||||
<NavLink to={`${match.url}/settings`}>Settings</NavLink>
|
|
||||||
</Can>
|
</Can>
|
||||||
|
:
|
||||||
|
<NavLink key={route.path} to={to(route.path, true)} exact={route.exact}>
|
||||||
|
{route.name}
|
||||||
|
</NavLink>
|
||||||
|
))}
|
||||||
{rootAdmin &&
|
{rootAdmin &&
|
||||||
<a href={'/admin/servers/view/' + serverId} rel="noreferrer" target={'_blank'}>
|
// eslint-disable-next-line react/jsx-no-target-blank
|
||||||
|
<a href={`/admin/servers/view/${serverId}`} target={'_blank'}>
|
||||||
<FontAwesomeIcon icon={faExternalLinkAlt}/>
|
<FontAwesomeIcon icon={faExternalLinkAlt}/>
|
||||||
</a>
|
</a>
|
||||||
}
|
}
|
||||||
|
@ -147,47 +100,13 @@ export default () => {
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<TransitionRouter>
|
<TransitionRouter>
|
||||||
<Switch location={location}>
|
<Switch location={location}>
|
||||||
<Route path={`${match.path}`} component={ServerConsole} exact/>
|
{routes.server.map(({ path, permission, component: Component }) => (
|
||||||
<Route path={`${match.path}/files`} exact>
|
<PermissionRoute key={path} permission={permission} path={to(path)} exact>
|
||||||
<RequireServerPermission permissions={'file.*'}>
|
|
||||||
<FileManagerContainer/>
|
|
||||||
</RequireServerPermission>
|
|
||||||
</Route>
|
|
||||||
<Route path={`${match.path}/files/:action(edit|new)`} exact>
|
|
||||||
<Spinner.Suspense>
|
<Spinner.Suspense>
|
||||||
<FileEditContainer/>
|
<Component/>
|
||||||
</Spinner.Suspense>
|
</Spinner.Suspense>
|
||||||
</Route>
|
</PermissionRoute>
|
||||||
<Route path={`${match.path}/databases`} exact>
|
))}
|
||||||
<RequireServerPermission permissions={'database.*'}>
|
|
||||||
<DatabasesContainer/>
|
|
||||||
</RequireServerPermission>
|
|
||||||
</Route>
|
|
||||||
<Route path={`${match.path}/schedules`} exact>
|
|
||||||
<RequireServerPermission permissions={'schedule.*'}>
|
|
||||||
<ScheduleContainer/>
|
|
||||||
</RequireServerPermission>
|
|
||||||
</Route>
|
|
||||||
<Route path={`${match.path}/schedules/:id`} exact>
|
|
||||||
<ScheduleEditContainer/>
|
|
||||||
</Route>
|
|
||||||
<Route path={`${match.path}/users`} exact>
|
|
||||||
<RequireServerPermission permissions={'user.*'}>
|
|
||||||
<UsersContainer/>
|
|
||||||
</RequireServerPermission>
|
|
||||||
</Route>
|
|
||||||
<Route path={`${match.path}/backups`} exact>
|
|
||||||
<RequireServerPermission permissions={'backup.*'}>
|
|
||||||
<BackupContainer/>
|
|
||||||
</RequireServerPermission>
|
|
||||||
</Route>
|
|
||||||
<Route path={`${match.path}/network`} exact>
|
|
||||||
<RequireServerPermission permissions={'allocation.*'}>
|
|
||||||
<NetworkContainer/>
|
|
||||||
</RequireServerPermission>
|
|
||||||
</Route>
|
|
||||||
<Route path={`${match.path}/startup`} component={StartupContainer} exact/>
|
|
||||||
<Route path={`${match.path}/settings`} component={SettingsContainer} exact/>
|
|
||||||
<Route path={'*'} component={NotFound}/>
|
<Route path={'*'} component={NotFound}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</TransitionRouter>
|
</TransitionRouter>
|
||||||
|
|
101
resources/scripts/routers/routes.ts
Normal file
101
resources/scripts/routers/routes.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import React, { lazy } from 'react';
|
||||||
|
import ServerConsole from '@/components/server/ServerConsole';
|
||||||
|
import DatabasesContainer from '@/components/server/databases/DatabasesContainer';
|
||||||
|
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
|
||||||
|
import UsersContainer from '@/components/server/users/UsersContainer';
|
||||||
|
import BackupContainer from '@/components/server/backups/BackupContainer';
|
||||||
|
import NetworkContainer from '@/components/server/network/NetworkContainer';
|
||||||
|
import StartupContainer from '@/components/server/startup/StartupContainer';
|
||||||
|
|
||||||
|
const FileManagerContainer = lazy(() => import('@/components/server/files/FileManagerContainer'));
|
||||||
|
const FileEditContainer = lazy(() => import('@/components/server/files/FileEditContainer'));
|
||||||
|
const ScheduleEditContainer = lazy(() => import('@/components/server/schedules/ScheduleEditContainer'));
|
||||||
|
const SettingsContainer = lazy(() => import('@/components/server/settings/SettingsContainer'));
|
||||||
|
|
||||||
|
interface ServerRouteDefinition {
|
||||||
|
path: string;
|
||||||
|
permission: string | string[] | null;
|
||||||
|
// If undefined is passed this route is still rendered into the router itself
|
||||||
|
// but no navigation link is displayed in the sub-navigation menu.
|
||||||
|
name: string | undefined;
|
||||||
|
component: React.ComponentType;
|
||||||
|
// The default for "exact" is assumed to be "true" unless you explicitly
|
||||||
|
// pass it as false.
|
||||||
|
exact?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Routes {
|
||||||
|
server: ServerRouteDefinition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
server: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
permission: null,
|
||||||
|
name: 'Console',
|
||||||
|
component: ServerConsole,
|
||||||
|
exact: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/files',
|
||||||
|
permission: 'file.*',
|
||||||
|
name: 'Files',
|
||||||
|
component: FileManagerContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/files/:action(edit|new)',
|
||||||
|
permission: 'file.*',
|
||||||
|
name: undefined,
|
||||||
|
component: FileEditContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/databases',
|
||||||
|
permission: 'database.*',
|
||||||
|
name: 'Databases',
|
||||||
|
component: DatabasesContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/schedules',
|
||||||
|
permission: 'schedule.*',
|
||||||
|
name: 'Schedules',
|
||||||
|
component: ScheduleContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/schedules/:id',
|
||||||
|
permission: 'schedule.*',
|
||||||
|
name: undefined,
|
||||||
|
component: ScheduleEditContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
permission: 'user.*',
|
||||||
|
name: 'Users',
|
||||||
|
component: UsersContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/backups',
|
||||||
|
permission: 'backup.*',
|
||||||
|
name: 'Backups',
|
||||||
|
component: BackupContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/network',
|
||||||
|
permission: 'allocation.*',
|
||||||
|
name: 'Network',
|
||||||
|
component: NetworkContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/startup',
|
||||||
|
permission: 'startup.*',
|
||||||
|
name: 'Startup',
|
||||||
|
component: StartupContainer,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
permission: [ 'settings.*', 'file.sftp' ],
|
||||||
|
name: 'Settings',
|
||||||
|
component: SettingsContainer,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} as Routes;
|
Loading…
Reference in a new issue