Version check and sorting.

This commit is contained in:
richburdon 2020-05-25 22:16:25 -04:00
parent 75459af67b
commit c9b139bb9b
21 changed files with 192 additions and 56 deletions

View File

@ -4,13 +4,21 @@ Apollo GraphQL client.
## Usage ## Usage
First start the server:
```bash ```bash
yarn cd packages/consoe-server
yarn start yarn start
``` ```
http://localhost:8080 Then start the Webpack devserver.
```bash
cd packages/consoe-client
yarn start
```
Then load the app: http://localhost:8080.
## Deploy ## Deploy

View File

@ -5,6 +5,6 @@
{ {
system_status { system_status {
timestamp timestamp
version json
} }
} }

View File

@ -4,6 +4,7 @@
{ {
wns_log @client { wns_log @client {
timestamp
log log
} }
} }

View File

@ -4,6 +4,7 @@
query ($type: String) { query ($type: String) {
wns_records (type: $type) @client { wns_records (type: $type) @client {
timestamp
json json
} }
} }

View File

@ -4,6 +4,7 @@
{ {
wns_status @client { wns_status @client {
timestamp
json json
} }
} }

View File

@ -41,6 +41,7 @@
"apollo-link-http": "^1.5.17", "apollo-link-http": "^1.5.17",
"build-url": "^2.0.0", "build-url": "^2.0.0",
"clsx": "^1.1.0", "clsx": "^1.1.0",
"compare-versions": "^3.6.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",

View File

@ -9,8 +9,8 @@ import { FullScreen } from '@dxos/gem-core';
import { ConsoleContext } from '../hooks'; import { ConsoleContext } from '../hooks';
import AppBar from './AppBar'; import AppBar from '../components/AppBar';
import Sidebar from './Sidebar'; import Sidebar from '../components/Sidebar';
import StatusBar from './StatusBar'; import StatusBar from './StatusBar';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -44,6 +44,9 @@ const useStyles = makeStyles((theme) => ({
} }
})); }));
/**
* Main layout for app.
*/
const Layout = ({ children }) => { const Layout = ({ children }) => {
const classes = useStyles(); const classes = useStyles();
const { config, modules } = useContext(ConsoleContext); const { config, modules } = useContext(ConsoleContext);

View File

@ -10,11 +10,13 @@ import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import config from '../../config.yml'; import config from '../../config.yml';
import { build } from '../../version.json';
import { createTheme } from '../theme'; import { createTheme } from '../theme';
import { clientFactory } from '../client'; import { clientFactory } from '../client';
import modules from '../modules'; import modules from '../modules';
import Layout from '../components/Layout'; import Layout from './Layout';
import ConsoleContextProvider from './ConsoleContextProvider'; import ConsoleContextProvider from './ConsoleContextProvider';
import AppRecords from './panels/apps/Apps'; import AppRecords from './panels/apps/Apps';
@ -26,8 +28,14 @@ import Signaling from './panels/Signaling';
import Status from './panels/Status'; import Status from './panels/Status';
import WNS from './panels/wns/WNS'; import WNS from './panels/wns/WNS';
// TODO(burdon): Config object.
Object.assign(config, { build });
debug.enable(config.system.debug); debug.enable(config.system.debug);
/**
* Root application.
*/
const Main = () => { const Main = () => {
return ( return (
<ApolloProvider client={clientFactory(config)}> <ApolloProvider client={clientFactory(config)}>

View File

@ -3,6 +3,7 @@
// //
import clsx from 'clsx'; import clsx from 'clsx';
import moment from 'moment';
import React, { useContext, useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core'; import { makeStyles } from '@material-ui/core';
import Link from '@material-ui/core/Link'; import Link from '@material-ui/core/Link';
@ -15,15 +16,16 @@ import grey from '@material-ui/core/colors/grey';
import green from '@material-ui/core/colors/green'; import green from '@material-ui/core/colors/green';
import red from '@material-ui/core/colors/red'; import red from '@material-ui/core/colors/red';
import { version } from '../../package.json';
import { ConsoleContext, useStatusReducer } from '../hooks'; import { ConsoleContext, useStatusReducer } from '../hooks';
import VersionCheck from './VersionCheck';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
root: { root: {
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'row',
flex: 1, flex: 1,
justifyContent: 'space-around', justifyContent: 'space-between',
backgroundColor: grey[900], backgroundColor: grey[900],
color: grey[400] color: grey[400]
}, },
@ -50,16 +52,26 @@ const useStyles = makeStyles((theme) => ({
running: { running: {
color: green[500] color: green[500]
}, },
loading:{ loading: {
color: theme.palette.primary.dark color: theme.palette.primary.dark
},
info: {
display: 'flex',
'& div': {
margin: 4
}
} }
})); }));
/**
* Displays status indicators at the bottom of the page.
*/
const StatusBar = () => { const StatusBar = () => {
const classes = useStyles(); const classes = useStyles();
const [{ loading, error }] = useStatusReducer(); const [{ loading, error }] = useStatusReducer();
const [isLoading, setLoading] = useState(loading); const [isLoading, setLoading] = useState(loading);
const { config } = useContext(ConsoleContext); const { config } = useContext(ConsoleContext);
const { build: { name, buildDate, version } } = config;
useEffect(() => { useEffect(() => {
let t; let t;
@ -92,7 +104,13 @@ const StatusBar = () => {
<PublicIcon /> <PublicIcon />
</Link> </Link>
</div> </div>
<div className={classes.center}>(c) {config.app.org} {version}</div>
<div className={classes.info}>
<div>{name} ({version})</div>
<div>{moment(buildDate).format('L')}</div>
<VersionCheck />
</div>
<div className={classes.right}> <div className={classes.right}>
<LoadingIcon className={clsx(classes.icon, isLoading && classes.loading)} /> <LoadingIcon className={clsx(classes.icon, isLoading && classes.loading)} />
<StatusIcon error={error} /> <StatusIcon error={error} />

View File

@ -0,0 +1,67 @@
//
// Copyright 2020 DxOS.org
//
import compareVersions from 'compare-versions';
import React, { useEffect, useState } from 'react';
import { useQuery } from '@apollo/react-hooks';
import { makeStyles } from '@material-ui/core';
import SYSTEM_STATUS from '../../gql/system_status.graphql';
import WNS_RECORDS from '../../gql/wns_records.graphql';
import { useQueryStatusReducer } from '../hooks';
const CHECK_INTERVAL = 5 * 60 * 1000;
const useStyles = makeStyles(theme => ({
update: {
color: theme.palette.error.light
}
}));
/**
* Checks for a system upgrade.
*/
const VersionCheck = () => {
const classes = useStyles();
const [{ current, latest }, setUpgrade] = useState({});
const status = useQueryStatusReducer(useQuery(SYSTEM_STATUS));
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: CHECK_INTERVAL,
variables: { type: 'wrn:resource' }
}));
// Check version.
useEffect(() => {
if (status && data) {
const { dxos: { image: current } } = JSON.parse(status.system_status.json);
let latest = current;
data.wns_records.json.forEach(({ attributes: { name, version } }) => {
if (name.startsWith('dxos/xbox:')) {
if (compareVersions(version, latest) > 0) {
latest = version;
}
}
});
if (latest !== current) {
setUpgrade({ current, latest });
}
}
}, [status, data]);
// TODO(burdon): Link to Github page with upgrade info.
return (
<>
{current && (
<div>SYS: {current}</div>
)}
{latest && (
<div className={classes.update}>LATEST: {latest}</div>
)}
</>
);
};
export default VersionCheck;

View File

@ -27,7 +27,7 @@ const Status = () => {
<Toolbar /> <Toolbar />
} }
> >
<Json data={data.system_status} /> <Json data={JSON.parse(data.system_status.json)} />
</Panel> </Panel>
); );
}; };

View File

@ -7,7 +7,7 @@ import { useQuery } from '@apollo/react-hooks';
import WNS_RECORDS from '../../../../gql/wns_records.graphql'; import WNS_RECORDS from '../../../../gql/wns_records.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../../hooks'; 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';
@ -21,6 +21,7 @@ import TableCell from '../../../components/TableCell';
const AppRecords = () => { const AppRecords = () => {
const { config } = useContext(ConsoleContext); const { config } = useContext(ConsoleContext);
const [sorter, sortBy] = useSorter('id');
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, { const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.pollInterval, pollInterval: config.api.pollInterval,
variables: { type: 'wrn:app' } variables: { type: 'wrn:app' }
@ -32,9 +33,6 @@ const AppRecords = () => {
const records = data.wns_records.json; 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. // TODO(burdon): Test if app is deployed.
const getAppUrl = ({ name, version }) => { const getAppUrl = ({ name, version }) => {
const base = getServiceUrl(config, 'app.server'); const base = getServiceUrl(config, 'app.server');
@ -56,9 +54,9 @@ const AppRecords = () => {
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Name</TableCell> <TableCell onClick={sortBy('name')}>ID</TableCell>
<TableCell size="small">Version</TableCell> <TableCell onClick={sortBy('version')} size="small">Version</TableCell>
<TableCell>Description</TableCell> <TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell>Link</TableCell> <TableCell>Link</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>

View File

@ -7,7 +7,7 @@ import { useQuery } from '@apollo/react-hooks';
import WNS_RECORDS from '../../../../gql/wns_records.graphql'; import WNS_RECORDS from '../../../../gql/wns_records.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../../hooks'; import { ConsoleContext, useQueryStatusReducer, useSorter } from '../../../hooks';
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';
@ -18,6 +18,7 @@ import TableCell from '../../../components/TableCell';
const AppRecords = () => { const AppRecords = () => {
const { config } = useContext(ConsoleContext); const { config } = useContext(ConsoleContext);
const [sorter, sortBy] = useSorter('id');
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, { const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.pollInterval, pollInterval: config.api.pollInterval,
variables: { type: 'wrn:bot' } variables: { type: 'wrn:bot' }
@ -29,16 +30,14 @@ const AppRecords = () => {
const records = data.wns_records.json; 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 ( return (
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell>Name</TableCell> <TableCell onClick={sortBy('name')}>ID</TableCell>
<TableCell size="small">Version</TableCell> <TableCell onClick={sortBy('version')} size="small">Version</TableCell>
<TableCell>Description</TableCell> <TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell />
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>

View File

@ -15,7 +15,7 @@ import TableBody from '@material-ui/core/TableBody';
import WNS_RECORDS from '../../../../gql/wns_records.graphql'; import WNS_RECORDS from '../../../../gql/wns_records.graphql';
import { ConsoleContext, useQueryStatusReducer } from '../../../hooks'; import { ConsoleContext, useQueryStatusReducer, useSorter } from '../../../hooks';
import Table from '../../../components/Table'; import Table from '../../../components/Table';
import TableCell from '../../../components/TableCell'; import TableCell from '../../../components/TableCell';
@ -64,7 +64,7 @@ export const WNSRecordType = ({ type = types[0].key, onChanged }) => {
const WNSRecords = ({ type }) => { const WNSRecords = ({ type }) => {
const { config } = useContext(ConsoleContext); const { config } = useContext(ConsoleContext);
const [{ sort, ascend }, setSort] = useState({ sort: 'type', ascend: true }); const [sorter, sortBy] = useSorter('id');
const data = useQueryStatusReducer(useQuery(WNS_RECORDS, { const data = useQueryStatusReducer(useQuery(WNS_RECORDS, {
pollInterval: config.api.pollInterval, pollInterval: config.api.pollInterval,
variables: { type } variables: { type }
@ -76,23 +76,14 @@ const WNSRecords = ({ type }) => {
const records = data.wns_records.json; 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 ( return (
<Table> <Table>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell onClick={sortBy('type')} size="small">Type</TableCell> <TableCell onClick={sortBy('type')} size="small">Type</TableCell>
<TableCell onClick={sortBy('name')}>Name</TableCell> <TableCell onClick={sortBy('name')}>ID</TableCell>
<TableCell onClick={sortBy('version')} size="small">Version</TableCell> <TableCell onClick={sortBy('version')} size="small">Version</TableCell>
<TableCell onClick={sortBy('attributes.displayName')}>Description</TableCell> <TableCell onClick={sortBy('attributes.displayName')}>Name</TableCell>
<TableCell onClick={sortBy('attributes.package')}>Package Hash</TableCell> <TableCell onClick={sortBy('attributes.package')}>Package Hash</TableCell>
<TableCell onClick={sortBy('createTime')} size="small">Created</TableCell> <TableCell onClick={sortBy('createTime')} size="small">Created</TableCell>
</TableRow> </TableRow>

View File

@ -4,4 +4,5 @@
export * from './context'; export * from './context';
export * from './registry'; export * from './registry';
export * from './sorter';
export * from './status'; export * from './status';

View File

@ -0,0 +1,25 @@
//
// Copyright 2020 DxOS.org
//
import get from 'lodash.get';
import { useState } from 'react';
// TODO(burdon): Enable multiple sort order (e.g., id, version).
export const useSorter = (initial) => {
const [{ sort, ascend }, setSort] = useState({ sort: initial, 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;
};
const sortBy = field => () => setSort({ sort: field, ascend: (field === sort ? !ascend : true) });
return [
sorter,
sortBy
];
};

View File

@ -10,6 +10,8 @@ import { getServiceUrl } from './util/config';
const log = debug('dxos:console:client:resolvers'); const log = debug('dxos:console:client:resolvers');
const timestamp = () => new Date().toUTCString();
/** /**
* Resolvers * Resolvers
* https://www.apollographql.com/docs/tutorial/local-state/#local-resolvers * https://www.apollographql.com/docs/tutorial/local-state/#local-resolvers
@ -27,6 +29,9 @@ export const createResolvers = config => {
return { return {
__typename: 'JSONResult', __typename: 'JSONResult',
timestamp: timestamp(),
// NOTE: Hack since this should be a string according to the schema.
json: data json: data
}; };
}, },
@ -37,6 +42,9 @@ export const createResolvers = config => {
return { return {
__typename: 'JSONResult', __typename: 'JSONResult',
timestamp: timestamp(),
// NOTE: Hack since this should be a string according to the schema.
json: data json: data
}; };
}, },
@ -47,6 +55,7 @@ export const createResolvers = config => {
// TODO(burdon): Use Registry API rather than from CLI? // TODO(burdon): Use Registry API rather than from CLI?
return { return {
__typename: 'JSONLog', __typename: 'JSONLog',
timestamp: timestamp(),
log: [] log: []
}; };
} }

View File

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

View File

@ -2,30 +2,28 @@
# Copyright 2020 DxOS.org # Copyright 2020 DxOS.org
# #
type JSONResult {
json: String!
}
type Log {
log: [String]!
}
type Status {
timestamp: String!
version: String!
}
type Result { type Result {
timestamp: String! timestamp: String!
code: Int! code: Int!
} }
type Log {
timestamp: String!
log: [String]!
}
# TODO(burdon): Generic result.
type JSONResult {
timestamp: String!
json: String!
}
# #
# Schema # Schema
# #
type Query { type Query {
system_status: Status! system_status: JSONResult!
ipfs_status: JSONResult! ipfs_status: JSONResult!
wns_status: JSONResult! wns_status: JSONResult!
# TODO(burdon): Import WNS schema! # TODO(burdon): Import WNS schema!

View File

@ -5,8 +5,6 @@
import debug from 'debug'; import debug from 'debug';
import IpfsHttpClient from 'ipfs-http-client'; import IpfsHttpClient from 'ipfs-http-client';
import { version } from '../package.json';
const log = debug('dxos:console:server:resolvers'); const log = debug('dxos:console:server:resolvers');
const timestamp = () => new Date().toUTCString(); const timestamp = () => new Date().toUTCString();
@ -39,7 +37,11 @@ export const createResolvers = config => ({
system_status: () => ({ system_status: () => ({
timestamp: timestamp(), timestamp: timestamp(),
version json: JSON.stringify({
dxos: {
image: '0.0.1'
}
})
}), }),
// //

View File

@ -5260,6 +5260,11 @@ compare-func@^1.3.1:
array-ify "^1.0.0" array-ify "^1.0.0"
dot-prop "^3.0.0" dot-prop "^3.0.0"
compare-versions@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62"
integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==
component-emitter@^1.2.1: component-emitter@^1.2.1:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0"