This commit is contained in:
richburdon 2020-05-24 22:40:15 -04:00
parent 5476d9fce8
commit 9fbdd0625a
20 changed files with 509 additions and 121 deletions

View File

@ -34,6 +34,7 @@
"@dxos/react-ux": "^1.0.0-beta.20",
"@material-ui/core": "^4.10.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.54",
"@wirelineio/registry-client": "^0.4.8",
"apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10",
@ -45,6 +46,7 @@
"lodash.isobject": "^3.0.2",
"lodash.omit": "^4.5.0",
"lodash.transform": "^4.6.0",
"moment": "^2.26.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "^5.2.0",

View File

@ -1,19 +0,0 @@
//
// Copyright 2020 DxOS
//
import React, { useContext } from 'react';
import { JsonTreeView } from '@dxos/react-ux';
import { ConsoleContext } from '../hooks';
const Config = () => {
const { config } = useContext(ConsoleContext);
return (
<JsonTreeView data={config} />
);
};
export default Config;

View File

@ -0,0 +1,35 @@
//
// Copyright 2020 DxOS
//
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;

View File

@ -0,0 +1,42 @@
//
// Copyright 2020 DxOS
//
import React 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: 60
}
}));
const Error = ({ message, ...rest }) => {
const classes = useStyles();
if (!message) {
return null;
}
const messages = Array.isArray(message) ? message : [message];
return (
<Snackbar
className={classes.root}
open={Boolean(message)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
TransitionProps={{ exit: false }}
>
<Alert severity="error" {...rest}>
<AlertTitle>Error</AlertTitle>
{messages.map((message, i) => (
<div key={i}>{message}</div>
))}
</Alert>
</Snackbar>
);
};
export default Error;

View File

@ -4,14 +4,14 @@
import React, { useContext } from 'react';
import { makeStyles } from '@material-ui/core';
import Paper from '@material-ui/core/Paper';
import { FullScreen } from '@dxos/gem-core';
import StatusBar from '../containers/StatusBar';
import { ConsoleContext } from '../hooks';
import AppBar from './AppBar';
import Sidebar from './Sidebar';
import { ConsoleContext } from '../hooks';
import StatusBar from './StatusBar';
const useStyles = makeStyles((theme) => ({
root: {
@ -56,9 +56,9 @@ const Layout = ({ children }) => {
<div className={classes.sidebar}>
<Sidebar modules={modules} />
</div>
<Paper className={classes.main}>
<div className={classes.main}>
{children}
</Paper>
</div>
</div>
<div className={classes.footer}>
<StatusBar />

View File

@ -0,0 +1,120 @@
//
// Copyright 2020 DxOS
//
import clsx from 'clsx';
import moment from 'moment';
import React, { Fragment } from 'react';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flex: 1,
flexDirection: 'column'
},
container: {
display: 'flex',
flex: 1,
overflowX: 'scroll',
overflowY: 'scroll'
},
log: {
padding: theme.spacing(1),
fontSize: 16,
// fontFamily: 'monospace',
whiteSpace: 'nowrap'
},
level: {
display: 'inline-block',
width: 48,
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 = [], onClear }) => {
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 = (
<Fragment>
<span className={classes.ts}>{datetime}</span>
<span className={clsx(classes.level, className)}>{label || level}</span>
<span>{pkg}{text}</span>
</Fragment>
);
return true;
}
return false;
});
return (
<div>{message}</div>
);
};
return (
<div className={classes.root}>
<div className={classes.container}>
<div className={classes.log}>
{log.reverse().map((line, i) => <Line key={i} message={line} />)}
</div>
</div>
</div>
);
};
export default Log;

View File

@ -0,0 +1,38 @@
//
// Copyright 2020 DxOS.org
//
import React from 'react';
import { makeStyles } from '@material-ui/core';
import Paper from '@material-ui/core/Paper';
const useStyles = makeStyles(theme => ({
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}
<Paper className={classes.container}>
{children}
</Paper>
</div>
);
};
export default Panel;

View File

@ -0,0 +1,28 @@
//
// Copyright 2020 DxOS
//
import React from 'react';
import MuiTableCell from '@material-ui/core/TableCell';
// TODO(burdon): Size for header.
// TODO(burdon): Standardize table.
const TableCell = ({ children, monospace = false, title, ...rest }) => (
<MuiTableCell
{...rest}
style={{
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
fontFamily: monospace ? 'monospace' : 'inherit',
fontSize: monospace ? 14 : 'inherit'
}}
title={title}
>
{children}
</MuiTableCell>
);
export default TableCell;

View File

@ -0,0 +1,30 @@
//
// Copyright 2020 DxOS
//
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',
'& > button': {
margin: theme.spacing(0.5),
}
}
}));
const Toolbar = ({ children }) => {
const classes = useStyles();
return (
<MuiToolbar variant="dense" disableGutters className={classes.toolbar}>
{children}
</MuiToolbar>
);
};
export default Toolbar;

View File

@ -1,25 +0,0 @@
//
// Copyright 2020 DxOS
//
import React from 'react';
import { useQuery } from '@apollo/react-hooks';
import Json from '../components/Json';
import { useQueryStatusReducer } from '../hooks';
import IPFS_STATUS from '../../gql/ipfs_status.graphql';
const IPFS = () => {
const data = useQueryStatusReducer(useQuery(IPFS_STATUS));
if (!data) {
return null;
}
return (
<Json data={JSON.parse(data.ipfs_status.json)} />
);
};
export default IPFS;

View File

@ -10,19 +10,17 @@ import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline';
import config from '../../config.yml';
import { createTheme } from '../theme';
import { clientFactory } from '../client';
import modules from '../modules';
import Config from '../components/Config';
import Layout from '../components/Layout';
import ConsoleContextProvider from './ConsoleContextProvider';
import IPFS from './IPFS';
import Status from './Status';
import WNS from './WNS';
import Config from './panels/Config';
import IPFS from './panels/IPFS';
import Status from './panels/Status';
import WNS from './panels/WNS';
debug.enable(config.system.debug);

View File

@ -1,58 +0,0 @@
//
// Copyright 2020 DxOS
//
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import { Mutation } from '@apollo/react-components';
import { makeStyles } from '@material-ui/core';
import Button from '@material-ui/core/Button';
import Json from '../components/Json';
import { ConsoleContext, useQueryStatusReducer } from '../hooks';
import WNS_STATUS from '../../gql/wns_status.graphql';
import WNS_ACTION from '../../gql/wns_action.graphql';
const useStyles = makeStyles((theme) => ({
root: {
display: 'flex',
flexDirection: 'column',
flex: 1,
overflowY: 'scroll'
}
}));
const WNS = () => {
const classes = useStyles();
const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(WNS_STATUS, { pollInterval: config.api.pollInterval }));
if (!data) {
return null;
}
return (
<div className={classes.root}>
<Mutation mutation={WNS_ACTION}>
{(action, { data }) => (
<div>
<Button
onClick={() => {
action({ variables: { command: 'test' } });
}}
>
Test
</Button>
<pre>Result: {JSON.stringify(data)}</pre>
</div>
)}
</Mutation>
<Json data={JSON.parse(data.wns_status.json)} />
</div>
);
};
export default WNS;

View File

@ -0,0 +1,27 @@
//
// Copyright 2020 DxOS
//
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;

View File

@ -0,0 +1,33 @@
//
// Copyright 2020 DxOS
//
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;

View File

@ -5,11 +5,14 @@
import React, { useContext } from 'react';
import { useQuery } from '@apollo/react-hooks';
import Json from '../components/Json';
import Json from '../../components/Json';
import { ConsoleContext, useQueryStatusReducer } from '../hooks';
import SYSTEM_STATUS from '../../../gql/system_status.graphql';
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);
@ -19,7 +22,13 @@ const Status = () => {
}
return (
<Panel
toolbar={
<Toolbar />
}
>
<Json data={data.system_status} />
</Panel>
);
};

View File

@ -0,0 +1,123 @@
//
// Copyright 2020 DxOS
//
import React, { useContext, useState } from 'react';
import { useQuery } from '@apollo/react-hooks';
import { Mutation } from '@apollo/react-components';
import { makeStyles } from '@material-ui/core';
import Button from '@material-ui/core/Button';
import ButtonGroup from '@material-ui/core/ButtonGroup';
import Tab from '@material-ui/core/Tab';
import Tabs from '@material-ui/core/Tabs';
import TabContext from '@material-ui/lab/TabContext';
import TabPanel from '@material-ui/lab/TabPanel';
import ControlButtons from '../../components/ControlButtons';
import Json from '../../components/Json';
import Log from '../../components/Log';
import Panel from '../../components/Panel';
import Toolbar from '../../components/Toolbar';
import { ConsoleContext, useQueryStatusReducer } from '../../hooks';
import WNS_STATUS from '../../../gql/wns_status.graphql';
import WNS_ACTION from '../../../gql/wns_action.graphql';
const types = [
{ key: null, label: 'ALL' },
{ key: 'wrn:xbox', label: 'XBox' },
{ key: 'wrn:resource', label: 'Resource' },
{ key: 'wrn:app', label: 'App' },
{ key: 'wrn:bot', label: 'Bot' },
{ key: 'wrn:type', label: 'Type' },
];
const TAB_RECORDS = 'records';
const TAB_LOG = 'log';
const TAB_EXPLORER = 'explorer';
const useStyles = makeStyles(() => ({
expand: {
flex: 1
}
}));
const WNS = () => {
const classes = useStyles();
const { config } = useContext(ConsoleContext);
const [type, setType] = useState(types[0].key);
const [tab, setTab] = useState(TAB_RECORDS);
const data = useQueryStatusReducer(useQuery(WNS_STATUS, { pollInterval: config.api.pollInterval }));
if (!data) {
return null;
}
return (
<Panel
toolbar={
<Toolbar>
<Tabs value={tab} onChange={(_, value) => setTab(value)}>
<Tab value={TAB_RECORDS} label="Records"/>
<Tab value={TAB_EXPLORER} label="Explorer"/>
<Tab value={TAB_LOG} label="Log"/>
</Tabs>
{tab === TAB_RECORDS && (
<ButtonGroup
className={classes.buttons}
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={() => setType(t.key)}
>
{t.label}
</Button>
))}
</ButtonGroup>
)}
<div className={classes.expand} />
<Mutation mutation={WNS_ACTION}>
{(action, { data }) => (
<div>
<ControlButtons
onStart={() => {
action({ variables: { command: 'start' } });
}}
onStop={() => {
action({ variables: { command: 'stop' } });
}}
/>
</div>
)}
</Mutation>
</Toolbar>
}
>
<TabContext value={tab}>
<TabPanel value={TAB_RECORDS}>
<Json data={JSON.parse(data.wns_status.json)} />
</TabPanel>
<TabPanel value={TAB_EXPLORER}>
</TabPanel>
<TabPanel value={TAB_LOG}>
<Log />
</TabPanel>
</TabContext>
</Panel>
);
};
export default WNS;

View File

@ -1,7 +1,7 @@
{
"build": {
"name": "@dxos/console-client",
"buildDate": "2020-05-24T13:13:49.317Z",
"buildDate": "2020-05-25T02:29:08.942Z",
"version": "1.0.0-beta.0"
}
}

View File

@ -21,8 +21,8 @@ export const createResolvers = config => ({
// WNS
//
wns_action: async (_, __, { action }) => {
log(`WNS action: ${action}`);
wns_action: async (_, { command }) => {
log(`WNS action: ${command}`);
return {
timestamp: timestamp(),

View File

@ -2178,7 +2178,7 @@
dependencies:
"@babel/runtime" "^7.4.4"
"@material-ui/lab@^4.0.0-alpha.42":
"@material-ui/lab@^4.0.0-alpha.42", "@material-ui/lab@^4.0.0-alpha.54":
version "4.0.0-alpha.54"
resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.54.tgz#f359fac05667549353e5e21e631ae22cb2c22996"
integrity sha512-BK/z+8xGPQoMtG6gWKyagCdYO1/2DzkBchvvXs2bbTVh3sbi/QQLIqWV6UA1KtMVydYVt22NwV3xltgPkaPKLg==
@ -11379,6 +11379,11 @@ modify-values@^1.0.0:
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==
moment@^2.26.0:
version "2.26.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a"
integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw==
move-concurrently@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"