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.
```bash
cd packages/consoe-app
cd packages/console-app
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 classes = useStyles();
const [{ current, latest }, setUpgrade] = useState({});
const status = useQueryStatusReducer(useQuery(SYSTEM_STATUS));
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
const statusRespone = useQueryStatusReducer(useQuery(SYSTEM_STATUS));
const wnsResponse = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: CHECK_INTERVAL,
variables: { type: 'wrn:resource' }
variables: { attributes: { type: 'wrn:resource' } }
}));
// Check version.
useEffect(() => {
if (status && data) {
const { dxos: { image: current } } = status.system_status;
if (statusRespone && wnsResponse) {
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;
data.wns_records.json.forEach(({ attributes: { name, version } }) => {
wnsData.forEach(({ attributes: { name, version } }) => {
// TODO(burdon): Filter by type (WRN?)
if (name.startsWith('dxos/xbox:')) {
if (compareVersions(version, latest) > 0) {
@ -48,7 +52,7 @@ const VersionCheck = () => {
setUpgrade({ current, latest: latest !== current ? latest : undefined });
}
}, [status, data]);
}, [status, wnsResponse]);
// TODO(burdon): Link to Github page with upgrade info.
return (

View File

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

View File

@ -3,36 +3,43 @@
//
import React, { useContext } from 'react';
import moment from 'moment';
import { useQuery } from '@apollo/react-hooks';
import WNS_RECORDS from '../../../gql/wns_records.graphql';
import { ConsoleContext, useQueryStatusReducer, useSorter } from '../../../hooks';
import Link from '@material-ui/core/Link';
import TableHead from '@material-ui/core/TableHead';
import TableRow from '@material-ui/core/TableRow';
import TableBody from '@material-ui/core/TableBody';
import { getServiceUrl } from '../../../util/config';
import 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 TableCell from '../../../components/TableCell';
import moment from 'moment';
import { getServiceUrl } from '../../../util/config';
const AppRecords = () => {
const { config } = useContext(ConsoleContext);
const [sorter, sortBy] = useSorter('createTime', false);
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
const appResponse = useQueryStatusReducer(useQuery(WNS_RECORDS, {
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;
}
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.
const getAppUrl = ({ name, version }) => {
@ -47,8 +54,12 @@ const AppRecords = () => {
pathComponents.push(config.services.app.prefix.substring(1));
}
pathComponents.push(`${name}@${version}`);
return pathComponents.join('/');
if (version) {
pathComponents.push(`${name}@${version}`);
} else {
pathComponents.push(name);
}
return `${pathComponents.join('/')}/`;
};
return (
@ -56,26 +67,29 @@ const AppRecords = () => {
<TableHead>
<TableRow>
<TableCell onClick={sortBy('name')}>Identifier</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell onClick={sortBy('version')} size='small'>Version</TableCell>
<TableCell onClick={sortBy('createTime')} size='small'>Created</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell>Link</TableCell>
<TableCell size='small'>Downloaded</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.sort(sorter).map(({ id, name, version, createTime, attributes: { displayName, publicUrl } }) => {
const link = getAppUrl({ id, name, version, publicUrl });
{appData.sort(sorter).map(({ id, name, version, createTime, attributes: { displayName, publicUrl, package: hash } }) => {
const verLink = getAppUrl({ id, name, version, publicUrl });
const appLink = getAppUrl({ id, name, publicUrl });
return (
<TableRow key={id} size='small'>
<TableCell monospace>{name}</TableCell>
<TableCell monospace>{version}</TableCell>
<TableCell>{moment.utc(createTime).fromNow()}</TableCell>
<TableCell monospace>
<Link href={appLink} target={name}>{name}</Link>
</TableCell>
<TableCell>{displayName}</TableCell>
<TableCell monospace>
{link && (
<Link href={link} target={name}>{link}</Link>
)}
<Link href={verLink} target={version}>{version}</Link>
</TableCell>
<TableCell>{moment.utc(createTime).fromNow()}</TableCell>
<TableCell>
<BooleanIcon yes={localRefs && localRefs.has(hash)} />
</TableCell>
</TableRow>
);

View File

@ -3,29 +3,157 @@
//
import React from 'react';
import get from 'lodash.get';
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 WNS_RECORDS from '../../../gql/wns_records.graphql';
import { useQueryStatusReducer } from '../../../hooks';
import Json from '../../../components/Json';
import Panel from '../../../components/Panel';
import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell';
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 data = useQueryStatusReducer(useQuery(IPFS_STATUS));
if (!data) {
const classes = useStyles();
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;
}
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 (
<Panel
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>
);
};

View File

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

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
#
query ($type: String) {
wns_records (type: $type) @client {
# TODO(telackey): Object would probably not be legal in general, but does work for a '@client' resolved query.
# 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
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...');
const data = await registry.queryRecords({ type });
const data = await registry.queryRecords(attributes);
return {
__typename: 'JSONResult',
timestamp: timestamp(),
// 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
#
type DXOSInfo {
image: String
}
type NetworkInfo {
address: [String]
}
type NodeInfo {
version: String
environment: String
}
type SystemInfo {
network: NetworkInfo
nodejs: NodeInfo
}
type SystemStatus {
timestamp: String!
dxos: DXOSInfo
system: SystemInfo
json: String!
}

View File

@ -21,14 +21,36 @@ export const ipfsResolvers = {
// NOTE: Hangs if server not running.
const ipfs = new IpfsHttpClient(config.services.ipfs.server);
const id = await ipfs.id();
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 {
timestamp: new Date().toUTCString(),
json: JSON.stringify({
id,
version,
status
repo: {
stats
},
refs: {
local: refs
},
swarm: {
peers
}
})
};
}

View File

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