Layout with sidebar.

This commit is contained in:
richburdon
2020-05-23 19:01:17 -04:00
parent b1fd59fdce
commit 3057bc0f1d
24 changed files with 1101 additions and 71 deletions
+7 -2
View File
@@ -1,6 +1,11 @@
{
"server": "http://localhost",
"publicUrl": "/app",
"port": 4000,
"path": "/api"
"path": "/api",
"app": {
"org": "DxOS",
"theme": "dark",
"title": "Console",
"website": "https://dxos.org"
}
}
+11 -3
View File
@@ -29,7 +29,13 @@
"dependencies": {
"@apollo/react-hooks": "^3.1.5",
"@babel/runtime": "^7.8.7",
"apollo-boost": "^0.4.9",
"@dxos/gem-core": "^1.0.0-beta.11",
"@material-ui/core": "^4.10.0",
"@material-ui/icons": "^4.9.1",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
"apollo-link-http": "^1.5.17",
"clsx": "^1.1.0",
"debug": "^4.1.1",
"graphql-tag": "^2.10.3",
"lodash.defaultsdeep": "^4.6.1",
@@ -52,9 +58,10 @@
"babel-plugin-inline-import": "^3.0.0",
"dotenv-webpack": "^1.8.0",
"eslint": "^6.7.2",
"eslint-loader": "^3.0.3",
"eslint-config-semistandard": "^15.0.0",
"eslint-config-standard": "^14.1.1",
"eslint-config-standard-jsx": "^8.1.0",
"eslint-loader": "^3.0.3",
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jest": "^23.13.1",
@@ -79,7 +86,8 @@
"parser": "babel-eslint",
"extends": [
"plugin:jest/recommended",
"semistandard"
"semistandard",
"standard-jsx"
],
"plugins": [
"babel"
+27
View File
@@ -0,0 +1,27 @@
//
// Copyright 2020 DxOS
//
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import config from '../config.json';
const defaultServer = `${window.location.protocol}//${window.location.hostname}`;
const { server = defaultServer, port = 80, path = '/graphql' } = config;
// TODO(burdon): Authentication: send signed message to server (from client wallet).
// https://www.apollographql.com/docs/react/networking/authentication/
// https://www.apollographql.com/docs/link/
const link = createHttpLink({
uri: `${server}:${port}${path}`
});
// https://www.apollographql.com/docs/react/api/apollo-client/
export const client = new ApolloClient({
cache: new InMemoryCache(),
link
});
@@ -0,0 +1,69 @@
//
// Copyright 2020 DxOS
//
import React from 'react';
import { makeStyles } from '@material-ui/core';
import MuiAppBar from '@material-ui/core/AppBar';
import MuiLink from '@material-ui/core/Link';
import Toolbar from '@material-ui/core/Toolbar';
import Typography from '@material-ui/core/Typography';
import blueGrey from '@material-ui/core/colors/blueGrey';
import PublicIcon from '@material-ui/icons/Public';
import DxOSIcon from '../icons/DXOS';
const useStyles = makeStyles((theme) => ({
offset: theme.mixins.denseToolbar,
logo: {
paddingBottom: 2,
marginTop: 4,
marginRight: theme.spacing(2),
color: '#333',
'& svg': {
width: 64,
height: 32
}
},
title: {
display: 'flex',
flex: 1
},
link: {
color: blueGrey[900]
}
}));
const AppBar = ({ config }) => {
const classes = useStyles();
return (
<>
<MuiAppBar position="fixed">
<Toolbar variant="dense">
<MuiLink href="/console">
<div className={classes.logo}>
<DxOSIcon />
</div>
</MuiLink>
<div className={classes.title}>
<Typography variant="h6">{config.app.title}</Typography>
</div>
<div>
<MuiLink href={config.app.website} className={classes.link} rel="noreferrer" target="_blank">
<PublicIcon />
</MuiLink>
</div>
</Toolbar>
</MuiAppBar>
<div className={classes.offset} />
</>
);
};
export default AppBar;
@@ -2,42 +2,66 @@
// Copyright 2020 DxOS
//
import React, { useEffect, useState } from 'react';
import React, { useContext } from 'react';
import { makeStyles } from '@material-ui/core';
import { useStatusReducer } from '../hooks';
import { FullScreen } from '@dxos/gem-core';
import config from '../../config.json';
import StatusBar from '../containers/StatusBar';
import AppBar from './AppBar';
import Sidebar from './Sidebar';
import { ConsoleContext } from '../hooks';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'column',
flex: 1
},
container: {
display: 'flex',
flexDirection: 'row',
flex: 1
},
sidebar: {
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
width: 200,
borderRight: `1px solid ${theme.palette.primary.dark}`
},
main: {
display: 'flex',
flex: 1
},
cooter: {
display: 'flex',
flexShrink: 0
}
}));
// TODO(burdon): Factor out LoadingIndicator.
const Layout = ({ children }) => {
const [{ loading, error = '' }] = useStatusReducer();
const [isLoading, setLoading] = useState(loading);
useEffect(() => {
let t;
if (loading) {
setLoading(loading);
t = setTimeout(() => {
setLoading(false);
}, 1000);
}
return () => clearTimeout(t);
}, [loading]);
const classes = useStyles();
const { modules } = useContext(ConsoleContext);
return (
<div>
<div>
{children}
<FullScreen>
<div className={classes.root}>
<AppBar config={config} />
<div className={classes.container}>
<div className={classes.sidebar}>
<Sidebar modules={modules} />
</div>
<div className={classes.main}>
{children}
</div>
</div>
<div className={classes.footer}>
<StatusBar />
</div>
</div>
<div>
{error && (
<span>{String(error)}</span>
)}
{isLoading && (
<span>Loading</span>
)}
</div>
</div>
</FullScreen>
);
};
@@ -0,0 +1,61 @@
//
// Copyright 2020 DxOS
//
import clsx from 'clsx';
import React from 'react';
import { makeStyles } from '@material-ui/core';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flex: 1,
flexDirection: 'column',
justifyContent: 'space-between'
},
list: {
padding: 0
},
icon: {
color: theme.palette.grey[500]
},
selected: {
color: theme.palette.primary.main
}
}));
const Sidebar = ({ modules: { services, settings } }) => {
const classes = useStyles();
// TODO(burdon): Change.
const router = {};
const Modules = ({ modules }) => (
<List aria-label="items" className={classes.list}>
{modules.map(({ path, title, icon: Icon }) => (
<ListItem button selected={path === router.pathname} key={path} onClick={() => router.push(path)}>
<ListItemIcon classes={{ root: classes.icon }}>
<Icon className={clsx(classes.icon, path === router.pathname && classes.selected)} />
</ListItemIcon>
<ListItemText primary={title} />
</ListItem>
))}
</List>
);
return (
<div className={classes.root}>
<Modules modules={services} />
<Modules modules={settings} />
</div>
);
};
export default Sidebar;
@@ -7,9 +7,7 @@ import defaultsDeep from 'lodash.defaultsdeep';
import ErrorBoundary from '../components/ErrorBoundary';
import { statusReducer, SET_STATUS } from '../hooks/status';
import { ConsoleContext } from '../hooks';
import { ConsoleContext, statusReducer, SET_STATUS } from '../hooks';
const defaultState = {};
@@ -29,11 +27,12 @@ const appReducer = (state, action) => ({
* Wraps children with a React ErrorBoundary component, which catches runtime errors and enables reset.
*
* @param {function} children
* @param {function} modules
* @param {Object} [initialState]
* @param {function} [errorHandler]
* @returns {function}
*/
const ConsoleContextProvider = ({ children, initialState = {}, errorHandler }) => {
const ConsoleContextProvider = ({ children, modules, initialState = {}, errorHandler }) => {
const [state, dispatch] = useReducer(appReducer, defaultsDeep({}, initialState, defaultState));
const { errors: { exceptions = [] } = {} } = state[SET_STATUS] || {};
@@ -53,7 +52,7 @@ const ConsoleContextProvider = ({ children, initialState = {}, errorHandler }) =
}
return (
<ConsoleContext.Provider value={{ state, dispatch }}>
<ConsoleContext.Provider value={{ modules, state, dispatch }}>
<ErrorBoundary>
{children}
</ErrorBoundary>
+16 -18
View File
@@ -2,34 +2,32 @@
// Copyright 2020 DxOS
//
import { ApolloProvider } from '@apollo/react-hooks';
import ApolloClient from 'apollo-boost';
import React from 'react';
import { ApolloProvider } from '@apollo/react-hooks';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import config from '../../config.json';
import { createTheme } from '../theme';
import { client } from '../client';
import modules from '../modules';
import Status from './Status';
import config from '../../config.json';
import Layout from '../components/Layout';
import ConsoleContextProvider from './ConsoleContextProvider';
const { server, port = 80, path } = config;
// TODO(burdon): Authentication:
// https://www.apollographql.com/docs/react/networking/authentication/
// TODO(burdon): Configure from server.
// TODO(burdon): Configure error handling for server errors.
const client = new ApolloClient({
uri: `${server}:${port}${path}`
});
const Main = () => {
return (
<ApolloProvider client={client}>
<ConsoleContextProvider>
<Layout>
<Status />
</Layout>
<ConsoleContextProvider modules={modules}>
<ThemeProvider theme={createTheme(config.app.theme)}>
<CssBaseline />
<Layout>
<Status />
</Layout>
</ThemeProvider>
</ConsoleContextProvider>
</ApolloProvider>
);
@@ -0,0 +1,96 @@
//
// Copyright 2020 DxOS
//
import clsx from 'clsx';
import React, { useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core';
import ErrorIcon from '@material-ui/icons/Error';
import LoadingIcon from '@material-ui/icons/Wifi';
import RunningIcon from '@material-ui/icons/CheckCircle';
import grey from '@material-ui/core/colors/grey';
import green from '@material-ui/core/colors/green';
import red from '@material-ui/core/colors/red';
import config from '../../config.json';
import { version } from '../../package.json';
import { useStatusReducer } from '../hooks';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'row',
flex: 1,
justifyContent: 'space-around',
backgroundColor: grey[900],
color: '#EEE',
height: 32,
padding: 4
},
left: {
width: 160
},
right: {
width: 160,
textAlign: 'right'
},
center: {
flex: 1,
textAlign: 'center'
},
icon: {
margin: '0 2px'
},
error: {
color: red[500]
},
running: {
color: green[500]
},
loading:{
color: theme.palette.primary.dark
}
}));
const StatusBar = () => {
const classes = useStyles();
const [{ loading, error }] = useStatusReducer();
const [isLoading, setLoading] = useState(loading);
useEffect(() => {
let t;
if (loading) {
setLoading(loading);
t = setTimeout(() => {
setLoading(false);
}, 1000);
}
return () => clearTimeout(t);
}, [loading]);
const StatusIcon = ({ error }) => {
if (error) {
return (
<ErrorIcon className={clsx(classes.icon, classes.error)} title={String(error)} />
);
} else {
return (
<RunningIcon className={clsx(classes.icon, classes.running)} />
);
}
};
return (
<div className={classes.root}>
<div className={classes.left} />
<div className={classes.center}>(c) {config.app.org} {version}</div>
<div className={classes.right}>
<LoadingIcon className={clsx(classes.icon, isLoading && classes.loading)} />
<StatusIcon error={error} />
</div>
</div>
);
};
export default StatusBar;
+40
View File
@@ -0,0 +1,40 @@
//
// Copyright 2020 DxOS
//
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
const Icon = (props) => (
<SvgIcon {...props} viewBox="0 0 512 256">
<g transform="matrix(1,0,0,1,-187.374,-7.5)">
<g transform="matrix(0.936373,0,0,0.906495,295.947,135.5)">
<path
d="M0,-0.233C0,-27.435 -16.74,-48.591 -48.359,-48.591L-76.722,-48.591L-76.722,48.358L-48.59,48.358C-17.902,48.358 0,26.271 0,-0.233M-109.736,77.419L-109.736,-77.652L-48.59,-77.652C0,-77.652 33.711,-46.731 33.711,-0.233C33.711,46.73 0,77.419 -48.359,77.419L-109.736,77.419Z"
/>
</g>
<g transform="matrix(0.936373,0,0,0.906495,530.058,135.499)">
<path
d="M0,0.002C0,-29.06 -18.367,-50.914 -46.964,-50.914C-75.793,-50.914 -94.16,-29.06 -94.16,0.002C-94.16,28.83 -75.793,50.917 -46.964,50.917C-18.367,50.917 0,28.83 0,0.002M-128.104,0.002C-128.104,-46.729 -93.927,-80.208 -46.964,-80.208C-0.233,-80.208 33.942,-46.729 33.942,0.002C33.942,46.731 -0.233,80.21 -46.964,80.21C-93.927,80.21 -128.104,46.731 -128.104,0.002"
/>
</g>
<g transform="matrix(0.936373,0,0,0.906495,573.603,85.3412)">
<path
d="M0,110.898L18.134,85.092C29.062,96.484 46.034,106.249 67.422,106.249C85.789,106.249 94.392,98.111 94.392,89.044C94.392,62.075 5.115,80.907 5.115,22.551C5.115,-3.255 27.434,-24.644 63.935,-24.644C88.579,-24.644 109.039,-17.204 124.383,-3.022L106.017,21.622C93.462,9.997 76.723,4.65 60.913,4.65C46.964,4.65 39.059,10.695 39.059,19.994C39.059,44.406 128.103,27.898 128.103,85.789C128.103,114.153 107.644,135.542 66.028,135.542C36.036,135.542 14.648,125.544 0,110.898"
/>
</g>
<g transform="matrix(0.936373,0,0,0.906495,368.158,65.109)">
<path
d="M0,56.844L-36.496,0L36.496,0L0,56.844Z"
/>
</g>
<g transform="matrix(0.936373,0,0,0.906495,368.158,205.681)">
<path
d="M0,-56.845L-36.496,0L36.496,0L0,-56.845Z"
/>
</g>
</g>
</SvgIcon>
);
export default Icon;
+37
View File
@@ -0,0 +1,37 @@
//
// Copyright 2020 DxOS
//
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
// TODO(burdon): Fixed color?
const Icon = (props) => (
<SvgIcon {...props} viewBox="0 0 256 256">
<g transform="matrix(1,0,0,1,-160,-124)">
<path
d="M282.254,147.134L195.254,194.589C191.399,196.692 189,200.732 189,205.124L189,298.876C189,303.268 191.399,307.308 195.254,309.411L282.254,356.866C285.836,358.819 290.164,358.819 293.746,356.866L380.746,309.411C384.601,307.308 387,303.268 387,298.876L387,205.124C387,200.732 384.601,196.692 380.746,194.589L293.746,147.134C290.164,145.181 285.836,145.181 282.254,147.134Z"
style={{ fill: 'none', fillRule: 'nonzero', stroke: 'rgb(0,68,121)', strokeWidth: '12px' }}
/>
<path
d="M288,252L216,216"
style={{ fill: 'none', fillRule: 'nonzero', stroke: 'rgb(0,68,121)', strokeWidth: '8px', strokeLinejoin: 'round' }}
/>
<path
d="M216,288L288,252"
style={{ fill: 'none', fillRule: 'nonzero', stroke: 'rgb(0,68,121)', strokeWidth: '8px', strokeLinejoin: 'round' }}
/>
<path
d="M360,288L288,252"
style={{ fill: 'none', fillRule: 'nonzero', stroke: 'rgb(0,68,121)', strokeWidth: '8px', strokeLinejoin: 'round' }}
/>
<path
d="M360,216L288,252"
style={{ fill: 'none', fillRule: 'nonzero', stroke: 'rgb(0,68,121)', strokeWidth: '8px', strokeLinejoin: 'round' }}
/>
</g>
</SvgIcon>
);
export default Icon;
+60
View File
@@ -0,0 +1,60 @@
//
// Copyright 2020 DxOS
//
import AppsIcon from '@material-ui/icons/Apps';
import BotsIcon from '@material-ui/icons/Android';
import StatsIcon from '@material-ui/icons/Equalizer';
import RegistryIcon from '@material-ui/icons/Language';
import IPFSIcon from '@material-ui/icons/GraphicEq';
import ConfigIcon from '@material-ui/icons/Settings';
import SignalIcon from '@material-ui/icons/Traffic';
import ServicesIcon from '@material-ui/icons/Storage';
export default {
services: [
{
path: '/console/status',
title: 'Status',
icon: StatsIcon
},
{
path: '/console/wns',
title: 'WNS',
icon: RegistryIcon
},
{
path: '/console/apps',
title: 'Apps',
icon: AppsIcon
},
{
path: '/console/bots',
title: 'Bots',
icon: BotsIcon
},
{
path: '/console/signal',
title: 'Signal Server',
icon: SignalIcon
},
{
path: '/console/ipfs',
title: 'IPFS',
icon: IPFSIcon
}
],
settings: [
{
path: '/console/metadata',
title: 'Metadata',
icon: ServicesIcon
},
{
path: '/console/config',
title: 'Config',
icon: ConfigIcon
}
]
};
+46
View File
@@ -0,0 +1,46 @@
//
// Copyright 2019 DxOS
//
import { createMuiTheme } from '@material-ui/core/styles';
import teal from '@material-ui/core/colors/teal';
import orange from '@material-ui/core/colors/orange';
export const createTheme = (theme) => createMuiTheme({
// https://stackoverflow.com/questions/60567673/reactjs-material-ui-theme-mixins-toolbar-offset-is-not-adapting-when-toolbar
mixins: {
denseToolbar: {
minHeight: 48
}
},
// https://material-ui.com/customization/globals/#default-props
props: {
MuiButtonBase: {
disableRipple: true
}
},
// https://material-ui.com/customization/palette/
palette: theme === 'dark' ? {
type: 'dark',
primary: orange
} : {
primary: teal
},
// https://material-ui.com/customization/theming/#theme-configuration-variables
// https://material-ui.com/customization/globals/
overrides: {
MuiCssBaseline: {
'@global': {
body: {
margin: 0,
overflow: 'hidden'
}
}
}
}
});
+1 -1
View File
@@ -1,7 +1,7 @@
{
"build": {
"name": "@dxos/console-client",
"buildDate": "2020-05-23T20:00:53.818Z",
"buildDate": "2020-05-23T22:14:40.153Z",
"version": "1.0.0-beta.0"
}
}
@@ -46,10 +46,8 @@ module.exports = {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
// name: 'vendor',
name(module) {
name (module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
// return `vendor-${packageName.replace('@', '')}`;
if (packageName.startsWith('@dxos')) {
return 'dxos';
+1 -1
View File
@@ -16,7 +16,7 @@ module.exports = merge(commonConfig, {
new HtmlWebPackPlugin({
template: './public/index.html',
templateParameters: {
title: 'Planner'
title: 'DxOS Console'
}
})
]