App/Bots/Records.

This commit is contained in:
richburdon 2020-05-25 18:32:45 -04:00
parent 9fbdd0625a
commit 75459af67b
60 changed files with 961 additions and 221 deletions

View File

@ -6,23 +6,31 @@ Apollo GraphQL client and server using express.
### POC ### POC
- [ ] Trigger server-side wire commands (mutation or separate express path?) - [ ] Complete WNS functionality
- [ ] Logging
- [ ] Webpack and dynamic config
- [ ] Routes for services
- [ ] Trigger server-side wire commands
- [ ] Test on device in production.
- [ ] IPFS
- [ ] Signal
- [ ] Apps
- [ ] Bots
- [ ] Meta
### Next ### Next
- [ ] Config routes for services (test).
- [ ] Webpack config (remove dynamic config?)
- [ ] Client/server API abstraction (error handler, etc.) - [ ] Client/server API abstraction (error handler, etc.)
- [ ] Port dashboard API calls (resolve config first). - [ ] Port dashboard API calls (resolve config first).
- [ ] Port dashboard react modules with dummy resolvers. - [ ] Port dashboard react modules with dummy resolvers.
- [ ] Fix JsonTree (yarn link).
- [ ] https://github.com/standard/standardx (JSX) - [ ] https://github.com/standard/standardx (JSX)
### Done ### Done
- [c] Client resolvers: https://www.apollographql.com/docs/tutorial/local-state/ - [x] Fix JsonTree (yarn link).
- [x] Client resolvers: https://www.apollographql.com/docs/tutorial/local-state/
- [x] Test backend IPFS client request. - [x] Test backend IPFS client request.
- [x] Hash Router. - [x] Hash Router.
- [x] Layout (with Material UI). - [x] Layout (with Material UI).

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
module.exports = { module.exports = {

View File

@ -22,6 +22,7 @@ system:
services: services:
app: app:
prefix: '/app'
server: 'http://127.0.0.1:5999' # TODO(burdon): ??? server: 'http://127.0.0.1:5999' # TODO(burdon): ???
wns: wns:

View File

@ -1,5 +1,5 @@
# #
# Copyright 2020 DxOS # Copyright 2020 DxOS.org
# #
{ {

View File

@ -1,5 +1,5 @@
# #
# Copyright 2020 DxOS # Copyright 2020 DxOS.org
# #
{ {

View File

@ -1,8 +1,8 @@
# #
# Copyright 2020 DxOS # Copyright 2020 DxOS.org
# #
mutation Action($command: String!) { mutation ($command: String!) {
wns_action(command: $command) { wns_action(command: $command) {
timestamp timestamp
code code

View File

@ -0,0 +1,9 @@
#
# Copyright 2020 DxOS.org
#
{
wns_log @client {
log
}
}

View File

@ -0,0 +1,9 @@
#
# Copyright 2020 DxOS.org
#
query ($type: String) {
wns_records (type: $type) @client {
json
}
}

View File

@ -1,5 +1,5 @@
# #
# Copyright 2020 DxOS # Copyright 2020 DxOS.org
# #
{ {

View File

@ -39,10 +39,12 @@
"apollo-cache-inmemory": "^1.6.6", "apollo-cache-inmemory": "^1.6.6",
"apollo-client": "^2.6.10", "apollo-client": "^2.6.10",
"apollo-link-http": "^1.5.17", "apollo-link-http": "^1.5.17",
"build-url": "^2.0.0",
"clsx": "^1.1.0", "clsx": "^1.1.0",
"debug": "^4.1.1", "debug": "^4.1.1",
"graphql-tag": "^2.10.3", "graphql-tag": "^2.10.3",
"lodash.defaultsdeep": "^4.6.1", "lodash.defaultsdeep": "^4.6.1",
"lodash.get": "^4.4.2",
"lodash.isobject": "^3.0.2", "lodash.isobject": "^3.0.2",
"lodash.omit": "^4.5.0", "lodash.omit": "^4.5.0",
"lodash.transform": "^4.6.0", "lodash.transform": "^4.6.0",

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import { ApolloClient } from 'apollo-client'; import { ApolloClient } from 'apollo-client';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React from 'react'; import React from 'react';
@ -45,7 +45,7 @@ const AppBar = ({ config }) => {
return ( return (
<> <>
<MuiAppBar position="fixed"> <MuiAppBar position="fixed">
<Toolbar variant="dense"> <Toolbar>
<Link href="/"> <Link href="/">
<div className={classes.logo}> <div className={classes.logo}>
<DxOSIcon /> <DxOSIcon />

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React from 'react'; import React from 'react';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React from 'react'; import React from 'react';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React, { Component } from 'react'; import React, { Component } from 'react';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import isObject from 'lodash.isobject'; import isObject from 'lodash.isobject';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React, { useContext } from 'react'; import React, { useContext } from 'react';
@ -36,7 +36,7 @@ const useStyles = makeStyles((theme) => ({
flexDirection: 'column', flexDirection: 'column',
flexShrink: 0, flexShrink: 0,
width: 200, width: 200,
borderRight: `1px solid ${theme.palette.primary.dark}` borderRight: `1px solid ${theme.palette.divider}`
}, },
footer: { footer: {
display: 'flex', display: 'flex',

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import clsx from 'clsx'; import clsx from 'clsx';

View File

@ -0,0 +1,57 @@
//
// Copyright 2020 DxOS.org.org
//
import React, { Fragment } 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 => {
Object.keys(pkg[platform]).forEach(arch => {
const cid = pkg[platform][arch];
const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${cid}` });
packageLinks.push(
<Fragment>
<Link
key={cid}
href={ipfsUrl}
title={cid}
target="ipfs"
>
{platform}/{arch}: {cid}
</Link>
</Fragment>
);
});
});
return (
<Fragment>{packageLinks}</Fragment>
);
}
}
return null;
};
export default PackageLink;

View File

@ -4,9 +4,8 @@
import React from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core'; import { makeStyles } from '@material-ui/core';
import Paper from '@material-ui/core/Paper';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(() => ({
root: { root: {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
@ -28,9 +27,9 @@ const Panel = ({ toolbar, children }) => {
return ( return (
<div className={classes.root}> <div className={classes.root}>
{toolbar} {toolbar}
<Paper className={classes.container}> <div className={classes.container}>
{children} {children}
</Paper> </div>
</div> </div>
); );
}; };

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import clsx from 'clsx'; import clsx from 'clsx';
@ -16,7 +16,8 @@ const useStyles = makeStyles(theme => ({
display: 'flex', display: 'flex',
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'column',
justifyContent: 'space-between' justifyContent: 'space-between',
// backgroundColor: theme.palette.grey[100]
}, },
list: { list: {

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import clsx from 'clsx'; import clsx from 'clsx';
@ -86,7 +86,7 @@ const StatusBar = () => {
}; };
return ( return (
<Toolbar variant="dense" className={classes.root}> <Toolbar className={classes.root}>
<div className={classes.left}> <div className={classes.left}>
<Link className={classes.link} href={config.app.website} rel="noreferrer" target="_blank"> <Link className={classes.link} href={config.app.website} rel="noreferrer" target="_blank">
<PublicIcon /> <PublicIcon />

View File

@ -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;

View File

@ -1,28 +1,38 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import clsx from 'clsx';
import React from 'react'; import React from 'react';
import MuiTableCell from '@material-ui/core/TableCell'; import MuiTableCell from '@material-ui/core/TableCell';
import { makeStyles } from '@material-ui/core';
// TODO(burdon): Size for header. const useStyles = makeStyles(() => ({
// TODO(burdon): Standardize table. small: {
width: 160
}
}));
const TableCell = ({ children, monospace = false, title, ...rest }) => ( const TableCell = ({ children, size, monospace = false, title, ...rest }) => {
<MuiTableCell const classes = useStyles();
{...rest}
style={{ return (
overflow: 'hidden', <MuiTableCell
textOverflow: 'ellipsis', {...rest}
whiteSpace: 'nowrap', className={clsx(size && classes[size])}
fontFamily: monospace ? 'monospace' : 'inherit', style={{
fontSize: monospace ? 14 : 'inherit' overflow: 'hidden',
}} textOverflow: 'ellipsis',
title={title} whiteSpace: 'nowrap',
> fontFamily: monospace ? 'monospace' : 'inherit',
{children} fontSize: monospace ? 14 : 'inherit'
</MuiTableCell> }}
); title={title}
>
{children}
</MuiTableCell>
);
};
export default TableCell; export default TableCell;

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React from 'react'; import React from 'react';
@ -10,18 +10,20 @@ const useStyles = makeStyles(theme => ({
toolbar: { toolbar: {
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
whiteSpace: 'nowrap',
'& > button': { '& > button': {
margin: theme.spacing(0.5), margin: theme.spacing(0.5)
} }
} }
})); }));
// TODO(burdon): Tabs.
const Toolbar = ({ children }) => { const Toolbar = ({ children }) => {
const classes = useStyles(); const classes = useStyles();
return ( return (
<MuiToolbar variant="dense" disableGutters className={classes.toolbar}> <MuiToolbar disableGutters className={classes.toolbar}>
{children} {children}
</MuiToolbar> </MuiToolbar>
); );

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React, { useEffect, useReducer } from 'react'; import React, { useEffect, useReducer } from 'react';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import debug from 'debug'; import debug from 'debug';
@ -17,10 +17,14 @@ import modules from '../modules';
import Layout from '../components/Layout'; import Layout from '../components/Layout';
import ConsoleContextProvider from './ConsoleContextProvider'; import ConsoleContextProvider from './ConsoleContextProvider';
import AppRecords from './panels/apps/Apps';
import Bots from './panels/bots/Bots';
import Config from './panels/Config'; import Config from './panels/Config';
import IPFS from './panels/IPFS'; import IPFS from './panels/ipfs/IPFS';
import Metadata from './panels/Metadata';
import Signaling from './panels/Signaling';
import Status from './panels/Status'; import Status from './panels/Status';
import WNS from './panels/WNS'; import WNS from './panels/wns/WNS';
debug.enable(config.system.debug); debug.enable(config.system.debug);
@ -34,8 +38,12 @@ const Main = () => {
<Switch> <Switch>
<Route path="/:module"> <Route path="/:module">
<Layout> <Layout>
<Route path="/apps" component={AppRecords} />
<Route path="/bots" component={Bots} />
<Route path="/config" component={Config} /> <Route path="/config" component={Config} />
<Route path="/ipfs" component={IPFS} /> <Route path="/ipfs" component={IPFS} />
<Route path="/metadata" component={Metadata} />
<Route path="/signaling" component={Signaling} />
<Route path="/status" component={Status} /> <Route path="/status" component={Status} />
<Route path="/wns" component={WNS} /> <Route path="/wns" component={WNS} />
</Layout> </Layout>

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React, { useContext } from 'react'; import React, { useContext } from 'react';

View File

@ -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;

View File

@ -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;

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React, { useContext } from 'react'; import React, { useContext } from 'react';

View File

@ -1,123 +0,0 @@
//
// 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

@ -0,0 +1,87 @@
//
// 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 } 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';
const AppRecords = () => {
const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.pollInterval,
variables: { type: 'wrn:app' }
}));
if (!data) {
return null;
}
const records = data.wns_records.json;
// TODO(burdon): Factor out.
const sorter = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
// 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>Name</TableCell>
<TableCell size="small">Version</TableCell>
<TableCell>Description</TableCell>
<TableCell>Link</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.sort(sorter).map(({ id, name, version, 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>{displayName}</TableCell>
<TableCell monospace>
{link && (
<Link href={link} target={name}>{link}</Link>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
};
export default AppRecords;

View File

@ -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;

View File

@ -0,0 +1,59 @@
//
// 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 } 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';
const AppRecords = () => {
const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.pollInterval,
variables: { type: 'wrn:bot' }
}));
if (!data) {
return null;
}
const records = data.wns_records.json;
// TODO(burdon): Factor out.
const sorter = (a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
return (
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell size="small">Version</TableCell>
<TableCell>Description</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.sort(sorter).map(({ id, name, version, attributes: { displayName } }) => {
return (
<TableRow key={id} size="small">
<TableCell monospace>{name}</TableCell>
<TableCell monospace>{version}</TableCell>
<TableCell>{displayName}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
};
export default AppRecords;

View File

@ -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;

View File

@ -1,17 +1,17 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React from 'react'; import React from 'react';
import { useQuery } from '@apollo/react-hooks'; import { useQuery } from '@apollo/react-hooks';
import IPFS_STATUS from '../../../gql/ipfs_status.graphql'; import IPFS_STATUS from '../../../../gql/ipfs_status.graphql';
import { useQueryStatusReducer } from '../../hooks'; import { useQueryStatusReducer } from '../../../hooks';
import Json from '../../components/Json'; import Json from '../../../components/Json';
import Panel from '../../components/Panel'; import Panel from '../../../components/Panel';
import Toolbar from '../../components/Toolbar'; import Toolbar from '../../../components/Toolbar';
const IPFS = () => { const IPFS = () => {
const data = useQueryStatusReducer(useQuery(IPFS_STATUS)); const data = useQueryStatusReducer(useQuery(IPFS_STATUS));

View File

@ -0,0 +1,108 @@
//
// Copyright 2020 DxOS.org
//
import React, { useState } from 'react';
import { Mutation } from '@apollo/react-components';
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 WNS_ACTION from '../../../../gql/wns_action.graphql';
import ControlButtons from '../../../components/ControlButtons';
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 = 'explorer';
const TAB_STATUS = 'records';
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} />
)}
<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}>
{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;

View File

@ -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.pollInterval }));
if (!data) {
return null;
}
return (
<Log log={data.wns_log.log} />
);
};
export default WNSLog;

View File

@ -0,0 +1,121 @@
//
// 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 } 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: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 [{ sort, ascend }, setSort] = useState({ sort: 'type', ascend: true });
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.pollInterval,
variables: { type }
}));
if (!data) {
return null;
}
const records = data.wns_records.json;
// TODO(burdon): Factor out.
const sortBy = field => () => setSort({ sort: field, ascend: (field === sort ? !ascend : true) });
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;
};
return (
<Table>
<TableHead>
<TableRow>
<TableCell onClick={sortBy('type')} size="small">Type</TableCell>
<TableCell onClick={sortBy('name')}>Name</TableCell>
<TableCell onClick={sortBy('version')} size="small">Version</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Description</TableCell>
<TableCell onClick={sortBy('attributes.package')}>Package Hash</TableCell>
<TableCell onClick={sortBy('createTime')} size="small">Created</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>{displayName}</TableCell>
<TableCell title={JSON.stringify(pkg)} monospace>
{pkg && (
<PackageLink config={config} type={type} pkg={pkg} />
)}
</TableCell>
<TableCell>{moment.utc(createTime).fromNow()}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
};
export default WNSRecords;

View File

@ -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.pollInterval }));
if (!data) {
return null;
}
return (
<Json data={data.wns_status.json} />
);
};
export default WNSStatus;

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import { createContext } from 'react'; import { createContext } from 'react';

View File

@ -1,6 +1,7 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
export * from './context'; export * from './context';
export * from './registry';
export * from './status'; export * from './status';

View File

@ -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 })
};
};

View File

@ -8,8 +8,12 @@ import { ConsoleContext } from './context';
export const SET_STATUS = 'errors'; export const SET_STATUS = 'errors';
/**
*
*/
export const useStatusReducer = () => { export const useStatusReducer = () => {
const { state, dispatch } = useContext(ConsoleContext); const { state, dispatch } = useContext(ConsoleContext);
return [ return [
state[SET_STATUS] || {}, state[SET_STATUS] || {},
value => dispatch({ type: SET_STATUS, payload: value || { exceptions: [] } }) value => dispatch({ type: SET_STATUS, payload: value || { exceptions: [] } })

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React from 'react'; import React from 'react';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React from 'react'; import React from 'react';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
export * from './hooks'; export * from './hooks';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import React from 'react'; import React from 'react';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import AppsIcon from '@material-ui/icons/Apps'; import AppsIcon from '@material-ui/icons/Apps';

View File

@ -1,34 +1,53 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import debug from 'debug'; import debug from 'debug';
import { Registry } from '@wirelineio/registry-client'; import { Registry } from '@wirelineio/registry-client';
import { getServiceUrl } from './util/config';
const log = debug('dxos:console:client:resolvers'); const log = debug('dxos:console:client:resolvers');
// /**
// Resolvers * Resolvers
// https://www.apollographql.com/docs/tutorial/local-state/#local-resolvers * https://www.apollographql.com/docs/tutorial/local-state/#local-resolvers
// * @param config
*/
export const createResolvers = config => { export const createResolvers = config => {
// TODO(burdon): Get route if served from xbox. const endpoint = getServiceUrl(config, 'wns.server', { absolute: true });
const { services: { wns: { server } } } = config; const registry = new Registry(endpoint);
const registry = new Registry(server);
return { return {
Query: { Query: {
wns_status: async () => { wns_status: async () => {
log('Querying WNS...'); log('WNS status...');
const data = await registry.getStatus();
const status = await registry.getStatus();
return { return {
__typename: 'JSONResult', __typename: 'JSONResult',
json: JSON.stringify(status) json: data
};
},
wns_records: async (_, { type }) => {
log('WNS records...');
const data = await registry.queryRecords({ type });
return {
__typename: 'JSONResult',
json: data
};
},
wns_log: async () => {
log('WNS log...');
// TODO(burdon): Use Registry API rather than from CLI?
return {
__typename: 'JSONLog',
log: []
}; };
} }
} }

View File

@ -19,6 +19,36 @@ export const createTheme = (theme) => createMuiTheme({
props: { props: {
MuiButtonBase: { MuiButtonBase: {
disableRipple: true 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'
} }
}, },

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);
};

View File

@ -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');
});

View File

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

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
module.exports = { module.exports = {

View File

@ -1,5 +1,5 @@
# #
# Copyright 2020 DxOS # Copyright 2020 DxOS.org
# #
type JSONResult { type JSONResult {
@ -25,14 +25,16 @@ type Result {
# #
type Query { type Query {
system_status: Status system_status: Status!
ipfs_status: JSONResult ipfs_status: JSONResult!
wns_status: JSONResult wns_status: JSONResult!
wns_log: Log # TODO(burdon): Import WNS schema!
wns_records(type: String): JSONResult!
wns_log: Log!
} }
type Mutation { type Mutation {
wns_action(command: String!): Result wns_action(command: String!): Result!
} }
schema { schema {

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import debug from 'debug'; import debug from 'debug';

View File

@ -1,5 +1,5 @@
// //
// Copyright 2020 DxOS // Copyright 2020 DxOS.org
// //
import debug from 'debug'; import debug from 'debug';
@ -9,12 +9,13 @@ import { version } from '../package.json';
const log = debug('dxos:console:server:resolvers'); const log = debug('dxos:console:server:resolvers');
//
// Resolvers
//
const timestamp = () => new Date().toUTCString(); const timestamp = () => new Date().toUTCString();
/**
* Resolvers
* https://www.apollographql.com/docs/graphql-tools/resolvers
* @param config
*/
export const createResolvers = config => ({ export const createResolvers = config => ({
Mutation: { Mutation: {
// //

View File

@ -4672,6 +4672,11 @@ buffer@^5.2.1, buffer@^5.4.2, buffer@^5.4.3, buffer@^5.5.0, buffer@^5.6.0:
base64-js "^1.0.2" base64-js "^1.0.2"
ieee754 "^1.1.4" ieee754 "^1.1.4"
build-url@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/build-url/-/build-url-2.0.0.tgz#7bdd4045e51caa96c1586990e4ca514937598fc2"
integrity sha512-LYvvOlDc9jT07wFXTQTKoQLYaXIJriVl/DgatTsSzY963+ip1O7M6G/jWBrlKKJ1L7HGD3oK+WykmOvbcSYXlQ==
builtin-status-codes@^3.0.0: builtin-status-codes@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8" resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"