Updated tabs (#52)

* Link update instructions to statusbar.

* Added KUBE Tab.
Updated formatting.
This commit is contained in:
Rich Burdon 2020-10-30 15:15:01 -04:00 committed by GitHub
parent 71e8b1198a
commit daf7179e4f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 249 additions and 95 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "@dxos/console", "name": "@dxos/console",
"version": "1.0.0-beta.0", "version": "1.0.0-beta.0",
"description": "Kubenet Console", "description": "Console",
"main": "index.js", "main": "index.js",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@ -4,7 +4,7 @@
# #
app: app:
title: 'Kubenet Console' title: 'Console'
org': 'DXOS.org' org': 'DXOS.org'
theme: 'dark' theme: 'dark'
website: 'https://dxos.org' website: 'https://dxos.org'

View File

@ -4,7 +4,7 @@
# #
app: app:
title: 'Kubenet Console' title: 'Console'
org': 'DXOS.org' org': 'DXOS.org'
theme: 'dark' theme: 'dark'
website: 'https://dxos.org' website: 'https://dxos.org'

View File

@ -4,7 +4,7 @@
# #
app: app:
title: 'Kubenet Console' title: 'Console'
org': 'DXOS.org' org': 'DXOS.org'
theme: 'dark' theme: 'dark'
website: 'https://dxos.org' website: 'https://dxos.org'

View File

@ -4,7 +4,7 @@
# #
app: app:
title: 'Kubenet Console' title: 'Console'
org': 'DXOS.org' org': 'DXOS.org'
theme: 'dark' theme: 'dark'
website: 'https://dxos.org' website: 'https://dxos.org'

View File

@ -48,7 +48,7 @@ const AppBar = ({ config }) => {
return ( return (
<> <>
<MuiAppBar position='fixed'> <MuiAppBar position='fixed' elevation={0}>
<Toolbar> <Toolbar>
<Link classes={{ root: classes.logoLink }} href='/'> <Link classes={{ root: classes.logoLink }} href='/'>
<div className={classes.logo}> <div className={classes.logo}>

View File

@ -17,28 +17,49 @@ import { getServiceUrl } from '../util/config';
const PackageLink = ({ config, type, pkg, text }) => { const PackageLink = ({ config, type, pkg, text }) => {
// eslint-disable-next-line default-case // eslint-disable-next-line default-case
switch (type) { switch (type) {
// Apps
case 'wrn:app': { case 'wrn:app': {
const cid = pkg['/']; const cid = pkg['/'];
const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${cid}` }); const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${cid}` });
return <Link href={ipfsUrl} key={cid} target={cid}>{text || cid}</Link>; if (!cid) {
console.warn('Invalid CID', type, pkg);
return;
}
return (
<Link
key={cid}
href={ipfsUrl}
title={cid}
target={cid}
>
{text || cid}
</Link>
);
} }
// Bots
case 'wrn:bot': { case 'wrn:bot': {
const packageLinks = []; const packageLinks = [];
Object.keys(pkg).forEach((platform, i) => { Object.keys(pkg).forEach((platform) => {
Object.keys(pkg[platform]).forEach(arch => { Object.keys(pkg[platform]).forEach(arch => {
const cid = pkg[platform][arch]['/']; const cid = pkg[platform][arch]['/'];
const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${cid}` }); const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${cid}` });
if (!cid) {
console.warn('Invalid CID', type, pkg);
return;
}
const label = `${platform}/${arch}: ${cid}`
packageLinks.push( packageLinks.push(
<div> <Link
<Link key={cid}
key={cid} href={ipfsUrl}
href={ipfsUrl} title={label}
title={cid} target={pkg}
target={pkg} >
> {cid}
{platform}/{arch}: {cid} </Link>
</Link>
</div>
); );
}); });
}); });

View File

@ -16,7 +16,7 @@ const useStyles = makeStyles(theme => ({
}, },
table: { table: {
tableLayout: 'fixed', // tableLayout: 'fixed',
'& th': { '& th': {
fontVariant: 'all-small-caps', fontVariant: 'all-small-caps',

View File

@ -23,7 +23,7 @@ const useStyles = makeStyles(() => ({
} }
})); }));
const TableCell = ({ children, size, monospace = false, style, title, ...rest }) => { const TableCell = ({ children, size, monospace = false, ellipsis = false, style, title, ...rest }) => {
const classes = useStyles(); const classes = useStyles();
return ( return (
@ -32,7 +32,7 @@ const TableCell = ({ children, size, monospace = false, style, title, ...rest })
className={clsx(size && classes[size])} className={clsx(size && classes[size])}
style={{ style={{
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: ellipsis ? 'ellipsis' : '',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
verticalAlign: 'top', verticalAlign: 'top',
fontFamily: monospace ? 'monospace' : 'inherit', fontFamily: monospace ? 'monospace' : 'inherit',

View File

@ -19,14 +19,15 @@ import modules from '../modules';
import Layout from './Layout'; import Layout from './Layout';
import ConsoleContextProvider from './ConsoleContextProvider'; import ConsoleContextProvider from './ConsoleContextProvider';
import AppRecords from './panels/apps/Apps'; import Apps from './panels/apps/Apps';
import Bots from './panels/bots/Bots'; import Bots from './panels/bots/Bots';
import Kubes from './panels/kubes/Kubes';
import Config from './panels/Config'; import Config from './panels/Config';
import IPFS from './panels/ipfs/IPFS'; import IPFS from './panels/ipfs/IPFS';
import Metadata from './panels/Metadata'; import Metadata from './panels/Metadata';
import Signaling from './panels/signal/Signaling'; import Signaling from './panels/signal/Signaling';
import System from './panels/system/System'; import System from './panels/system/System';
import WNS from './panels/wns/WNS'; import Registry from './panels/registry/Registry';
// Global error handler. // Global error handler.
const errorHandler = new ErrorHandler(); const errorHandler = new ErrorHandler();
@ -46,14 +47,15 @@ const Main = ({ config }) => {
<Switch> <Switch>
<Route path='/:module'> <Route path='/:module'>
<Layout> <Layout>
<Route path='/apps' component={AppRecords} /> <Route path='/kubes' component={Kubes} />
<Route path='/apps' component={Apps} />
<Route path='/bots' component={Bots} /> <Route path='/bots' component={Bots} />
<Route path='/config' component={Config} /> <Route path='/config' component={Config} />
<Route path='/registry' component={Registry} />
<Route path='/ipfs' component={IPFS} /> <Route path='/ipfs' component={IPFS} />
<Route path='/metadata' component={Metadata} /> <Route path='/metadata' component={Metadata} />
<Route path='/signaling' component={Signaling} /> <Route path='/signaling' component={Signaling} />
<Route path='/system' component={System} /> <Route path='/system' component={System} />
<Route path='/wns' component={WNS} />
</Layout> </Layout>
</Route> </Route>
<Redirect to='/system' /> <Redirect to='/system' />

View File

@ -0,0 +1,67 @@
//
// Copyright 2020 DXOS.org
//
import React, { useContext } from 'react';
import moment from 'moment';
import { useQuery } from '@apollo/react-hooks';
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 AppLink from '../../../components/AppLink';
const KubeRecords = () => {
const { config } = useContext(ConsoleContext);
const [sorter, sortBy] = useSorter('names[0]');
const appResponse = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.intervalQuery,
variables: { attributes: { type: 'wrn:kube' } }
}));
if (!appResponse) {
return null;
}
const appData = JSON.parse(appResponse.wns_records.json);
return (
<Table>
<TableHead>
<TableRow>
<TableCell onClick={sortBy('names[0]')}>Registered Names</TableCell>
<TableCell onClick={sortBy('attributes.version')} size='small'>Version</TableCell>
<TableCell onClick={sortBy('attributes.name')}>Name</TableCell>
<TableCell onClick={sortBy('createTime')} size='small'>Created</TableCell>
</TableRow>
</TableHead>
<TableBody>
{appData.sort(sorter).map(({ id, names, createTime, attributes: { name: displayName, version, package: packageLink } }) => {
return (
<TableRow key={id} size='small'>
<TableCell monospace>
{names.map(wrn => <div key={wrn}> <AppLink config={config} wrn={wrn} /> </div>)}
</TableCell>
<TableCell monospace>
{version}
</TableCell>
<TableCell>
{displayName}
</TableCell>
<TableCell>{moment.utc(createTime).fromNow()}</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
);
};
export default KubeRecords;

View File

@ -0,0 +1,43 @@
//
// 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 KubeRecords from './KubeRecords';
const TAB_RECORDS = 'records';
const useStyles = makeStyles(theme => ({
root: {}
}));
const Kubes = () => {
// eslint-disable-next-line
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 && (
<KubeRecords />
)}
</Panel>
);
};
export default Kubes;

View File

@ -13,9 +13,9 @@ import LogPoller from '../../../components/LogPoller';
import Panel from '../../../components/Panel'; import Panel from '../../../components/Panel';
import Toolbar from '../../../components/Toolbar'; import Toolbar from '../../../components/Toolbar';
import WNSRecords, { WNSRecordType } from './WNSRecords'; import RegistryRecords, { RecordType } from './RegistryRecords';
import WNSStatus from './WNSStatus'; import RegistryStatus from './RegistryStatus';
import WNSLookup from './WNSLookup'; import RegistryLookup from './RegistryLookup';
const TAB_RECORDS = 'records'; const TAB_RECORDS = 'records';
const TAB_STATUS = 'status'; const TAB_STATUS = 'status';
@ -40,7 +40,7 @@ const useStyles = makeStyles(() => ({
} }
})); }));
const WNS = () => { const Registry = () => {
const classes = useStyles(); const classes = useStyles();
const [tab, setTab] = useState(TAB_RECORDS); const [tab, setTab] = useState(TAB_RECORDS);
const [type, setType] = useState(); const [type, setType] = useState();
@ -57,7 +57,7 @@ const WNS = () => {
</Tabs> </Tabs>
{tab === TAB_RECORDS && ( {tab === TAB_RECORDS && (
<WNSRecordType type={type} onChanged={setType} /> <RecordType type={type} onChanged={setType} />
)} )}
</Toolbar> </Toolbar>
} }
@ -65,20 +65,20 @@ const WNS = () => {
<TabContext value={tab}> <TabContext value={tab}>
{tab === TAB_RECORDS && ( {tab === TAB_RECORDS && (
<div className={classes.panel}> <div className={classes.panel}>
<WNSRecords type={type} /> <RegistryRecords type={type} />
</div> </div>
)} )}
{tab === TAB_LOOKUP && ( {tab === TAB_LOOKUP && (
<div className={classes.panel}> <div className={classes.panel}>
<WNSLookup /> <RegistryLookup />
</div> </div>
)} )}
{tab === TAB_STATUS && ( {tab === TAB_STATUS && (
<div className={classes.panel}> <div className={classes.panel}>
<Paper className={classes.paper}> <Paper className={classes.paper}>
<WNSStatus /> <RegistryStatus />
</Paper> </Paper>
</div> </div>
)} )}
@ -93,4 +93,4 @@ const WNS = () => {
); );
}; };
export default WNS; export default Registry;

View File

@ -5,22 +5,37 @@
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import Autocomplete from '@material-ui/lab/Autocomplete'; import Autocomplete from '@material-ui/lab/Autocomplete';
import Button from '@material-ui/core/Button'; import Button from '@material-ui/core/Button';
import NativeSelect from '@material-ui/core/NativeSelect'; import MenuItem from '@material-ui/core/MenuItem';
import TableBody from '@material-ui/core/TableBody'; import Select from '@material-ui/core/Select';
import TableContainer from '@material-ui/core/TableContainer';
import TableRow from '@material-ui/core/TableRow';
import TextField from '@material-ui/core/TextField'; import TextField from '@material-ui/core/TextField';
import Toolbar from '@material-ui/core/Toolbar';
import { makeStyles } from '@material-ui/core';
import { useQuery } from '@apollo/react-hooks'; import { useQuery } from '@apollo/react-hooks';
import Json from '../../../components/Json'; import Json from '../../../components/Json';
import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell';
import { ConsoleContext, useQueryStatusReducer, useRegistry } from '../../../hooks'; import { ConsoleContext, useQueryStatusReducer, useRegistry } from '../../../hooks';
import WNS_RECORDS from '../../../gql/wns_records.graphql'; import WNS_RECORDS from '../../../gql/wns_records.graphql';
const WNSLookup = () => { const useStyles = makeStyles(theme => ({
root: {
display: 'flex',
flexDirection: 'column',
flex: 1
},
select: {
width: 160,
marginRight: theme.spacing(2)
},
button: {
marginLeft: theme.spacing(2)
}
}));
const RegistryLookup = () => {
const classes = useStyles();
const { config } = useContext(ConsoleContext); const { config } = useContext(ConsoleContext);
const { registry } = useRegistry(config); const { registry } = useRegistry(config);
const [result, setResult] = useState({}); const [result, setResult] = useState({});
@ -34,6 +49,7 @@ const WNSLookup = () => {
if (!data) { if (!data) {
return null; return null;
} }
const records = JSON.parse(data.wns_records.json); const records = JSON.parse(data.wns_records.json);
const getNames = () => { const getNames = () => {
@ -44,6 +60,7 @@ const WNSLookup = () => {
records.forEach(item => ret.push(...item.names)); records.forEach(item => ret.push(...item.names));
break; break;
} }
case 'authority': { case 'authority': {
// Use the known names to come up with a default list of authorities. // Use the known names to come up with a default list of authorities.
// TODO(telackey): Should we be able to query WNS for a list of authorities? // TODO(telackey): Should we be able to query WNS for a list of authorities?
@ -57,9 +74,11 @@ const WNSLookup = () => {
ret = Array.from(names.values()); ret = Array.from(names.values());
break; break;
} }
default: default:
throw new Error(`Unrecognized lookup type: ${lookupType}`); throw new Error(`Unrecognized lookup type: ${lookupType}`);
} }
ret.sort(); ret.sort();
return ret; return ret;
}; };
@ -67,6 +86,7 @@ const WNSLookup = () => {
const handleSelect = (evt) => { const handleSelect = (evt) => {
evt.preventDefault(); evt.preventDefault();
// TODO(burdon): Change to controlled component.
setLookupType(evt.target.value); setLookupType(evt.target.value);
}; };
@ -83,54 +103,46 @@ const WNSLookup = () => {
case 'wrn': case 'wrn':
result = await registry.lookupNames([inputValue], true); result = await registry.lookupNames([inputValue], true);
break; break;
case 'authority': case 'authority':
result = await registry.lookupAuthorities([inputValue]); result = await registry.lookupAuthorities([inputValue]);
break; break;
default: default:
throw new Error(`Unrecognized lookup type: ${lookupType}`); throw new Error(`Unrecognized lookup type: ${lookupType}`);
} }
setResult(result); setResult(result);
}; };
return ( return (
<div> <div className={classes.root}>
<form onSubmit={handleSubmit}> <Toolbar>
<TableContainer> <Select id='lookupType' className={classes.select} name='lookupType' defaultValue='wrn' onChange={handleSelect}>
<Table> <MenuItem value='authority'>Authority</MenuItem>
<TableBody> <MenuItem value='wrn'>WRN</MenuItem>
<TableRow> </Select>
<TableCell align='left' width='150px' style={{ verticalAlign: 'middle' }}>
<NativeSelect id='lookupType' name='lookupType' defaultValue='wrn' onChange={handleSelect}> <Autocomplete
<option value='authority'>Authority</option> options={getNames()}
<option value='wrn'>WRN</option> autoFocus
</NativeSelect> name='value'
</TableCell> id='value'
<TableCell align='left'> fullWidth
<Autocomplete freeSolo
options={getNames()} inputValue={inputValue}
autoFocus onInputChange={(event, newInputValue) => {
name='value' setInputValue(newInputValue);
id='value' }}
fullWidth renderInput={(params) => <TextField {...params} variant='outlined' />}
freeSolo />
inputValue={inputValue}
onInputChange={(event, newInputValue) => { <Button className={classes.button} variant='contained' color='primary' onClick={handleSubmit}>Search</Button>
setInputValue(newInputValue); </Toolbar>
}}
renderInput={(params) => <TextField {...params} variant='outlined' />}
/>
</TableCell>
<TableCell align='left' width='150px' style={{ verticalAlign: 'middle' }}>
<Button variant='contained' color='primary' type='submit'>Search</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</form>
<Json data={result} /> <Json data={result} />
</div> </div>
); );
}; };
export default WNSLookup; export default RegistryLookup;

View File

@ -39,7 +39,7 @@ const useStyles = makeStyles(theme => ({
} }
})); }));
export const WNSRecordType = ({ type = types[0].key, onChanged }) => { export const RecordType = ({ type = types[0].key, onChanged }) => {
const classes = useStyles(); const classes = useStyles();
return ( return (
@ -64,7 +64,7 @@ export const WNSRecordType = ({ type = types[0].key, onChanged }) => {
); );
}; };
const WNSRecords = ({ type }) => { const RegistryRecords = ({ type }) => {
const { config } = useContext(ConsoleContext); const { config } = useContext(ConsoleContext);
const [sorter, sortBy] = useSorter('createTime', false); const [sorter, sortBy] = useSorter('createTime', false);
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, { const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
@ -85,7 +85,7 @@ const WNSRecords = ({ type }) => {
<TableCell onClick={sortBy('attributes.type')} size='medium'>Type</TableCell> <TableCell onClick={sortBy('attributes.type')} size='medium'>Type</TableCell>
<TableCell onClick={sortBy('names[0]')}>Registered Names</TableCell> <TableCell onClick={sortBy('names[0]')}>Registered Names</TableCell>
<TableCell onClick={sortBy('attributes.version')} size='small'>Version</TableCell> <TableCell onClick={sortBy('attributes.version')} size='small'>Version</TableCell>
<TableCell onClick={sortBy('attributes.name')}>Name</TableCell> <TableCell onClick={sortBy('attributes.name')}>Display Name</TableCell>
<TableCell onClick={sortBy('createTime')} size='small'>Created</TableCell> <TableCell onClick={sortBy('createTime')} size='small'>Created</TableCell>
<TableCell onClick={sortBy('attributes.package')}>Package</TableCell> <TableCell onClick={sortBy('attributes.package')}>Package</TableCell>
<TableCell size='icon' /> <TableCell size='icon' />
@ -145,4 +145,4 @@ const WNSRecords = ({ type }) => {
); );
}; };
export default WNSRecords; export default RegistryRecords;

View File

@ -11,7 +11,7 @@ import { ConsoleContext, useQueryStatusReducer } from '../../../hooks';
import Json from '../../../components/Json'; import Json from '../../../components/Json';
const WNSStatus = () => { const RegistryStatus = () => {
const { config } = useContext(ConsoleContext); const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(WNS_STATUS, { pollInterval: config.api.intervalQuery })); const data = useQueryStatusReducer(useQuery(WNS_STATUS, { pollInterval: config.api.intervalQuery }));
if (!data) { if (!data) {
@ -23,4 +23,4 @@ const WNSStatus = () => {
); );
}; };
export default WNSStatus; export default RegistryStatus;

View File

@ -9,7 +9,11 @@ import RegistryIcon from '@material-ui/icons/Language';
import IPFSIcon from '@material-ui/icons/GraphicEq'; import IPFSIcon from '@material-ui/icons/GraphicEq';
import ConfigIcon from '@material-ui/icons/Settings'; import ConfigIcon from '@material-ui/icons/Settings';
import SignalIcon from '@material-ui/icons/Traffic'; import SignalIcon from '@material-ui/icons/Traffic';
import KubeIcon from '@material-ui/icons/Dns';
/**
* Paths should match Main routes.
*/
export default { export default {
services: [ services: [
{ {
@ -18,10 +22,15 @@ export default {
icon: StatsIcon icon: StatsIcon
}, },
{ {
path: '/wns', path: '/registry',
title: 'WNS', title: 'Registry',
icon: RegistryIcon icon: RegistryIcon
}, },
{
path: '/kubes',
title: 'KUBE Nodes',
icon: KubeIcon
},
{ {
path: '/apps', path: '/apps',
title: 'Apps', title: 'Apps',

View File

@ -1,7 +1,7 @@
{ {
"build": { "build": {
"name": "@dxos/console-app", "name": "@dxos/console-app",
"buildDate": "2020-10-19T16:21:30.158Z", "buildDate": "2020-10-30T16:31:29.034Z",
"version": "1.1.0-beta.9" "version": "1.1.0-beta.10"
} }
} }

View File

@ -15,7 +15,7 @@ module.exports = merge(commonConfig, {
new HtmlWebPackPlugin({ new HtmlWebPackPlugin({
template: './public/index.html', template: './public/index.html',
templateParameters: { templateParameters: {
title: 'Kubenet Console' title: 'Console'
} }
}) })
] ]

View File

@ -4,7 +4,7 @@
# #
app: app:
title: 'Kubenet Console' title: 'Console'
org': 'DXOS.org' org': 'DXOS.org'
theme: 'dark' theme: 'dark'
website: 'https://dxos.org' website: 'https://dxos.org'

View File

@ -4,7 +4,7 @@
# #
app: app:
title: 'Kubenet Console' title: 'Console'
org': 'DXOS.org' org': 'DXOS.org'
theme: 'dark' theme: 'dark'
website: 'https://dxos.org' website: 'https://dxos.org'

View File

@ -4,7 +4,7 @@
# #
app: app:
title: 'Kubenet Console' title: 'Console'
org': 'DXOS.org' org': 'DXOS.org'
theme: 'dark' theme: 'dark'
website: 'https://dxos.org' website: 'https://dxos.org'

View File

@ -67,7 +67,7 @@ module.exports = {
new HtmlWebPackPlugin({ new HtmlWebPackPlugin({
template: './public/index.html', template: './public/index.html',
templateParameters: { templateParameters: {
title: 'Kubenet Console' title: 'Console'
} }
}), }),