Merge pull request #14 from dxos/tel-m1

IPFS, Status, and App fixes
This commit is contained in:
Thomas E Lackey 2020-06-09 16:25:58 -05:00 committed by GitHub
commit c831181c1d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 263 additions and 107 deletions

View File

@ -14,7 +14,7 @@ First start the server:
Then start the Webpack devserver. Then start the Webpack devserver.
```bash ```bash
cd packages/consoe-app cd packages/console-app
yarn start yarn start
``` ```

View File

@ -0,0 +1,30 @@
//
// Copyright 2020 DxOS
//
import React from 'react';
import YesIcon from '@material-ui/icons/CheckCircleOutline';
import NoIcon from '@material-ui/icons/RadioButtonUnchecked';
import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
error: {
color: theme.palette.error.main
},
on: {
color: theme.palette.primary[500]
},
off: {
color: 'transparent'
}
}));
export const BooleanIcon = ({ yes = false, error = false }) => {
const classes = useStyles();
return (yes
? <YesIcon className={classes.on} />
: <NoIcon className={error ? classes.error : classes.off} />
);
};

View File

@ -26,18 +26,22 @@ const useStyles = makeStyles(theme => ({
const VersionCheck = () => { const VersionCheck = () => {
const classes = useStyles(); const classes = useStyles();
const [{ current, latest }, setUpgrade] = useState({}); const [{ current, latest }, setUpgrade] = useState({});
const status = useQueryStatusReducer(useQuery(SYSTEM_STATUS)); const statusRespone = useQueryStatusReducer(useQuery(SYSTEM_STATUS));
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, { const wnsResponse = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: CHECK_INTERVAL, pollInterval: CHECK_INTERVAL,
variables: { type: 'wrn:resource' } variables: { attributes: { type: 'wrn:resource' } }
})); }));
// Check version. // Check version.
useEffect(() => { useEffect(() => {
if (status && data) { if (statusRespone && wnsResponse) {
const { dxos: { image: current } } = status.system_status; const statusData = JSON.parse(statusRespone.system_status.json);
const wnsData = JSON.parse(wnsResponse.wns_records.json);
const { dxos: { image: current = '0.0.0' } = {} } = statusData;
let latest = current; let latest = current;
data.wns_records.json.forEach(({ attributes: { name, version } }) => { wnsData.forEach(({ attributes: { name, version } }) => {
// TODO(burdon): Filter by type (WRN?) // TODO(burdon): Filter by type (WRN?)
if (name.startsWith('dxos/xbox:')) { if (name.startsWith('dxos/xbox:')) {
if (compareVersions(version, latest) > 0) { if (compareVersions(version, latest) > 0) {
@ -48,7 +52,7 @@ const VersionCheck = () => {
setUpgrade({ current, latest: latest !== current ? latest : undefined }); setUpgrade({ current, latest: latest !== current ? latest : undefined });
} }
}, [status, data]); }, [status, wnsResponse]);
// TODO(burdon): Link to Github page with upgrade info. // TODO(burdon): Link to Github page with upgrade info.
return ( return (

View File

@ -16,18 +16,20 @@ import Toolbar from '../../components/Toolbar';
const Status = () => { const Status = () => {
const { config } = useContext(ConsoleContext); const { config } = useContext(ConsoleContext);
const data = useQueryStatusReducer(useQuery(SYSTEM_STATUS, { pollInterval: config.api.intervalQuery })); const systemResponse = useQueryStatusReducer(useQuery(SYSTEM_STATUS, { pollInterval: config.api.intervalQuery }));
if (!data) { if (!systemResponse) {
return null; return null;
} }
const data = JSON.parse(systemResponse.system_status.json);
return ( return (
<Panel <Panel
toolbar={ toolbar={
<Toolbar /> <Toolbar />
} }
> >
<Json data={data.system_status} /> <Json data={data} />
</Panel> </Panel>
); );
}; };

View File

@ -3,36 +3,43 @@
// //
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import moment from 'moment';
import { useQuery } from '@apollo/react-hooks'; 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 Link from '@material-ui/core/Link';
import TableHead from '@material-ui/core/TableHead'; import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow'; import TableRow from '@material-ui/core/TableRow';
import TableBody from '@material-ui/core/TableBody'; import TableBody from '@material-ui/core/TableBody';
import { getServiceUrl } from '../../../util/config'; import IPFS_STATUS from '../../../gql/ipfs_status.graphql';
import WNS_RECORDS from '../../../gql/wns_records.graphql';
import { ConsoleContext, useQueryStatusReducer, useSorter } from '../../../hooks';
import { BooleanIcon } from '../../../components/BooleanIcon';
import Table from '../../../components/Table'; import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell'; import TableCell from '../../../components/TableCell';
import moment from 'moment'; import { getServiceUrl } from '../../../util/config';
const AppRecords = () => { const AppRecords = () => {
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 appResponse = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.intervalQuery, pollInterval: config.api.intervalQuery,
variables: { type: 'wrn:app' } variables: { attributes: { type: 'wrn:app' } }
})); }));
if (!data) { // TODO(telackey): Does this also need an interval?
const ipfsResponse = useQueryStatusReducer(useQuery(IPFS_STATUS));
if (!appResponse || !ipfsResponse) {
return null; return null;
} }
const records = data.wns_records.json; const appData = JSON.parse(appResponse.wns_records.json);
const ipfsData = JSON.parse(ipfsResponse.ipfs_status.json);
const localRefs = new Set(ipfsData.refs.local);
// TODO(burdon): Test if app is deployed. // TODO(burdon): Test if app is deployed.
const getAppUrl = ({ name, version }) => { const getAppUrl = ({ name, version }) => {
@ -47,8 +54,12 @@ const AppRecords = () => {
pathComponents.push(config.services.app.prefix.substring(1)); pathComponents.push(config.services.app.prefix.substring(1));
} }
pathComponents.push(`${name}@${version}`); if (version) {
return pathComponents.join('/'); pathComponents.push(`${name}@${version}`);
} else {
pathComponents.push(name);
}
return `${pathComponents.join('/')}/`;
}; };
return ( return (
@ -56,26 +67,29 @@ const AppRecords = () => {
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell onClick={sortBy('name')}>Identifier</TableCell> <TableCell onClick={sortBy('name')}>Identifier</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell onClick={sortBy('version')} size='small'>Version</TableCell> <TableCell onClick={sortBy('version')} size='small'>Version</TableCell>
<TableCell onClick={sortBy('createTime')} size='small'>Created</TableCell> <TableCell onClick={sortBy('createTime')} size='small'>Created</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell> <TableCell size='small'>Downloaded</TableCell>
<TableCell>Link</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{records.sort(sorter).map(({ id, name, version, createTime, attributes: { displayName, publicUrl } }) => { {appData.sort(sorter).map(({ id, name, version, createTime, attributes: { displayName, publicUrl, package: hash } }) => {
const link = getAppUrl({ id, name, version, publicUrl }); const verLink = getAppUrl({ id, name, version, publicUrl });
const appLink = getAppUrl({ id, name, publicUrl });
return ( return (
<TableRow key={id} size='small'> <TableRow key={id} size='small'>
<TableCell monospace>{name}</TableCell> <TableCell monospace>
<TableCell monospace>{version}</TableCell> <Link href={appLink} target={name}>{name}</Link>
<TableCell>{moment.utc(createTime).fromNow()}</TableCell> </TableCell>
<TableCell>{displayName}</TableCell> <TableCell>{displayName}</TableCell>
<TableCell monospace> <TableCell monospace>
{link && ( <Link href={verLink} target={version}>{version}</Link>
<Link href={link} target={name}>{link}</Link> </TableCell>
)} <TableCell>{moment.utc(createTime).fromNow()}</TableCell>
<TableCell>
<BooleanIcon yes={localRefs && localRefs.has(hash)} />
</TableCell> </TableCell>
</TableRow> </TableRow>
); );

View File

@ -3,29 +3,157 @@
// //
import React from 'react'; import React from 'react';
import get from 'lodash.get';
import { useQuery } from '@apollo/react-hooks'; import { useQuery } from '@apollo/react-hooks';
import { makeStyles } from '@material-ui/core';
import TableBody from '@material-ui/core/TableBody';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import IPFS_STATUS from '../../../gql/ipfs_status.graphql'; import IPFS_STATUS from '../../../gql/ipfs_status.graphql';
import WNS_RECORDS from '../../../gql/wns_records.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 Table from '../../../components/Table';
import TableCell from '../../../components/TableCell';
import Toolbar from '../../../components/Toolbar'; import Toolbar from '../../../components/Toolbar';
import { BooleanIcon } from '../../../components/BooleanIcon';
const RECORD_TYPE = 'wrn:service';
const SERVICE_TYPE = 'ipfs';
const useStyles = makeStyles((theme) => ({
tableContainer: {
flex: 1,
overflowY: 'scroll'
},
table: {
tableLayout: 'fixed',
'& th': {
fontVariant: 'all-small-caps',
fontSize: 18,
cursor: 'ns-resize'
}
},
connected: {
fontWeight: 'bold'
},
disconnected: {
fontStyle: 'italic'
},
colShort: {
width: '30%'
},
colWide: {},
colBoolean: {
width: '10%'
},
caption: {
backgroundColor: theme.palette.primary[500],
color: theme.palette.primary.contrastText,
paddingLeft: '1em',
margin: 0
}
}));
const IPFS = () => { const IPFS = () => {
const data = useQueryStatusReducer(useQuery(IPFS_STATUS)); const classes = useStyles();
if (!data) { const ipfsResponse = useQueryStatusReducer(useQuery(IPFS_STATUS));
const wnsResponse = useQueryStatusReducer(useQuery(WNS_RECORDS, {
variables: { attributes: { type: RECORD_TYPE, service: SERVICE_TYPE } }
}));
if (!wnsResponse || !ipfsResponse) {
return null; return null;
} }
const ipfsData = JSON.parse(ipfsResponse.ipfs_status.json);
const registeredServers = JSON.parse(wnsResponse.wns_records.json);
const displayServers = registeredServers.map((service) => {
console.error(service);
const addresses = get(service, 'attributes.ipfs.addresses');
let connected = false;
for (const address of addresses) {
const parts = address.split('/');
const nodeId = parts[parts.length - 1];
connected = !!ipfsData.swarm.peers.find(({ peer }) => peer === nodeId);
if (connected) {
break;
}
}
return {
name: get(service, 'name'),
version: get(service, 'version'),
description: get(service, 'attributes.description'),
ipfs: get(service, 'attributes.ipfs'),
connected
};
});
displayServers.sort((a, b) => {
return a.connected && !b.connected ? -1 : b.connected && !a.connected ? 1 : b.name < a.name ? 1 : -1;
});
if (displayServers.length === 0) {
displayServers.push({ name: 'None' });
}
return ( return (
<Panel <Panel
toolbar={ toolbar={
<Toolbar /> <Toolbar />
} }
> >
<Json data={JSON.parse(data.ipfs_status.json)} /> <h4 className={classes.caption}>WNS-registered IPFS Servers</h4>
<Table stickyHeader size='small' className={classes.table}>
<TableHead>
<TableRow>
<TableCell size=''>Identifier</TableCell>
<TableCell size='small'>Description</TableCell>
<TableCell size='small'>Connected</TableCell>
<TableCell>Address</TableCell>
</TableRow>
</TableHead>
<TableBody>
{displayServers.map(({ name, description, ipfs, connected }) => (
<TableRow key={name}>
<TableCell>{name}</TableCell>
<TableCell>{description}</TableCell>
<TableCell>
<BooleanIcon yes={connected} />
</TableCell>
<TableCell>
{ipfs.addresses}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<h4 className={classes.caption}>Local IPFS Server</h4>
<Json data={{
id: ipfsData.id.id,
version: ipfsData.id.agentVersion,
addresses: ipfsData.id.addresses,
peers: ipfsData.swarm.peers.length,
numObjects: ipfsData.repo.stats.numObjects,
repoSize: ipfsData.repo.stats.repoSize
}}
/>
</Panel> </Panel>
); );
}; };

View File

@ -4,18 +4,7 @@
query { query {
system_status { system_status {
timestamp timestamp,
dxos { json
image
}
system {
network {
address
}
nodejs {
version
environment
}
}
} }
} }

View File

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

View File

@ -2,8 +2,10 @@
# Copyright 2020 DxOS.org # Copyright 2020 DxOS.org
# #
query ($type: String) { # TODO(telackey): Object would probably not be legal in general, but does work for a '@client' resolved query.
wns_records (type: $type) @client { # When we do strong typing across the board we should replace it with something like KeyValueInput.
query ($attributes: Object) {
wns_records (attributes: $attributes) @client {
timestamp timestamp
json json
} }

View File

@ -43,16 +43,17 @@ export const createResolvers = config => {
}; };
}, },
wns_records: async (_, { type }) => { wns_records: async (_, { attributes }) => {
console.error(attributes);
log('WNS records...'); log('WNS records...');
const data = await registry.queryRecords({ type }); const data = await registry.queryRecords(attributes);
return { return {
__typename: 'JSONResult', __typename: 'JSONResult',
timestamp: timestamp(), timestamp: timestamp(),
// NOTE: Hack since this should be a string according to the schema. // NOTE: Hack since this should be a string according to the schema.
json: data json: JSON.stringify(data)
}; };
}, },

View File

@ -1,7 +0,0 @@
{
"build": {
"name": "@dxos/console-app",
"buildDate": "2020-06-01T01:13:48.575Z",
"version": "1.0.0-beta.0"
}
}

View File

@ -2,26 +2,7 @@
# Copyright 2020 DxOS.org # Copyright 2020 DxOS.org
# #
type DXOSInfo {
image: String
}
type NetworkInfo {
address: [String]
}
type NodeInfo {
version: String
environment: String
}
type SystemInfo {
network: NetworkInfo
nodejs: NodeInfo
}
type SystemStatus { type SystemStatus {
timestamp: String! timestamp: String!
dxos: DXOSInfo json: String!
system: SystemInfo
} }

View File

@ -21,14 +21,36 @@ export const ipfsResolvers = {
// NOTE: Hangs if server not running. // NOTE: Hangs if server not running.
const ipfs = new IpfsHttpClient(config.services.ipfs.server); const ipfs = new IpfsHttpClient(config.services.ipfs.server);
const id = await ipfs.id();
const version = await ipfs.version(); const version = await ipfs.version();
const status = await ipfs.id(); const peers = await ipfs.swarm.peers();
const stats = await ipfs.stats.repo();
// Do not expose the repo path.
delete stats.repoPath;
const refs = [];
for await (const ref of ipfs.refs.local()) {
if (ref.err) {
log(ref.err);
} else {
refs.push(ref.ref);
}
}
return { return {
timestamp: new Date().toUTCString(), timestamp: new Date().toUTCString(),
json: JSON.stringify({ json: JSON.stringify({
id,
version, version,
status repo: {
stats
},
refs: {
local: refs
},
swarm: {
peers
}
}) })
}; };
} }

View File

@ -30,7 +30,9 @@ const getSystemInfo = async () => {
const ifaces = os.networkInterfaces(); const ifaces = os.networkInterfaces();
const addresses = Object.entries(ifaces).reduce((result, [, values]) => { const addresses = Object.entries(ifaces).reduce((result, [, values]) => {
values.forEach(({ family, address }) => { values.forEach(({ family, address }) => {
if (family === 'IPv4' && address !== '127.0.0.1') { address = address.toLowerCase();
// TODO(telackey): Include link-local IPv6?
if (!address.startsWith('127.') && !address.startsWith('fe80::') && !address.startsWith('::1')) {
result.push(address); result.push(address);
} }
}); });
@ -42,7 +44,7 @@ const getSystemInfo = async () => {
const device = await si.system(); const device = await si.system();
return { return {
cpu: pick(cpu, 'brand', 'cores', 'manufacturer', 'vendor'), cpu: pick(cpu, 'brand', 'cores', 'manufacturer', 'vendor', 'speed'),
memory: { memory: {
total: size(memory.total, 'M'), total: size(memory.total, 'M'),
@ -54,21 +56,23 @@ const getSystemInfo = async () => {
device: pick(device, 'model', 'serial', 'version'), device: pick(device, 'model', 'serial', 'version'),
network: { network: {
address: addresses addresses
}, },
// https://nodejs.org/api/os.html // https://nodejs.org/api/os.html
os: { os: {
arch: os.arch(), arch: os.arch(),
type: os.type(),
platform: os.platform(), platform: os.platform(),
version: os.version ? os.version() : undefined, // Node > 13 version: os.version ? os.version() : undefined // Node > 13
uptime: moment().subtract(os.uptime(), 'seconds').fromNow() },
time: {
now: moment(),
up: moment().subtract(os.uptime(), 'seconds')
}, },
nodejs: { nodejs: {
version: process.version, version: process.version
environment: process.env.NODE_ENV
} }
}; };
}; };
@ -80,11 +84,7 @@ export const systemResolvers = {
return { return {
timestamp: new Date().toUTCString(), timestamp: new Date().toUTCString(),
dxos: { json: JSON.stringify(system)
// TODO(burdon): ???
image: '0.0.1'
},
system
}; };
} }
} }