Merge pull request #208 from filecoin-project/feat/single-node-ui

Basic single node UI
This commit is contained in:
Łukasz Magiera 2019-09-19 16:39:10 +02:00 committed by GitHub
commit 374759edeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 531 additions and 91 deletions

41
cli/auth.go Normal file
View File

@ -0,0 +1,41 @@
package cli
import (
"fmt"
"gopkg.in/urfave/cli.v2"
"github.com/filecoin-project/go-lotus/api"
)
var authCmd = &cli.Command{
Name: "auth",
Usage: "Manage RPC permissions",
Subcommands: []*cli.Command{
authCreateAdminToken,
},
}
var authCreateAdminToken = &cli.Command{
Name: "create-admin-token",
Usage: "Create admin token",
Action: func(cctx *cli.Context) error {
napi, err := GetFullNodeAPI(cctx)
if err != nil {
return err
}
ctx := ReqContext(cctx)
// TODO: Probably tell the user how powerful this token is
token, err := napi.AuthNew(ctx, api.AllPermissions)
if err != nil {
return err
}
// TODO: Log in audit log when it is implemented
fmt.Println(string(token))
return nil
},
}

View File

@ -110,6 +110,7 @@ func ReqContext(cctx *cli.Context) context.Context {
}
var Commands = []*cli.Command{
authCmd,
chainCmd,
clientCmd,
createMinerCmd,

View File

@ -5816,6 +5816,11 @@
"resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
"integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE="
},
"gud": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz",
"integrity": "sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw=="
},
"gzip-size": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.0.0.tgz",
@ -5957,6 +5962,19 @@
"resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz",
"integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ=="
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
@ -8378,6 +8396,16 @@
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
},
"mini-create-react-context": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz",
"integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==",
"requires": {
"@babel/runtime": "^7.4.0",
"gud": "^1.0.0",
"tiny-warning": "^1.0.2"
}
},
"mini-css-extract-plugin": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz",
@ -10521,6 +10549,60 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
},
"react-router": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.1.tgz",
"integrity": "sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.3.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"hoist-non-react-statics": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz",
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
"requires": {
"react-is": "^16.7.0"
}
},
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
},
"path-to-regexp": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz",
"integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=",
"requires": {
"isarray": "0.0.1"
}
}
}
},
"react-router-dom": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.0.1.tgz",
"integrity": "sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.0.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-scripts": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-3.0.1.tgz",
@ -10887,6 +10969,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz",
"integrity": "sha1-six699nWiBvItuZTM17rywoYh0g="
},
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz",
@ -12083,6 +12170,16 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"tiny-invariant": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz",
"integrity": "sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -12478,6 +12575,11 @@
"spdx-expression-parse": "^3.0.0"
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"varint": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/varint/-/varint-5.0.0.tgz",

View File

@ -11,6 +11,7 @@
"react": "^16.8.6",
"react-cristal": "^0.0.12",
"react-dom": "^16.8.6",
"react-router-dom": "^5.0.1",
"react-scripts": "3.0.1",
"rpc-websockets": "^4.5.1",
"styled-components": "^3.3.3",

View File

@ -105,7 +105,7 @@ class Address extends React.Component {
}
addr = <span className={`pondaddr-${this.props.addr}`}
onMouseEnter={() => sheet.sheet.insertRule(`.pondaddr-${this.props.addr}, .pondaddr-${this.props.addr} * { color: #11ee11; }`, 0)}
onMouseEnter={() => sheet.sheet.insertRule(`.pondaddr-${this.props.addr}, .pondaddr-${this.props.addr} * { color: #11ee11 !important; }`, 0)}
onMouseLeave={() => sheet.sheet.deleteRule(0)}
>{addr}</span>

View File

@ -1,11 +1,129 @@
.Index {
width: 100vw;
height: 100vh;
background: #1a1a1a;
color: #f0f0f0;
font-family: monospace;
display: grid;
grid-template-columns: auto 40vw auto;
grid-template-rows: auto auto auto 3em;
grid-template-areas:
". . ."
". main ."
". . ."
"footer footer footer";
}
.Index-footer {
background: #2a2a2a;
grid-area: footer;
}
.Index-footer > div {
padding-left: 0.7em;
padding-top: 0.7em;
}
.Index-nodes {
grid-area: main;
background: #2a2a2a;
}
.Index-node {
margin: 5px;
padding: 15px;
background: #1f1f1f;
}
.Index-addwrap {
margin-top: 5px;
}
/* SingleNode */
.SingleNode-connecting {
width: 100vw;
height: 100vh;
background: #1a1a1a;
color: #ffffff;
font-family: monospace;
display: grid;
grid-template-columns: auto min-content auto;
grid-template-rows: auto min-content auto;
grid-template-areas:
". . ."
". main ."
". . ."
}
.SingleNode-connecting > div {
grid-area: main;
padding: 15px;
white-space: nowrap;
background: #2a2a2a;
}
/*****/
a:link {
color: #50f020;
}
a:visited {
color: #50f020;
}
a:hover {
color: #30a00a;
}
.Button {
display: inline-block;
padding: 15px;
background: #1f1f1f;
margin-left: 5px;
}
.Window {
background: #2a2a2a !important;
color: #e0e0e0;
}
.Window b {
color: #f0f0f0;
}
.Window > :first-child > :nth-child(2)::before {
background: #f0f0f0;
}
.Window > :first-child > :nth-child(2)::after {
background: #f0f0f0;
}
.Window a:link {
color: #30a015;
}
.Window a:visited {
color: #30a015;
}
/* POND */
.App {
min-height: 100vh;
background: #b7c4cd;
background: #1a1a1a;
font-family: monospace;
}
.NodeList {
background: #f9be77;
user-select: text;
font-family: monospace;
min-width: 40em;
@ -13,7 +131,6 @@
}
.FullNode {
background: #f9be77;
user-select: text;
font-family: monospace;
min-width: 50em;
@ -25,7 +142,6 @@
}
.StorageNode {
background: #f9be77;
user-select: text;
font-family: monospace;
min-width: 40em;
@ -33,7 +149,6 @@
}
.Block {
background: #f9be77;
user-select: text;
font-family: monospace;
min-width: 50em;
@ -41,7 +156,6 @@
}
.State {
background: #f9be77;
user-select: text;
font-family: monospace;
min-width: 40em;
@ -49,7 +163,6 @@
}
.Client {
background: #f9be77;
user-select: text;
font-family: monospace;
display: inline-block;
@ -68,19 +181,20 @@
.ChainExplorer {
font-family: monospace;
color: #d0d0d0;
}
.ChainExplorer-at {
min-width: 40em;
background: #77ff77;
background: #222222;
}
.ChainExplorer-after {
background: #cc9c44
background: #440000
}
.ChainExplorer-before {
background: #cccc00
background: #444400
}
.Logs {

View File

@ -1,53 +1,96 @@
import React from 'react';
import './App.css';
import { Client } from 'rpc-websockets'
import NodeList from "./NodeList";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
import Pond from "./Pond";
import SingleNode from "./SingleNode";
class Index extends React.Component {
constructor(props) {
super(props)
this.state = {rpcUrl: "ws://127.0.0.1:1234/rpc/v0", rpcToken: ''}
const initialState = JSON.parse(window.localStorage.getItem('saved-nodes'))
if (initialState) {
this.state.nodes = initialState
} else {
this.state.nodes = []
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
window.localStorage.setItem('saved-nodes', JSON.stringify(this.state.nodes))
}
onAdd = () => {
this.setState({addingNode: true})
}
update = (name) => (e) => this.setState({ [name]: e.target.value })
tokenOk = () => {
let m = this.state.rpcToken.match(/\.(.+)\./)
// TODO: eww
if(m && atob(m[1]) === '{"Allow":["read","write","sign","admin"]}') {
return (
<span>-Token OK-
<div>
<button onClick={this.addNode}>Add Node</button>
</div>
</span>
)
}
return <span>-Expecting valid admin token-</span>
}
addNode = async () => {
this.setState(p => ({nodes: [...p.nodes, {addr: this.state.rpcUrl, token: this.state.rpcToken}], addingNode: true}))
}
render() {
return (
<div className="Index">
<div className="Index-nodes">
<div>
{
this.state.nodes.map((node, i) => <div className="Index-node">
<span>{i}. {node.addr} <Link to={`/app/node/${i}`}>[OPEN UI]</Link></span>
</div>)
}
</div>
<a hidden={this.state.addingNode} href='#' onClick={this.onAdd} className="Button">[Add Node]</a>
<div hidden={!this.state.addingNode}>
<div>---------------</div>
<div>
+ RPC:<input defaultValue={"ws://127.0.0.1:1234/rpc/v0"} onChange={this.update("rpcUrl")}/>
</div>
<div>
Token (<code>lotus auth create-admin-token</code>): <input onChange={this.update("rpcToken")}/>{this.tokenOk()}
</div>
</div>
</div>
<div className="Index-footer">
<div>
<Link to={"/app/pond"}>Open Pond</Link>
</div>
</div>
</div>
)
}
}
class App extends React.Component {
constructor(props) {
super(props)
const client = new Client('ws://127.0.0.1:2222/rpc/v0')
client.on('open', () => {
this.setState(() => ({client: client}))
})
this.state = {
windows: {},
nextWindow: 0,
}
this.mountWindow = this.mountWindow.bind(this)
}
mountWindow(cb) {
const id = this.state.nextWindow
this.setState({nextWindow: id + 1})
const window = cb(() => {
this.setState(prev => ({windows: {...prev.windows, [id]: undefined}}))
})
this.setState(prev => ({windows: {...prev.windows, [id]: window}}))
}
render() {
if (this.state.client === undefined) {
return (
<div>
Connecting to RPC
</div>
)
}
return (
<div className="App">
<NodeList client={this.state.client} mountWindow={this.mountWindow}/>
<div>
{Object.keys(this.state.windows).map((w, i) => <div key={i}>{this.state.windows[w]}</div>)}
</div>
</div>
<Router>
<Route path="/" exact component={Index} />
<Route path="/app/pond/" component={Pond} />
<Route path="/app/node/:node" component={SingleNode} />
</Router>
)
}
}

View File

@ -1,9 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import Pond from './Pond';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
ReactDOM.render(<Pond />, div);
ReactDOM.unmountComponentAtNode(div);
});

View File

@ -1,7 +1,7 @@
import React from 'react';
import {Cristal} from "react-cristal";
import {BlockLinks} from "./BlockLink";
import Address from "./Address";
import Window from "./Window";
class Block extends React.Component {
constructor(props) {
@ -57,9 +57,9 @@ class Block extends React.Component {
)
}
return (<Cristal className="CristalScroll" initialSize={{width: 700, height: 400}} onClose={this.props.onClose} title={`Block ${this.props.cid['/']}`}>
return (<Window className="CristalScroll" initialSize={{width: 700, height: 400}} onClose={this.props.onClose} title={`Block ${this.props.cid['/']}`}>
{content}
</Cristal>)
</Window>)
}
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import {Cristal} from "react-cristal";
import {BlockLinks} from "./BlockLink";
import Window from "./Window";
const rows = 32
@ -132,9 +132,9 @@ class ChainExplorer extends React.Component {
return <div key={row} className={className}>@{row} {info}</div>
})}</div>
return (<Cristal onClose={this.props.onClose} title={`Chain Explorer ${this.state.follow ? '(Following)' : ''}`}>
return (<Window onClose={this.props.onClose} title={`Chain Explorer ${this.state.follow ? '(Following)' : ''}`}>
{content}
</Cristal>)
</Window>)
}
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import Cristal from 'react-cristal'
import Address from "./Address";
import Window from "./Window";
const dealStates = [
"Unknown",
@ -70,7 +70,7 @@ class Client extends React.Component {
let ppb = Math.round(this.state.total / this.state.blocks * 100) / 100
let ppmbb = Math.round(ppb / (this.state.kbs / 1000) * 100) / 100
let dealMaker = <div>
let dealMaker = <div hidden={!this.props.pondClient}>
<span>Make Deal: </span>
<select><option>t0101</option></select>
<abbr title="Data length">L:</abbr> <input placeholder="KBs" defaultValue={1} onChange={this.update("kbs")}/>
@ -94,12 +94,12 @@ class Client extends React.Component {
</div>)
return <Cristal title={"Client - Node " + this.props.node.ID}>
return <Window title={"Client - Node " + this.props.node.ID} onClose={this.props.onClose}>
<div className="Client">
<div>{dealMaker}</div>
<div>{deals}</div>
</div>
</Cristal>
</Window>
}
}

View File

@ -1,5 +1,5 @@
import React from 'react';
import Cristal from 'react-cristal'
import Window from "./Window";
async function awaitReducer(prev, c) {
return {...await prev, ...await c}
@ -107,7 +107,7 @@ class ConnMgr extends React.Component {
})
return(
<Cristal title={`Connection Manager${this.state.lock ? ' (syncing)' : ''}`}>
<Window title={`Connection Manager${this.state.lock ? ' (syncing)' : ''}`}>
<table>
<thead><tr><td></td>{keys.slice(0, -1).map((i) => (<td key={i}>{i}</td>))}</tr></thead>
<tbody>{rows}</tbody>
@ -118,7 +118,7 @@ class ConnMgr extends React.Component {
<button onClick={this.connect1}>Conn1</button>
<button onClick={this.connectChain}>ConnChain</button>
</div>
</Cristal>
</Window>
)
}
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import {Cristal} from "react-cristal";
import {BlockLinks} from "./BlockLink";
import Window from "./Window";
function styleForHDiff(max, act) {
switch (max - act) {
@ -42,7 +42,7 @@ class Consensus extends React.Component {
}
render() {
return (<Cristal title={`Consensus`}>
return (<Window title={`Consensus`}>
<div className='Consensus'>
<div>Max Height: {this.state.maxH}</div>
<div>
@ -62,7 +62,7 @@ class Consensus extends React.Component {
</table>
</div>
</div>
</Cristal>)
</Window>)
}
}

View File

@ -1,10 +1,9 @@
import React from 'react';
import Cristal from 'react-cristal'
import { BlockLinks } from "./BlockLink";
import StorageNodeInit from "./StorageNodeInit";
import Address from "./Address";
import ChainExplorer from "./ChainExplorer";
import Client from "./Client";
import Window from "./Window";
class FullNode extends React.Component {
constructor(props) {
@ -121,7 +120,7 @@ class FullNode extends React.Component {
miners = this.state.minerList.map((a, k) => <div key={k}><Address miner={true} client={this.props.client} addr={a} mountWindow={this.props.mountWindow}/></div>)
}
let storageMine = <a href="#" onClick={this.startStorageMiner}>[Spawn Storage Miner]</a>
let storageMine = <a href="#" onClick={this.startStorageMiner} hidden={!this.props.spawnStorageNode}>[Spawn Storage Miner]</a>
let addresses = this.state.addrs.map((addr) => {
let line = <Address client={this.props.client} add1k={this.add1k} add10k={true} nonce={true} addr={addr} mountWindow={this.props.mountWindow}/>
@ -167,10 +166,13 @@ class FullNode extends React.Component {
)
}
let nodeID = this.props.node.ID ? this.props.node.ID : ''
let nodePos = this.props.node.ID ? {x: this.props.node.ID*30, y: this.props.node.ID * 30} : 'center'
return (
<Cristal
title={"Node " + this.props.node.ID}
initialPosition={{x: this.props.node.ID*30, y: this.props.node.ID * 30}}
<Window
title={"Node " + nodeID}
initialPosition={nodePos}
initialSize={{width: 690, height: 300}}
onClose={this.stop} >
<div className="CristalScroll">
@ -178,7 +180,7 @@ class FullNode extends React.Component {
{runtime}
</div>
</div>
</Cristal>
</Window>
)
}
}

View File

@ -1,9 +1,9 @@
import React from 'react'
import {Cristal} from "react-cristal";
import { Terminal } from 'xterm';
import { AttachAddon } from "xterm-addon-attach";
import 'xterm/dist/xterm.css';
import * as fit from 'xterm/lib/addons/fit/fit';
import Window from "./Window";
class Logs extends React.Component {
constructor(props) {
@ -22,9 +22,9 @@ class Logs extends React.Component {
}
render() {
return <Cristal className="Logs-window" onClose={this.props.onClose} initialSize={{width: 1000, height: 480}} title={`Node ${this.props.node} Logs`}>
return <Window className="Logs-window" onClose={this.props.onClose} initialSize={{width: 1000, height: 480}} title={`Node ${this.props.node} Logs`}>
<div ref={this.termRef} className="Logs"/>
</Cristal>
</Window>
}
}

View File

@ -2,12 +2,12 @@ import React from 'react';
import FullNode from "./FullNode";
import ConnMgr from "./ConnMgr";
import Consensus from "./Consensus";
import {Cristal} from "react-cristal";
import StorageNode from "./StorageNode";
import {Client} from "rpc-websockets";
import pushMessage from "./chain/send";
import Logs from "./Logs";
import StorageNodeInit from "./StorageNodeInit";
import Window from "./Window";
const [NodeUnknown, NodeRunning, NodeStopped] = [0, 1, 2]
@ -155,7 +155,7 @@ class NodeList extends React.Component {
}
return (
<Cristal title={"Node List"} initialPosition="bottom-left">
<Window title={"Node List"} initialPosition="bottom-left">
<div className={'NodeList'}>
<div>
<button onClick={this.spawnNode} disabled={!this.state.existingLoaded}>Spawn Node</button>
@ -190,7 +190,7 @@ class NodeList extends React.Component {
{connMgr}
{consensus}
</div>
</Cristal>
</Window>
);
}
}

View File

@ -0,0 +1,55 @@
import React from 'react';
import './App.css';
import { Client } from 'rpc-websockets'
import NodeList from "./NodeList";
class Pond extends React.Component {
constructor(props) {
super(props)
const client = new Client('ws://127.0.0.1:2222/rpc/v0')
client.on('open', () => {
this.setState(() => ({client: client}))
})
this.state = {
windows: {},
nextWindow: 0,
}
this.mountWindow = this.mountWindow.bind(this)
}
mountWindow(cb) {
const id = this.state.nextWindow
this.setState({nextWindow: id + 1})
const window = cb(() => {
this.setState(prev => ({windows: {...prev.windows, [id]: undefined}}))
})
this.setState(prev => ({windows: {...prev.windows, [id]: window}}))
}
render() {
if (this.state.client === undefined) {
return (
<div>
Connecting to RPC
</div>
)
}
return (
<div className="App">
<NodeList client={this.state.client} mountWindow={this.mountWindow}/>
<div>
{Object.keys(this.state.windows).map((w, i) => <div key={i}>{this.state.windows[w]}</div>)}
</div>
</div>
)
}
}
export default Pond

View File

@ -0,0 +1,67 @@
import React from 'react';
import './App.css';
import {Client} from "rpc-websockets";
import FullNode from "./FullNode";
class SingleNode extends React.Component {
constructor(props) {
super(props)
const nodes = JSON.parse(window.localStorage.getItem('saved-nodes'))
const node = nodes[this.props.match.params.node]
const client = new Client(`${node.addr}?token=${node.token}`)
client.on('open', async () => {
this.setState(() => ({client: client}))
})
this.state = {
windows: {},
nextWindow: 0,
addr: node.addr
}
}
mountWindow = (cb) => {
const id = this.state.nextWindow
this.setState({nextWindow: id + 1})
const window = cb(() => {
this.setState(prev => ({windows: {...prev.windows, [id]: undefined}}))
})
this.setState(prev => ({windows: {...prev.windows, [id]: window}}))
}
render() {
if (this.state.client === undefined) {
return (
<div className="SingleNode-connecting">
<div>
<div>Connecting to Node RPC:</div>
<div>{`${this.state.addr}?token=****`}</div>
</div>
</div>
)
}
let node = <FullNode
node={{Repo: '/i/dont/exist/fixme', ID: ''}}
client={this.state.client}
give1k={null}
mountWindow={this.mountWindow}
/>
return (
<div className="App">
{node}
<div>
{Object.keys(this.state.windows).map((w, i) => <div key={i}>{this.state.windows[w]}</div>)}
</div>
</div>
)
}
}
export default SingleNode;

View File

@ -1,5 +1,5 @@
import React from 'react'
import {Cristal} from "react-cristal";
import Window from "./Window";
class State extends React.Component {
constructor(props) {
@ -21,9 +21,9 @@ class State extends React.Component {
<div>{Object.keys(this.state.State).map(k => <div key={k}>{k}: <span>{JSON.stringify(this.state.State[k])}</span></div>)}</div>
</div>
return <Cristal onClose={this.props.onClose} title={`Actor ${this.props.addr} @{this.props.ts.Height}`}>
return <Window onClose={this.props.onClose} title={`Actor ${this.props.addr} @{this.props.ts.Height}`}>
{content}
</Cristal>
</Window>
}
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import {Cristal} from "react-cristal";
import { Client } from 'rpc-websockets'
import Address from "./Address";
import Window from "./Window";
const stateConnected = 'connected'
const stateConnecting = 'connecting'
@ -121,7 +121,7 @@ class StorageNode extends React.Component {
)
}
return <Cristal
return <Window
title={"Storage Miner Node " + this.props.node.ID}
initialPosition={{x: this.props.node.ID*30, y: this.props.node.ID * 30}}
onClose={this.stop} >
@ -130,7 +130,7 @@ class StorageNode extends React.Component {
{runtime}
</div>
</div>
</Cristal>
</Window>
}
}

View File

@ -1,6 +1,5 @@
import React from 'react';
import {Cristal} from "react-cristal";
import StorageNode from "./StorageNode";
import Window from "./Window";
class StorageNodeInit extends React.Component {
async componentDidMount() {
@ -11,7 +10,7 @@ class StorageNodeInit extends React.Component {
}
render() {
return <Cristal
return <Window
title={"Storage miner initializing"}
initialPosition={'center'}>
<div className="CristalScroll">
@ -19,7 +18,7 @@ class StorageNodeInit extends React.Component {
Waiting for init, make sure at least one miner is enabled
</div>
</div>
</Cristal>
</Window>
}
}

View File

@ -0,0 +1,15 @@
import React from 'react'
import {Cristal} from "react-cristal";
class Window extends React.Component {
render() {
let props = {className: '', ...this.props}
props.className = `${props.className} Window`
return <Cristal {...props}>
{this.props.children}
</Cristal>
}
}
export default Window