Server Console App directly from the server.

Use Moustache to create template and set config in page.
Use babel plugins to process GQL (and fix GQL queries).
Added service type.
This commit is contained in:
richburdon
2020-05-27 19:09:20 -04:00
parent 0b0234761f
commit eb6bcd9c95
77 changed files with 586 additions and 210 deletions
+44
View File
@@ -0,0 +1,44 @@
//
// Copyright 2020 DxOS.org
//
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createResolvers } from './resolvers';
const defaultServer = `${window.location.protocol}//${window.location.hostname}`;
export const graphqlApi = config => {
const { api: { server = defaultServer, port = 80, path = '/graphql' } } = config;
return `${server}:${port}${path}`;
};
/**
* Craetes an Apollo client.
* @param {Object} config
* @returns {ApolloClient}
*/
export const clientFactory = config => {
// https://www.apollographql.com/docs/link/
const link = createHttpLink({
uri: graphqlApi(config),
// TODO(burdon): Authentication: send signed message to server (from client wallet).
// https://www.apollographql.com/docs/react/networking/authentication/
headers: {
authorization: 'HALO_TOKEN'
}
});
// https://www.apollographql.com/docs/react/api/apollo-client/
return new ApolloClient({
connectToDevTools: true,
cache: new InMemoryCache(),
resolvers: createResolvers(config),
link
});
};
@@ -0,0 +1,76 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { makeStyles } from '@material-ui/core';
import MuiAppBar from '@material-ui/core/AppBar';
import Link 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 GraphQLIcon from '@material-ui/icons/Adb';
import DxOSIcon from '../icons/DXOS';
import { graphqlApi } from '../client';
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>
<Link href="/">
<div className={classes.logo}>
<DxOSIcon />
</div>
</Link>
<div className={classes.title}>
<Typography variant="h6">{config.app.title}</Typography>
</div>
<div>
<Link
className={classes.link}
href={graphqlApi(config)}
rel="noreferrer"
target="_blank"
title="Console GraphQL"
>
<GraphQLIcon />
</Link>
</div>
</Toolbar>
</MuiAppBar>
<div className={classes.offset} />
</>
);
};
export default AppBar;
@@ -0,0 +1,35 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import OpenIcon from '@material-ui/icons/OpenInBrowser';
import StartIcon from '@material-ui/icons/PlayCircleOutline';
import StopIcon from '@material-ui/icons/HighlightOff';
import IconButton from '@material-ui/core/IconButton';
const ControlButtons = ({ onStart, onStop, onOpen }) => {
return (
<div>
{onStart && (
<IconButton onClick={onStart} title="Restart">
<StartIcon />
</IconButton>
)}
{onStop && (
<IconButton onClick={onStop} title="Stop">
<StopIcon />
</IconButton>
)}
{onOpen && (
<IconButton onClick={onOpen} title="Open console">
<OpenIcon />
</IconButton>
)}
</div>
);
};
export default ControlButtons;
@@ -0,0 +1,49 @@
//
// Copyright 2020 DxOS.org
//
import React, { useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Alert from '@material-ui/lab/Alert';
import AlertTitle from '@material-ui/lab/AlertTitle';
import Snackbar from '@material-ui/core/Snackbar';
const useStyles = makeStyles(() => ({
root: {
marginBottom: 48
},
alert: {
minWidth: 400
}
}));
const Error = ({ error, ...rest }) => {
const classes = useStyles();
const [message, setMessage] = useState(error);
useEffect(() => {
setMessage(error ? error.message || String(error) : null);
}, [error]);
const handleClose = () => {
setMessage(null);
};
return (
<Snackbar
classes={{ root: classes.root }}
open={Boolean(message)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
TransitionProps={{ exit: false }}
autoHideDuration={1000}
>
<Alert classes={{ root: classes.alert }} severity="error" {...rest} onClose={handleClose}>
<AlertTitle>Error</AlertTitle>
<div>{message}</div>
</Alert>
</Snackbar>
);
};
export default Error;
@@ -0,0 +1,57 @@
//
// Copyright 2020 DxOS.org
//
import React, { Component } from 'react';
import Typography from '@material-ui/core/Typography';
/**
* Root-level error boundary.
* https://reactjs.org/docs/error-boundaries.html
*
* NOTE: Must currently be a Component.
* https://reactjs.org/docs/hooks-faq.html#do-hooks-cover-all-use-cases-for-classes
*/
class ErrorBoundary extends Component {
static getDerivedStateFromError (error) {
return { error };
}
state = {
error: null
};
componentDidCatch (error, errorInfo) {
const { onError } = this.props;
// TODO(burdon): Show error indicator.
// TODO(burdon): Logging service; output error file.
onError(error);
}
render () {
const { children } = this.props;
const { error } = this.state;
if (error) {
return (
<div>
<Typography>Error</Typography>
<pre>{String(error)}</pre>
</div>
);
}
return (
<div>
{children}
</div>
);
}
}
ErrorBoundary.defaultProps = {
onError: console.warn
};
export default ErrorBoundary;
@@ -0,0 +1,39 @@
//
// Copyright 2020 DxOS.org
//
import isObject from 'lodash.isobject';
import omit from 'lodash.omit';
import transform from 'lodash.transform';
import React from 'react';
import { makeStyles } from '@material-ui/core';
import { JsonTreeView } from '@dxos/react-ux';
const useStyles = makeStyles(() => ({
root: {
flex: 1
}
}));
/**
* Remove Apollo __typename directive.
* @param {Object} data
* @returns {Object}
*/
const removeTypename = data => transform(data, (result, value, key) => {
if (key !== '__typename') {
result[key] = isObject(value) ? ('__typename' in value ? omit(value, '__typename') : value) : value;
}
}, {});
const Json = ({ data }) => {
const classes = useStyles();
// TODO(burdon): Bug expands when updated.
return (
<JsonTreeView className={classes.root} data={removeTypename(data)} />
);
};
export default Json;
+118
View File
@@ -0,0 +1,118 @@
//
// Copyright 2020 DxOS.org
//
import clsx from 'clsx';
import moment from 'moment';
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flex: 1,
overflow: 'hidden'
},
log: {
display: 'flex',
// Pin to bottom (render in time order).
flexDirection: 'column-reverse',
overflow: 'scroll',
padding: theme.spacing(1),
'& div': {
fontSize: 16,
fontFamily: 'monospace',
whiteSpace: 'nowrap'
}
},
level: {
display: 'inline-block',
width: 60,
marginRight: 8,
color: theme.palette.grey[500]
},
level_warn: {
color: theme.palette.warning.main
},
level_error: {
color: theme.palette.error.main
},
ts: {
marginRight: 8,
color: theme.palette.primary[500]
}
}));
const Log = ({ log = [] }) => {
const classes = useStyles();
const levels = {
'I': { label: 'INFO', className: classes.level_info },
'W': { label: 'WARN', className: classes.level_warn },
'E': { label: 'ERROR', className: classes.level_error }
};
// TODO(burdon): Parse in backend and normalize numbers.
const Line = ({ message }) => {
// https://regex101.com/
const patterns = [
{
// 2020-03-30T18:02:43.189Z bot-factory
pattern: /()(.+Z)\s+(.+)/,
transform: ([datetime]) => moment(datetime)
},
{
// I[2020-03-30|15:29:05.436] Executed block module=state height=11533 validTxs=0 invalidTxs=0
pattern: /(.)\[(.+)\|(.+)]\s+(.+)/,
transform: ([date, time]) => moment(`${date} ${time}`)
},
{
// [cors] 2020/03/30 15:28:53 Handler: Actual request
pattern: /\[(\w+)] (\S+) (\S+)\s+(.+)/,
transform: ([date, time]) => moment(`${date.replace(/\//g, '-')} ${time}`)
}
];
patterns.some(({ pattern, transform }) => {
const match = message.match(pattern);
if (match) {
const [, level = 'I', ...rest] = match;
const datetime = transform(rest).format('YYYY-MM-DD HH:mm:ss');
const text = match[match.length - 1];
const { label, className } = levels[level] || levels['I'];
const pkg = levels[level] ? '' : `[${level}]: `;
message = (
<>
<span className={classes.ts}>{datetime}</span>
<span className={clsx(classes.level, className)}>{label || level}</span>
<span>{pkg}{text}</span>
</>
);
return true;
}
return false;
});
return (
<div>{message}</div>
);
};
return (
<div className={classes.root}>
<div className={classes.log}>
{log.map((line, i) => <Line key={i} message={line} />)}
</div>
</div>
);
};
export default Log;
@@ -0,0 +1,54 @@
//
// Copyright 2020 DxOS.org.org
//
import React from 'react';
import Link from '@material-ui/core/Link';
import { getServiceUrl } from '../util/config';
/**
* Render IPFS links in package.
* @param {Object} config
* @param {string} [type]
* @param {string} pkg
*/
const PackageLink = ({ config, type, pkg }) => {
// TODO(burdon): Pass in expected arg types.
if (typeof pkg === 'string') {
const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${pkg}` });
return <Link href={ipfsUrl} target="ipfs">{pkg}</Link>;
}
// eslint-disable-next-line default-case
switch (type) {
case 'wrn:bot': {
const packageLinks = [];
Object.keys(pkg).forEach((platform, i) => {
Object.keys(pkg[platform]).forEach(arch => {
const cid = pkg[platform][arch];
const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${cid}` });
packageLinks.push(
<Link
key={`${cid}`}
href={ipfsUrl}
title={cid}
target="ipfs"
>
{platform}/{arch}: {cid}
</Link>
);
});
});
return (
<>{packageLinks}</>
);
}
}
return null;
};
export default PackageLink;
@@ -0,0 +1,37 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(() => ({
root: {
display: 'flex',
flexDirection: 'column',
flex: 1,
overflow: 'hidden'
},
container: {
display: 'flex',
flexDirection: 'column',
flex: 1,
overflowY: 'scroll'
}
}));
const Panel = ({ toolbar, children }) => {
const classes = useStyles();
return (
<div className={classes.root}>
{toolbar}
<div className={classes.container}>
{children}
</div>
</div>
);
};
export default Panel;
@@ -0,0 +1,65 @@
//
// Copyright 2020 DxOS.org
//
import clsx from 'clsx';
import React from 'react';
import { useHistory, useParams } from 'react-router';
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',
// backgroundColor: theme.palette.grey[100]
},
list: {
padding: 0
},
icon: {
minWidth: 40,
color: theme.palette.grey[500]
},
selected: {
color: theme.palette.primary.main
}
}));
const Sidebar = ({ modules: { services, settings } }) => {
const classes = useStyles();
const history = useHistory();
const { module } = useParams();
const isSelected = path => path === `/${module}`;
const Modules = ({ modules }) => (
<List aria-label="items" className={classes.list}>
{modules.map(({ path, title, icon: Icon }) => (
<ListItem button selected={isSelected(path)} key={path} onClick={() => history.push(path)}>
<ListItemIcon classes={{ root: classes.icon }}>
<Icon className={clsx(classes.icon, isSelected(path) && classes.selected)} />
</ListItemIcon>
<ListItemText primary={title} />
</ListItem>
))}
</List>
);
return (
<div className={classes.root}>
<Modules modules={services} />
<Modules modules={settings} />
</div>
);
};
export default Sidebar;
@@ -0,0 +1,41 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { makeStyles } from '@material-ui/core';
import MuiTable from '@material-ui/core/Table';
import TableContainer from '@material-ui/core/TableContainer';
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flex: 1,
overflowY: 'scroll',
backgroundColor: theme.palette.background.paper
},
table: {
tableLayout: 'fixed',
'& th': {
fontVariant: 'all-small-caps',
fontSize: 18,
cursor: 'ns-resize'
}
}
}));
const Table = ({ children }) => {
const classes = useStyles();
return (
<TableContainer className={classes.root}>
<MuiTable stickyHeader size="small" className={classes.table}>
{children}
</MuiTable>
</TableContainer>
);
};
export default Table;
@@ -0,0 +1,38 @@
//
// Copyright 2020 DxOS.org
//
import clsx from 'clsx';
import React from 'react';
import MuiTableCell from '@material-ui/core/TableCell';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(() => ({
small: {
width: 160
}
}));
const TableCell = ({ children, size, monospace = false, title, ...rest }) => {
const classes = useStyles();
return (
<MuiTableCell
{...rest}
className={clsx(size && classes[size])}
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontFamily: monospace ? 'monospace' : 'inherit',
fontSize: monospace ? 14 : 'inherit'
}}
title={title}
>
{children}
</MuiTableCell>
);
};
export default TableCell;
@@ -0,0 +1,32 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { makeStyles } from '@material-ui/core';
import MuiToolbar from '@material-ui/core/Toolbar';
const useStyles = makeStyles(theme => ({
toolbar: {
display: 'flex',
justifyContent: 'space-between',
whiteSpace: 'nowrap',
'& > button': {
margin: theme.spacing(0.5)
}
}
}));
// TODO(burdon): Tabs.
const Toolbar = ({ children }) => {
const classes = useStyles();
return (
<MuiToolbar disableGutters className={classes.toolbar}>
{children}
</MuiToolbar>
);
};
export default Toolbar;
@@ -0,0 +1,61 @@
//
// Copyright 2020 DxOS.org
//
import React, { useEffect, useReducer } from 'react';
import defaultsDeep from 'lodash.defaultsdeep';
import ErrorBoundary from '../components/ErrorBoundary';
import { ConsoleContext, statusReducer, STATUS, SET_STATUS } from '../hooks';
const defaultState = {};
/**
* Actions reducer.
* https://reactjs.org/docs/hooks-reference.html#usereducer
* @param {Object} state
* @param {string} action
*/
const appReducer = (state, action) => ({
[STATUS]: statusReducer(state[STATUS], action)
});
/**
* Creates the Console framework context, which provides the global UX state.
* Wraps children with a React ErrorBoundary component, which catches runtime errors and enables reset.
*
* @param {function} children
* @param {Object} config
* @param {Object} modules
* @param {Object} [initialState]
* @param {function} [errorHandler]
* @returns {function}
*/
const ConsoleContextProvider = ({ children, config, modules, initialState = {}, errorHandler }) => {
const [state, dispatch] = useReducer(appReducer, defaultsDeep({}, initialState, defaultState));
// Bind the error handler.
if (errorHandler) {
useEffect(() => {
errorHandler.on('error', error => {
dispatch({
type: SET_STATUS,
payload: {
error
}
});
});
}, []);
}
return (
<ConsoleContext.Provider value={{ config, modules, state, dispatch }}>
<ErrorBoundary>
{children}
</ErrorBoundary>
</ConsoleContext.Provider>
);
};
export default ConsoleContextProvider;
@@ -0,0 +1,74 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { makeStyles } from '@material-ui/core';
import { FullScreen } from '@dxos/gem-core';
import { ConsoleContext } from '../hooks';
import AppBar from '../components/AppBar';
import Sidebar from '../components/Sidebar';
import StatusBar from './StatusBar';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'column',
flex: 1,
overflow: 'hidden'
},
container: {
display: 'flex',
flexDirection: 'row',
flex: 1,
overflow: 'hidden'
},
main: {
display: 'flex',
flex: 1,
overflow: 'hidden'
},
sidebar: {
display: 'flex',
flexDirection: 'column',
flexShrink: 0,
width: 200,
borderRight: `1px solid ${theme.palette.divider}`
},
footer: {
display: 'flex',
flexShrink: 0
}
}));
/**
* Main layout for app.
*/
const Layout = ({ children }) => {
const classes = useStyles();
const { config, modules } = useContext(ConsoleContext);
return (
<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>
</FullScreen>
);
};
export default Layout;
@@ -0,0 +1,68 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { HashRouter, Redirect, Route, Switch } from 'react-router-dom';
import { ApolloProvider } from '@apollo/react-hooks';
import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import { ErrorHandler } from '@dxos/debug';
import { build } from '../version.json';
import { clientFactory } from '../client';
import { createTheme } from '../theme';
import modules from '../modules';
import Layout from './Layout';
import ConsoleContextProvider from './ConsoleContextProvider';
import AppRecords from './panels/apps/Apps';
import Bots from './panels/bots/Bots';
import Config from './panels/Config';
import IPFS from './panels/ipfs/IPFS';
import Metadata from './panels/Metadata';
import Signaling from './panels/Signaling';
import Status from './panels/Status';
import WNS from './panels/wns/WNS';
// Global error handler.
const errorHandler = new ErrorHandler();
/**
* Root application.
*/
const Main = ({ config }) => {
Object.assign(config, { build });
return (
<ApolloProvider client={clientFactory(config)}>
<ConsoleContextProvider config={config} modules={modules} errorHandler={errorHandler}>
<ThemeProvider theme={createTheme(config.app.theme)}>
<CssBaseline />
<HashRouter>
<Switch>
<Route path="/:module">
<Layout>
<Route path="/apps" component={AppRecords} />
<Route path="/bots" component={Bots} />
<Route path="/config" component={Config} />
<Route path="/ipfs" component={IPFS} />
<Route path="/metadata" component={Metadata} />
<Route path="/signaling" component={Signaling} />
<Route path="/status" component={Status} />
<Route path="/wns" component={WNS} />
</Layout>
</Route>
<Redirect to="/status" />
</Switch>
</HashRouter>
</ThemeProvider>
</ConsoleContextProvider>
</ApolloProvider>
);
};
export default Main;
@@ -0,0 +1,128 @@
//
// Copyright 2020 DxOS.org
//
import clsx from 'clsx';
import moment from 'moment';
import React, { useContext, useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core';
import Link from '@material-ui/core/Link';
import Toolbar from '@material-ui/core/Toolbar';
import ErrorIcon from '@material-ui/icons/Error';
import LoadingIcon from '@material-ui/icons/Wifi';
import RunningIcon from '@material-ui/icons/CheckCircle';
import PublicIcon from '@material-ui/icons/Public';
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 { ConsoleContext, useStatusReducer } from '../hooks';
import Error from '../components/Error';
import VersionCheck from './VersionCheck';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'row',
flex: 1,
justifyContent: 'space-between',
backgroundColor: grey[900],
color: grey[400]
},
left: {
display: 'flex',
width: 160
},
right: {
display: 'flex',
width: 160,
justifyContent: 'flex-end'
},
center: {
display: 'flex',
fontFamily: 'monospace',
fontSize: 'large',
'& div': {
margin: 4
}
},
icon: {
margin: '0 2px'
},
link: {
color: grey[400]
},
error: {
color: red[500]
},
running: {
color: green[500]
},
loading: {
color: theme.palette.primary.dark
}
}));
/**
* Displays status indicators at the bottom of the page.
*/
const StatusBar = () => {
const classes = useStyles();
const [{ loading, error }] = useStatusReducer();
const [isLoading, setLoading] = useState(loading);
const { config } = useContext(ConsoleContext);
const { build: { name, buildDate, version } } = config;
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)} />
);
} else {
return (
<RunningIcon className={clsx(classes.icon, classes.running)} />
);
}
};
return (
<>
<Toolbar className={classes.root}>
<div className={classes.left}>
<Link className={classes.link} href={config.app.website} rel="noreferrer" target="_blank">
<PublicIcon />
</Link>
</div>
<div className={classes.center}>
<div>{name} ({version})</div>
<div>{moment(buildDate).format('L')}</div>
<VersionCheck />
</div>
<div className={classes.right}>
<LoadingIcon className={clsx(classes.icon, isLoading && classes.loading)} />
<StatusIcon error={error} />
</div>
</Toolbar>
<Error error={error} />
</>
);
};
export default StatusBar;
@@ -0,0 +1,66 @@
//
// Copyright 2020 DxOS.org
//
import compareVersions from 'compare-versions';
import React, { useEffect, useState } from 'react';
import { useQuery } from '@apollo/react-hooks';
import { makeStyles } from '@material-ui/core';
import SYSTEM_STATUS from '../gql/system_status.graphql';
import WNS_RECORDS from '../gql/wns_records.graphql';
import { useQueryStatusReducer } from '../hooks';
const CHECK_INTERVAL = 5 * 60 * 1000;
const useStyles = makeStyles(theme => ({
update: {
color: theme.palette.error.light
}
}));
/**
* Checks for a system upgrade.
*/
const VersionCheck = () => {
const classes = useStyles();
const [{ current, latest }, setUpgrade] = useState({});
const status = useQueryStatusReducer(useQuery(SYSTEM_STATUS));
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: CHECK_INTERVAL,
variables: { type: 'wrn:resource' }
}));
// Check version.
useEffect(() => {
if (status && data) {
const { dxos: { image: current } } = JSON.parse(status.system_status.json);
let latest = current;
data.wns_records.json.forEach(({ attributes: { name, version } }) => {
// TODO(burdon): Filter by type (WRN?)
if (name.startsWith('dxos/xbox:')) {
if (compareVersions(version, latest) > 0) {
latest = version;
}
}
});
setUpgrade({ current, latest: latest !== current ? latest : undefined });
}
}, [status, data]);
// TODO(burdon): Link to Github page with upgrade info.
return (
<>
{current && (
<div>SYS: {current}</div>
)}
{latest && (
<div className={classes.update}>LATEST: {latest}</div>
)}
</>
);
};
export default VersionCheck;
@@ -0,0 +1,27 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { ConsoleContext } from '../../hooks';
import Panel from '../../components/Panel';
import Toolbar from '../../components/Toolbar';
import Json from '../../components/Json';
const Config = () => {
const { config } = useContext(ConsoleContext);
return (
<Panel
toolbar={
<Toolbar />
}
>
<Json data={config} />
</Panel>
);
};
export default Config;
@@ -0,0 +1,21 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
root: {}
}));
const Signal = () => {
const classes = useStyles();
return (
<div className={classes.root}>
</div>
);
};
export default Signal;
@@ -0,0 +1,21 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { makeStyles } from '@material-ui/core';
const useStyles = makeStyles(theme => ({
root: {}
}));
const Signaling = () => {
const classes = useStyles();
return (
<div className={classes.root}>
</div>
);
};
export default Signaling;
@@ -0,0 +1,35 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import Json from '../../components/Json';
import SYSTEM_STATUS from '../../gql/system_status.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../hooks';
import Panel from '../../components/Panel';
import Toolbar from '../../components/Toolbar';
const Status = () => {
const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(SYSTEM_STATUS, { pollInterval: config.api.intervalQuery }));
if (!data) {
return null;
}
return (
<Panel
toolbar={
<Toolbar />
}
>
<Json data={JSON.parse(data.system_status.json)} />
</Panel>
);
};
export default Status;
@@ -0,0 +1,88 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import WNS_RECORDS from '../../../gql/wns_records.graphql';
import { ConsoleContext, useQueryStatusReducer, useSorter } from '../../../hooks';
import Link from '@material-ui/core/Link';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableBody from '@material-ui/core/TableBody';
import { getServiceUrl } from '../../../util/config';
import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell';
import moment from 'moment';
const AppRecords = () => {
const { config } = useContext(ConsoleContext);
const [sorter, sortBy] = useSorter('createTime', false);
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.intervalQuery,
variables: { type: 'wrn:app' }
}));
if (!data) {
return null;
}
const records = data.wns_records.json;
// TODO(burdon): Test if app is deployed.
const getAppUrl = ({ name, version }) => {
const base = getServiceUrl(config, 'app.server');
const pathComponents = [base];
// TODO(burdon): Fix.
// `wire app serve` expects the /wrn/ prefix.
// That is OK in the production config where we can make it part of the the route,
// but in development it must be prepended since we don't want to make it part of services.app.server.
if (!base.startsWith(`/${config.services.app.prefix}`) && !base.endsWith(`/${config.services.app.prefix}`)) {
pathComponents.push(config.services.app.prefix.substring(1));
}
pathComponents.push(`${name}@${version}`);
return pathComponents.join('/');
};
return (
<Table>
<TableHead>
<TableRow>
<TableCell onClick={sortBy('name')}>Identifier</TableCell>
<TableCell onClick={sortBy('version')} size="small">Version</TableCell>
<TableCell onClick={sortBy('createTime')} size="small">Created</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell>Link</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.sort(sorter).map(({ id, name, version, createTime, attributes: { displayName, publicUrl } }) => {
const link = getAppUrl({ id, name, version, publicUrl });
return (
<TableRow key={id} size="small">
<TableCell monospace>{name}</TableCell>
<TableCell monospace>{version}</TableCell>
<TableCell>{moment.utc(createTime).fromNow()}</TableCell>
<TableCell>{displayName}</TableCell>
<TableCell monospace>
{link && (
<Link href={link} target={name}>{link}</Link>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
};
export default AppRecords;
@@ -0,0 +1,42 @@
//
// Copyright 2020 DxOS.org
//
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Panel from '../../../components/Panel';
import Toolbar from '../../../components/Toolbar';
import AppRecords from './AppRecords';
const TAB_RECORDS = 'records';
const useStyles = makeStyles(theme => ({
root: {}
}));
const Apps = () => {
const classes = useStyles();
const [tab, setTab] = useState(TAB_RECORDS);
return (
<Panel
toolbar={
<Toolbar>
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
<Tab value={TAB_RECORDS} label="Records" />
</Tabs>
</Toolbar>
}
>
{tab === TAB_RECORDS && (
<AppRecords />
)}
</Panel>
);
};
export default Apps;
@@ -0,0 +1,62 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import WNS_RECORDS from '../../../gql/wns_records.graphql';
import { ConsoleContext, useQueryStatusReducer, useSorter } from '../../../hooks';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableBody from '@material-ui/core/TableBody';
import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell';
import moment from 'moment';
const AppRecords = () => {
const { config } = useContext(ConsoleContext);
const [sorter, sortBy] = useSorter('createTime', false);
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.intervalQuery,
variables: { type: 'wrn:bot' }
}));
if (!data) {
return null;
}
const records = data.wns_records.json;
return (
<Table>
<TableHead>
<TableRow>
<TableCell onClick={sortBy('name')}>Identifier</TableCell>
<TableCell onClick={sortBy('version')} size="small">Version</TableCell>
<TableCell onClick={sortBy('createTime')} size="small">Created</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell />
</TableRow>
</TableHead>
<TableBody>
{records.sort(sorter).map(({ id, name, version, createTime, attributes: { displayName } }) => {
return (
<TableRow key={id} size="small">
<TableCell monospace>{name}</TableCell>
<TableCell monospace>{version}</TableCell>
<TableCell>{moment.utc(createTime).fromNow()}</TableCell>
<TableCell>{displayName}</TableCell>
<TableCell />
</TableRow>
);
})}
</TableBody>
</Table>
);
};
export default AppRecords;
@@ -0,0 +1,42 @@
//
// Copyright 2020 DxOS.org
//
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';
import Panel from '../../../components/Panel';
import Toolbar from '../../../components/Toolbar';
import BotRecords from './BotRecords';
const TAB_RECORDS = 'records';
const useStyles = makeStyles(theme => ({
root: {}
}));
const Apps = () => {
const classes = useStyles();
const [tab, setTab] = useState(TAB_RECORDS);
return (
<Panel
toolbar={
<Toolbar>
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
<Tab value={TAB_RECORDS} label="Records" />
</Tabs>
</Toolbar>
}
>
{tab === TAB_RECORDS && (
<BotRecords />
)}
</Panel>
);
};
export default Apps;
@@ -0,0 +1,33 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import IPFS_STATUS from '../../../gql/ipfs_status.graphql';
import { useQueryStatusReducer } from '../../../hooks';
import Json from '../../../components/Json';
import Panel from '../../../components/Panel';
import Toolbar from '../../../components/Toolbar';
const IPFS = () => {
const data = useQueryStatusReducer(useQuery(IPFS_STATUS));
if (!data) {
return null;
}
return (
<Panel
toolbar={
<Toolbar />
}
>
<Json data={JSON.parse(data.ipfs_status.json)} />
</Panel>
);
};
export default IPFS;
@@ -0,0 +1,87 @@
//
// Copyright 2020 DxOS.org
//
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core';
import Paper from '@material-ui/core/Paper';
import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import TabContext from '@material-ui/lab/TabContext';
import Panel from '../../../components/Panel';
import Toolbar from '../../../components/Toolbar';
import WNSLog from './WNSLog';
import WNSRecords, { WNSRecordType } from './WNSRecords';
import WNSStatus from './WNSStatus';
const TAB_RECORDS = 'records';
const TAB_STATUS = 'status';
const TAB_LOG = 'log';
const useStyles = makeStyles(() => ({
expand: {
flex: 1
},
panel: {
display: 'flex',
overflow: 'hidden',
flex: 1
},
paper: {
display: 'flex',
overflow: 'hidden',
flex: 1
}
}));
const WNS = () => {
const classes = useStyles();
const [tab, setTab] = useState(TAB_RECORDS);
const [type, setType] = useState();
return (
<Panel
toolbar={
<Toolbar>
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
<Tab value={TAB_RECORDS} label="Records" />
<Tab value={TAB_STATUS} label="Status" />
<Tab value={TAB_LOG} label="Log" />
</Tabs>
{tab === TAB_RECORDS && (
<WNSRecordType type={type} onChanged={setType} />
)}
</Toolbar>
}
>
<TabContext value={tab}>
{tab === TAB_RECORDS && (
<div className={classes.panel}>
<WNSRecords type={type} />
</div>
)}
{tab === TAB_STATUS && (
<div className={classes.panel}>
<Paper className={classes.paper}>
<WNSStatus />
</Paper>
</div>
)}
{tab === TAB_LOG && (
<div className={classes.panel}>
<WNSLog />
</div>
)}
</TabContext>
</Panel>
);
};
export default WNS;
@@ -0,0 +1,26 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import WNS_LOG from '../../../gql/wns_log.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../../hooks';
import Log from '../../../components/Log';
const WNSLog = () => {
const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(WNS_LOG, { pollInterval: config.api.intervalLog }));
if (!data) {
return null;
}
return (
<Log log={data.wns_log.log} />
);
};
export default WNSLog;
@@ -0,0 +1,113 @@
//
// Copyright 2020 DxOS.org
//
import get from 'lodash.get';
import moment from 'moment';
import React, { useContext, useState } from 'react';
import { useQuery } from '@apollo/react-hooks';
import { makeStyles } from '@material-ui/core';
import ButtonGroup from '@material-ui/core/ButtonGroup';
import Button from '@material-ui/core/Button';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableBody from '@material-ui/core/TableBody';
import WNS_RECORDS from '../../../gql/wns_records.graphql';
import { ConsoleContext, useQueryStatusReducer, useSorter } from '../../../hooks';
import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell';
import PackageLink from '../../../components/PackageLink';
const types = [
{ key: null, label: 'ALL' },
{ key: 'wrn:xbox', label: 'XBox' },
{ key: 'wrn:resource', label: 'Resource' },
{ key: 'wrn:service', label: 'Service' },
{ key: 'wrn:app', label: 'App' },
{ key: 'wrn:bot', label: 'Bot' },
{ key: 'wrn:type', label: 'Type' }
];
const useStyles = makeStyles(theme => ({
selected: {
color: theme.palette.text.primary
}
}));
export const WNSRecordType = ({ type = types[0].key, onChanged }) => {
const classes = useStyles();
return (
<ButtonGroup
disableRipple
disableFocusRipple
variant="outlined"
color="primary"
size="small"
aria-label="text primary button group"
>
{types.map(t => (
<Button
key={t.key}
className={t.key === type && classes.selected}
onClick={() => onChanged(t.key)}
>
{t.label}
</Button>
))}
</ButtonGroup>
);
};
const WNSRecords = ({ type }) => {
const { config } = useContext(ConsoleContext);
const [sorter, sortBy] = useSorter('createTime', false);
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.intervalQuery,
variables: { type }
}));
if (!data) {
return null;
}
const records = data.wns_records.json;
return (
<Table>
<TableHead>
<TableRow>
<TableCell onClick={sortBy('type')} size="small">Type</TableCell>
<TableCell onClick={sortBy('name')}>Identifier</TableCell>
<TableCell onClick={sortBy('version')} size="small">Version</TableCell>
<TableCell onClick={sortBy('createTime')} size="small">Created</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell onClick={sortBy('attributes.package')}>Package Hash</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.sort(sorter)
.map(({ id, type, name, version, createTime, attributes: { displayName, package: pkg } }) => (
<TableRow key={id} size="small">
<TableCell monospace>{type}</TableCell>
<TableCell monospace>{name}</TableCell>
<TableCell monospace>{version}</TableCell>
<TableCell>{moment.utc(createTime).fromNow()}</TableCell>
<TableCell>{displayName}</TableCell>
<TableCell title={JSON.stringify(pkg)} monospace>
{pkg && (
<PackageLink config={config} type={type} pkg={pkg} />
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};
export default WNSRecords;
@@ -0,0 +1,26 @@
//
// Copyright 2020 DxOS.org
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import WNS_STATUS from '../../../gql/wns_status.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../../hooks';
import Json from '../../../components/Json';
const WNSStatus = () => {
const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(WNS_STATUS, { pollInterval: config.api.intervalQuery }));
if (!data) {
return null;
}
return (
<Json data={data.wns_status.json} />
);
};
export default WNSStatus;
@@ -0,0 +1,9 @@
#
# Copyright 2020 DxOS.org
#
query {
ipfs_status {
json
}
}
@@ -0,0 +1,10 @@
#
# Copyright 2020 DxOS.org
#
query {
system_status {
timestamp
json
}
}
@@ -0,0 +1,10 @@
#
# Copyright 2020 DxOS.org
#
mutation ($command: String!) {
wns_action(command: $command) {
timestamp
code
}
}
@@ -0,0 +1,10 @@
#
# Copyright 2020 DxOS.org
#
query {
wns_log @client {
timestamp
log
}
}
@@ -0,0 +1,10 @@
#
# Copyright 2020 DxOS.org
#
query ($type: String) {
wns_records (type: $type) @client {
timestamp
json
}
}
@@ -0,0 +1,10 @@
#
# Copyright 2020 DxOS.org
#
query {
wns_status @client {
timestamp
json
}
}
+11
View File
@@ -0,0 +1,11 @@
//
// Copyright 2020 DxOS.org
//
import { createContext } from 'react';
/**
* https://reactjs.org/docs/context.html#reactcreatecontext
* @type {React.Context}
*/
export const ConsoleContext = createContext({});
+8
View File
@@ -0,0 +1,8 @@
//
// Copyright 2020 DxOS.org
//
export * from './context';
export * from './registry';
export * from './sorter';
export * from './status';
@@ -0,0 +1,19 @@
//
// Copyright 2020 DxOS.org
//
import { Registry } from '@wirelineio/registry-client';
import { getServiceUrl } from '../util/config';
export const useRegistry = (config) => {
const endpoint = getServiceUrl(config, 'wns.server', { absolute: true });
const registry = new Registry(endpoint);
return {
registry,
// TODO(burdon): Separate hook.
webui: getServiceUrl(config, 'wns.webui', { absolute: true })
};
};
+25
View File
@@ -0,0 +1,25 @@
//
// Copyright 2020 DxOS.org
//
import get from 'lodash.get';
import { useState } from 'react';
// TODO(burdon): Enable multiple sort order (e.g., id, version).
export const useSorter = (initSort, initAscend) => {
const [{ sort, ascend }, setSort] = useState({ sort: initSort, ascend: initAscend });
const sorter = (item1, item2) => {
const a = get(item1, sort);
const b = get(item2, sort);
const dir = ascend ? 1 : -1;
return (a < b) ? -1 * dir : (a > b) ? dir : 0;
};
const sortBy = field => () => setSort({ sort: field, ascend: (field === sort ? !ascend : true) });
return [
sorter,
sortBy
];
};
+54
View File
@@ -0,0 +1,54 @@
//
// Copyright 2019 DxOS
//
import { useContext, useEffect } from 'react';
import { ConsoleContext } from './context';
export const STATUS = 'xxx';
export const SET_STATUS = 'set.status';
/**
* Dispatcher for app status.
*/
export const useStatusReducer = () => {
const { state, dispatch } = useContext(ConsoleContext);
return [
state[STATUS] || {},
value => dispatch({ type: SET_STATUS, payload: value || { exceptions: [] } })
];
};
/**
* Handle Apollo queries.
*/
export const useQueryStatusReducer = ({ loading, error, data }) => {
const [, setStatus] = useStatusReducer();
useEffect(() => {
if (loading) {
setStatus({ loading });
}
if (error) {
setStatus({ error });
}
}, [loading, error]);
return data;
};
export const statusReducer = (state, action) => {
switch (action.type) {
case SET_STATUS:
return {
...state,
...action.payload
};
default:
return state;
}
};
+40
View File
@@ -0,0 +1,40 @@
//
// Copyright 2020 DxOS.org
//
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.org
//
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;
+9
View File
@@ -0,0 +1,9 @@
//
// Copyright 2020 DxOS.org
//
import Main from './containers/Main';
export {
Main
};
+15
View File
@@ -0,0 +1,15 @@
//
// Copyright 2020 DxOS.org
//
import debug from 'debug';
import React from 'react';
import { render } from 'react-dom';
import config from '../config.yml';
import Main from './containers/Main';
debug.enable(config.system.debug);
render(<Main config={config} />, document.getElementById('root'));
+60
View File
@@ -0,0 +1,60 @@
//
// Copyright 2020 DxOS.org
//
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: '/status',
title: 'Status',
icon: StatsIcon
},
{
path: '/wns',
title: 'WNS',
icon: RegistryIcon
},
{
path: '/apps',
title: 'Apps',
icon: AppsIcon
},
{
path: '/bots',
title: 'Bots',
icon: BotsIcon
},
{
path: '/signaling',
title: 'Signaling',
icon: SignalIcon
},
{
path: '/ipfs',
title: 'IPFS',
icon: IPFSIcon
}
],
settings: [
{
path: '/metadata',
title: 'Metadata',
icon: ServicesIcon
},
{
path: '/config',
title: 'Config',
icon: ConfigIcon
}
]
};
+89
View File
@@ -0,0 +1,89 @@
//
// Copyright 2020 DxOS.org
//
import debug from 'debug';
import { Registry } from '@wirelineio/registry-client';
import { getServiceUrl } from './util/config';
const log = debug('dxos:console:client:resolvers');
const timestamp = () => new Date().toUTCString();
const MAX_LOG_LENGTH = 200;
/**
* Resolvers
* https://www.apollographql.com/docs/tutorial/local-state/#local-resolvers
* @param config
*/
export const createResolvers = config => {
const endpoint = getServiceUrl(config, 'wns.server', { absolute: true });
const registry = new Registry(endpoint);
// TODO(burdon): Errors swallowed!
// Oldest to latest.
let cachedLog = [];
return {
Query: {
wns_status: async () => {
log('WNS status...');
const data = await registry.getStatus();
return {
__typename: 'JSONResult',
timestamp: timestamp(),
// NOTE: Hack since this should be a string according to the schema.
json: data
};
},
wns_records: async (_, { type }) => {
log('WNS records...');
const data = await registry.queryRecords({ type });
return {
__typename: 'JSONResult',
timestamp: timestamp(),
// NOTE: Hack since this should be a string according to the schema.
json: data
};
},
wns_log: async () => {
log('WNS log...');
const data = await registry.getLogs();
// TODO(burdon): Bug returns blank line at end.
const filtered = data.map(line => line).filter(Boolean);
// Cache and merge previous state.
let i = filtered.findIndex(line => line === cachedLog[cachedLog.length - 1]);
if (i === -1) {
cachedLog = filtered;
} else {
i++;
for (; i < filtered.length - 1; i++) {
cachedLog.push(filtered[i]);
}
// Trim.
cachedLog.splice(0, cachedLog.length - MAX_LOG_LENGTH);
}
return {
__typename: 'JSONLog',
timestamp: timestamp(),
log: [...cachedLog].reverse()
};
}
}
};
};
+76
View File
@@ -0,0 +1,76 @@
//
// 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
},
MuiButton: {
size: 'small'
},
MuiFilledInput: {
margin: 'dense'
},
MuiFormControl: {
margin: 'dense'
},
MuiFormHelperText: {
margin: 'dense'
},
MuiIconButton: {
size: 'small'
},
MuiInputBase: {
margin: 'dense'
},
MuiInputLabel: {
margin: 'dense'
},
MuiTable: {
size: 'small'
},
MuiTextField: {
margin: 'dense'
},
MuiToolbar: {
variant: 'dense'
}
},
// 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'
}
}
}
}
});
+40
View File
@@ -0,0 +1,40 @@
//
// Copyright 2020 DxOS.org
//
import assert from 'assert';
import buildUrl from 'build-url';
import get from 'lodash.get';
/**
* Returns the service URL that can be used by the client.
* @param {Object} config
* @param {string} service
* @param {Object} [options]
* @param {string} [options.path]
* @param {boolean} [options.absolute]
* @returns {string|*}
*/
export const getServiceUrl = (config, service, options = {}) => {
const { path, absolute = false } = options;
const { routes, services } = config;
const appendPath = (url) => buildUrl(url, { path });
// Relative route.
const routePath = get(routes, service);
if (routePath) {
if (absolute) {
assert(typeof window !== 'undefined');
return buildUrl(window.location.origin, { path: appendPath(routePath) });
}
// Relative.
return appendPath(routePath);
}
// Absolute service path.
const serviceUrl = get(services, service);
assert(serviceUrl, `Invalid service definition: ${service}`);
return appendPath(serviceUrl);
};
@@ -0,0 +1,42 @@
//
// Copyright 2020 DxOS.org
//
import { getServiceUrl } from './config';
// noinspection JSConstantReassignment
global.window = {
location: {
origin: 'http://localhost'
}
};
const config = {
services: {
foo: {
server: 'http://localhost:3000/foo'
},
bar: {
server: 'http://localhost:3000/bar'
}
},
routes: {
foo: {
server: '/foo'
}
}
};
test('getServiceUrl', () => {
expect(() => getServiceUrl({}, 'foo.server')).toThrow();
expect(getServiceUrl(config, 'foo.server')).toEqual('/foo');
expect(getServiceUrl(config, 'foo.server', { path: '/123' })).toEqual('/foo/123');
expect(getServiceUrl(config, 'foo.server', { path: '/123', absolute: true })).toEqual('http://localhost/foo/123');
expect(getServiceUrl(config, 'bar.server')).toEqual('http://localhost:3000/bar');
expect(getServiceUrl(config, 'bar.server', { path: '/123' })).toEqual('http://localhost:3000/bar/123');
expect(getServiceUrl(config, 'bar.server', { path: '/123', absolute: true })).toEqual('http://localhost:3000/bar/123');
});
+7
View File
@@ -0,0 +1,7 @@
{
"build": {
"name": "@dxos/console-client",
"buildDate": "2020-05-27T22:04:52.661Z",
"version": "1.0.0-beta.0"
}
}