Skip to content

Commit 04e97cc

Browse files
committed
Make it easier for plugins to extend the navigation and add routes
1 parent 88a7bd7 commit 04e97cc

File tree

5 files changed

+198
-116
lines changed

5 files changed

+198
-116
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React from 'react';
2+
import { Route } from 'react-router-dom';
3+
import { RouteProps } from 'react-router';
4+
import Can from '@/components/elements/Can';
5+
import { ServerError } from '@/components/elements/ScreenBlock';
6+
7+
interface Props extends Omit<RouteProps, 'path'> {
8+
path: string;
9+
permission: string | string[] | null;
10+
}
11+
12+
export default ({ permission, children, ...props }: Props) => (
13+
<Route {...props}>
14+
{!permission ?
15+
children
16+
:
17+
<Can
18+
action={permission}
19+
renderOnError={
20+
<ServerError
21+
title={'Access Denied'}
22+
message={'You do not have permission to access this page.'}
23+
/>
24+
}
25+
>
26+
{children}
27+
</Can>
28+
}
29+
</Route>
30+
);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import React from 'react';
2+
import { ServerContext } from '@/state/server';
3+
import ScreenBlock from '@/components/elements/ScreenBlock';
4+
import ServerInstallSvg from '@/assets/images/server_installing.svg';
5+
import ServerErrorSvg from '@/assets/images/server_error.svg';
6+
import ServerRestoreSvg from '@/assets/images/server_restore.svg';
7+
8+
export default () => {
9+
const status = ServerContext.useStoreState(state => state.server.data?.status || null);
10+
const isTransferring = ServerContext.useStoreState(state => state.server.data?.isTransferring || false);
11+
12+
return (
13+
status === 'installing' || status === 'install_failed' ?
14+
<ScreenBlock
15+
title={'Running Installer'}
16+
image={ServerInstallSvg}
17+
message={'Your server should be ready soon, please try again in a few minutes.'}
18+
/>
19+
:
20+
status === 'suspended' ?
21+
<ScreenBlock
22+
title={'Server Suspended'}
23+
image={ServerErrorSvg}
24+
message={'This server is suspended and cannot be accessed.'}
25+
/>
26+
:
27+
<ScreenBlock
28+
title={isTransferring ? 'Transferring' : 'Restoring from Backup'}
29+
image={ServerRestoreSvg}
30+
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.'}
31+
/>
32+
);
33+
};

resources/scripts/components/server/files/FileEditContainer.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { lazy, useEffect, useState } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import getFileContents from '@/api/server/files/getFileContents';
33
import { httpErrorToHuman } from '@/api/http';
44
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
@@ -19,8 +19,7 @@ import { ServerContext } from '@/state/server';
1919
import ErrorBoundary from '@/components/elements/ErrorBoundary';
2020
import { encodePathSegments, hashToPath } from '@/helpers';
2121
import { dirname } from 'path';
22-
23-
const LazyCodemirrorEditor = lazy(() => import(/* webpackChunkName: "editor" */'@/components/elements/CodemirrorEditor'));
22+
import CodemirrorEditor from '@/components/elements/CodemirrorEditor';
2423

2524
export default () => {
2625
const [ error, setError ] = useState('');
@@ -116,7 +115,7 @@ export default () => {
116115
/>
117116
<div css={tw`relative`}>
118117
<SpinnerOverlay visible={loading}/>
119-
<LazyCodemirrorEditor
118+
<CodemirrorEditor
120119
mode={mode}
121120
filename={hash.replace(/^#/, '')}
122121
onModeChanged={setMode}

resources/scripts/routers/ServerRouter.tsx

Lines changed: 31 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -2,63 +2,24 @@ import TransferListener from '@/components/server/TransferListener';
22
import React, { useEffect, useState } from 'react';
33
import { NavLink, Route, Switch, useRouteMatch } from 'react-router-dom';
44
import NavigationBar from '@/components/NavigationBar';
5-
import ServerConsole from '@/components/server/ServerConsole';
65
import TransitionRouter from '@/TransitionRouter';
76
import WebsocketHandler from '@/components/server/WebsocketHandler';
87
import { ServerContext } from '@/state/server';
9-
import DatabasesContainer from '@/components/server/databases/DatabasesContainer';
10-
import FileManagerContainer from '@/components/server/files/FileManagerContainer';
118
import { CSSTransition } from 'react-transition-group';
12-
import FileEditContainer from '@/components/server/files/FileEditContainer';
13-
import SettingsContainer from '@/components/server/settings/SettingsContainer';
14-
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
15-
import ScheduleEditContainer from '@/components/server/schedules/ScheduleEditContainer';
16-
import UsersContainer from '@/components/server/users/UsersContainer';
179
import Can from '@/components/elements/Can';
18-
import BackupContainer from '@/components/server/backups/BackupContainer';
1910
import Spinner from '@/components/elements/Spinner';
20-
import ScreenBlock, { NotFound, ServerError } from '@/components/elements/ScreenBlock';
11+
import { NotFound, ServerError } from '@/components/elements/ScreenBlock';
2112
import { httpErrorToHuman } from '@/api/http';
2213
import { useStoreState } from 'easy-peasy';
2314
import SubNavigation from '@/components/elements/SubNavigation';
24-
import NetworkContainer from '@/components/server/network/NetworkContainer';
2515
import InstallListener from '@/components/server/InstallListener';
26-
import StartupContainer from '@/components/server/startup/StartupContainer';
2716
import ErrorBoundary from '@/components/elements/ErrorBoundary';
2817
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
2918
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
30-
import RequireServerPermission from '@/hoc/RequireServerPermission';
31-
import ServerInstallSvg from '@/assets/images/server_installing.svg';
32-
import ServerRestoreSvg from '@/assets/images/server_restore.svg';
33-
import ServerErrorSvg from '@/assets/images/server_error.svg';
3419
import { useLocation } from 'react-router';
35-
36-
const ConflictStateRenderer = () => {
37-
const status = ServerContext.useStoreState(state => state.server.data?.status || null);
38-
const isTransferring = ServerContext.useStoreState(state => state.server.data?.isTransferring || false);
39-
40-
return (
41-
status === 'installing' || status === 'install_failed' ?
42-
<ScreenBlock
43-
title={'Running Installer'}
44-
image={ServerInstallSvg}
45-
message={'Your server should be ready soon, please try again in a few minutes.'}
46-
/>
47-
:
48-
status === 'suspended' ?
49-
<ScreenBlock
50-
title={'Server Suspended'}
51-
image={ServerErrorSvg}
52-
message={'This server is suspended and cannot be accessed.'}
53-
/>
54-
:
55-
<ScreenBlock
56-
title={isTransferring ? 'Transferring' : 'Restoring from Backup'}
57-
image={ServerRestoreSvg}
58-
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.'}
59-
/>
60-
);
61-
};
20+
import ConflictStateRenderer from '@/components/server/ConflictStateRenderer';
21+
import PermissionRoute from '@/components/elements/PermissionRoute';
22+
import routes from '@/routers/routes';
6223

6324
export default () => {
6425
const match = useRouteMatch<{ id: string }>();
@@ -74,6 +35,10 @@ export default () => {
7435
const getServer = ServerContext.useStoreActions(actions => actions.server.getServer);
7536
const clearServerState = ServerContext.useStoreActions(actions => actions.clearServerState);
7637

38+
const to = (value: string, url = false) => {
39+
return `${(url ? match.url : match.path).replace(/\/*$/, '')}/${value.replace(/^\/+/, '')}`;
40+
};
41+
7742
useEffect(() => () => {
7843
clearServerState();
7944
}, []);
@@ -105,35 +70,23 @@ export default () => {
10570
<CSSTransition timeout={150} classNames={'fade'} appear in>
10671
<SubNavigation>
10772
<div>
108-
<NavLink to={`${match.url}`} exact>Console</NavLink>
109-
<Can action={'file.*'}>
110-
<NavLink to={`${match.url}/files`}>File Manager</NavLink>
111-
</Can>
112-
<Can action={'database.*'}>
113-
<NavLink to={`${match.url}/databases`}>Databases</NavLink>
114-
</Can>
115-
<Can action={'schedule.*'}>
116-
<NavLink to={`${match.url}/schedules`}>Schedules</NavLink>
117-
</Can>
118-
<Can action={'user.*'}>
119-
<NavLink to={`${match.url}/users`}>Users</NavLink>
120-
</Can>
121-
<Can action={'backup.*'}>
122-
<NavLink to={`${match.url}/backups`}>Backups</NavLink>
123-
</Can>
124-
<Can action={'allocation.*'}>
125-
<NavLink to={`${match.url}/network`}>Network</NavLink>
126-
</Can>
127-
<Can action={'startup.*'}>
128-
<NavLink to={`${match.url}/startup`}>Startup</NavLink>
129-
</Can>
130-
<Can action={[ 'settings.*', 'file.sftp' ]} matchAny>
131-
<NavLink to={`${match.url}/settings`}>Settings</NavLink>
132-
</Can>
73+
{routes.server.filter(route => !!route.name).map((route) => (
74+
route.permission ?
75+
<Can key={route.path} action={route.permission} matchAny>
76+
<NavLink to={to(route.path, true)} exact={route.exact}>
77+
{route.name}
78+
</NavLink>
79+
</Can>
80+
:
81+
<NavLink key={route.path} to={to(route.path, true)} exact={route.exact}>
82+
{route.name}
83+
</NavLink>
84+
))}
13385
{rootAdmin &&
134-
<a href={'/admin/servers/view/' + serverId} rel="noreferrer" target={'_blank'}>
135-
<FontAwesomeIcon icon={faExternalLinkAlt}/>
136-
</a>
86+
// eslint-disable-next-line react/jsx-no-target-blank
87+
<a href={`/admin/servers/view/${serverId}`} target={'_blank'}>
88+
<FontAwesomeIcon icon={faExternalLinkAlt}/>
89+
</a>
13790
}
13891
</div>
13992
</SubNavigation>
@@ -147,47 +100,13 @@ export default () => {
147100
<ErrorBoundary>
148101
<TransitionRouter>
149102
<Switch location={location}>
150-
<Route path={`${match.path}`} component={ServerConsole} exact/>
151-
<Route path={`${match.path}/files`} exact>
152-
<RequireServerPermission permissions={'file.*'}>
153-
<FileManagerContainer/>
154-
</RequireServerPermission>
155-
</Route>
156-
<Route path={`${match.path}/files/:action(edit|new)`} exact>
157-
<Spinner.Suspense>
158-
<FileEditContainer/>
159-
</Spinner.Suspense>
160-
</Route>
161-
<Route path={`${match.path}/databases`} exact>
162-
<RequireServerPermission permissions={'database.*'}>
163-
<DatabasesContainer/>
164-
</RequireServerPermission>
165-
</Route>
166-
<Route path={`${match.path}/schedules`} exact>
167-
<RequireServerPermission permissions={'schedule.*'}>
168-
<ScheduleContainer/>
169-
</RequireServerPermission>
170-
</Route>
171-
<Route path={`${match.path}/schedules/:id`} exact>
172-
<ScheduleEditContainer/>
173-
</Route>
174-
<Route path={`${match.path}/users`} exact>
175-
<RequireServerPermission permissions={'user.*'}>
176-
<UsersContainer/>
177-
</RequireServerPermission>
178-
</Route>
179-
<Route path={`${match.path}/backups`} exact>
180-
<RequireServerPermission permissions={'backup.*'}>
181-
<BackupContainer/>
182-
</RequireServerPermission>
183-
</Route>
184-
<Route path={`${match.path}/network`} exact>
185-
<RequireServerPermission permissions={'allocation.*'}>
186-
<NetworkContainer/>
187-
</RequireServerPermission>
188-
</Route>
189-
<Route path={`${match.path}/startup`} component={StartupContainer} exact/>
190-
<Route path={`${match.path}/settings`} component={SettingsContainer} exact/>
103+
{routes.server.map(({ path, permission, component: Component }) => (
104+
<PermissionRoute key={path} permission={permission} path={to(path)} exact>
105+
<Spinner.Suspense>
106+
<Component/>
107+
</Spinner.Suspense>
108+
</PermissionRoute>
109+
))}
191110
<Route path={'*'} component={NotFound}/>
192111
</Switch>
193112
</TransitionRouter>
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import React, { lazy } from 'react';
2+
import ServerConsole from '@/components/server/ServerConsole';
3+
import DatabasesContainer from '@/components/server/databases/DatabasesContainer';
4+
import ScheduleContainer from '@/components/server/schedules/ScheduleContainer';
5+
import UsersContainer from '@/components/server/users/UsersContainer';
6+
import BackupContainer from '@/components/server/backups/BackupContainer';
7+
import NetworkContainer from '@/components/server/network/NetworkContainer';
8+
import StartupContainer from '@/components/server/startup/StartupContainer';
9+
10+
const FileManagerContainer = lazy(() => import('@/components/server/files/FileManagerContainer'));
11+
const FileEditContainer = lazy(() => import('@/components/server/files/FileEditContainer'));
12+
const ScheduleEditContainer = lazy(() => import('@/components/server/schedules/ScheduleEditContainer'));
13+
const SettingsContainer = lazy(() => import('@/components/server/settings/SettingsContainer'));
14+
15+
interface ServerRouteDefinition {
16+
path: string;
17+
permission: string | string[] | null;
18+
// If undefined is passed this route is still rendered into the router itself
19+
// but no navigation link is displayed in the sub-navigation menu.
20+
name: string | undefined;
21+
component: React.ComponentType;
22+
// The default for "exact" is assumed to be "true" unless you explicitly
23+
// pass it as false.
24+
exact?: boolean;
25+
}
26+
27+
interface Routes {
28+
server: ServerRouteDefinition[];
29+
}
30+
31+
export default {
32+
server: [
33+
{
34+
path: '/',
35+
permission: null,
36+
name: 'Console',
37+
component: ServerConsole,
38+
exact: true,
39+
},
40+
{
41+
path: '/files',
42+
permission: 'file.*',
43+
name: 'Files',
44+
component: FileManagerContainer,
45+
},
46+
{
47+
path: '/files/:action(edit|new)',
48+
permission: 'file.*',
49+
name: undefined,
50+
component: FileEditContainer,
51+
},
52+
{
53+
path: '/databases',
54+
permission: 'database.*',
55+
name: 'Databases',
56+
component: DatabasesContainer,
57+
},
58+
{
59+
path: '/schedules',
60+
permission: 'schedule.*',
61+
name: 'Schedules',
62+
component: ScheduleContainer,
63+
},
64+
{
65+
path: '/schedules/:id',
66+
permission: 'schedule.*',
67+
name: undefined,
68+
component: ScheduleEditContainer,
69+
},
70+
{
71+
path: '/users',
72+
permission: 'user.*',
73+
name: 'Users',
74+
component: UsersContainer,
75+
},
76+
{
77+
path: '/backups',
78+
permission: 'backup.*',
79+
name: 'Backups',
80+
component: BackupContainer,
81+
},
82+
{
83+
path: '/network',
84+
permission: 'allocation.*',
85+
name: 'Network',
86+
component: NetworkContainer,
87+
},
88+
{
89+
path: '/startup',
90+
permission: 'startup.*',
91+
name: 'Startup',
92+
component: StartupContainer,
93+
},
94+
{
95+
path: '/settings',
96+
permission: [ 'settings.*', 'file.sftp' ],
97+
name: 'Settings',
98+
component: SettingsContainer,
99+
},
100+
],
101+
} as Routes;

0 commit comments

Comments
 (0)