Begin implementing Pond webui

This commit is contained in:
Łukasz Magiera 2019-07-24 19:10:44 +02:00
parent 663cdbe167
commit 4cf09f724b
13 changed files with 13222 additions and 0 deletions

23
testbed/front/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

12826
testbed/front/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,33 @@
{
"name": "front",
"version": "0.1.0",
"private": true,
"dependencies": {
"jsonrpc-websocket-client": "^0.5.0",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"rpc-websockets": "^4.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#b7c4cd" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Lotus Pond</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

View File

@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

11
testbed/front/src/App.css Normal file
View File

@ -0,0 +1,11 @@
.App {
min-height: 100vh;
background: #b7c4cd;
font-family: monospace;
}
.FullNode {
background: #f9be77;
margin-bottom: 5px;
padding: 5px;
}

36
testbed/front/src/App.js Normal file
View File

@ -0,0 +1,36 @@
import React from 'react';
import './App.css';
import { Client } from 'rpc-websockets'
import NodeList from "./NodeList";
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 = {}
}
render() {
if (this.state.client === undefined) {
return (
<div>
Connecting to RPC
</div>
)
}
return (
<div className="App">
<NodeList client={this.state.client}/>
</div>
)
}
}
export default App

View File

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

View File

@ -0,0 +1,80 @@
import React from 'react';
import { Client } from 'rpc-websockets'
const stateConnected = 'connected'
const stateConnecting = 'connecting'
const stateGettingToken = 'getting-token'
class FullNode extends React.Component {
constructor(props) {
super(props)
this.state = {
state: stateGettingToken
}
this.loadInfo = this.loadInfo.bind(this);
this.connect()
}
async connect() {
console.log("gettok")
const token = await this.props.pondClient.call('Pond.TokenFor', [this.props.node.ID])
this.setState(() => ({
state: stateConnecting,
token: token,
}))
const client = new Client(`ws://127.0.0.1:${this.props.node.ApiPort}/rpc/v0`)
client.on('open', () => {
this.setState(() => ({
state: stateConnected,
client: client,
version: {Version: "~version~"},
id: "~peerid~",
peers: -1
}))
this.loadInfo()
setInterval(this.loadInfo, 1000)
})
console.log(token) // todo: use
}
async loadInfo() {
const version = await this.state.client.call("Filecoin.Version", [])
this.setState(() => ({version: version}))
const id = await this.state.client.call("Filecoin.ID", [])
this.setState(() => ({id: id}))
const peers = await this.state.client.call("Filecoin.NetPeers", [])
this.setState(() => ({peers: peers.length}))
}
render() {
let runtime = <div></div>
if (this.state.state === stateConnected) {
runtime = (
<div>
<div>v{this.state.version.Version}, {this.state.id.substr(-8)}, {this.state.peers} peers</div>
</div>
)
}
return (
<div className="FullNode">
<div>{this.props.node.ID} - {this.state.state}</div>
{runtime}
</div>
)
}
}
export default FullNode

View File

@ -0,0 +1,40 @@
import React from 'react';
import FullNode from "./FullNode";
class NodeList extends React.Component {
constructor(props) {
super(props);
this.state = {
existingLoaded: false,
nodes: []
};
// This binding is necessary to make `this` work in the callback
this.spawnNode = this.spawnNode.bind(this);
this.props.client.call('Pond.Nodes').then(nodes => this.setState({existingLoaded: true, nodes: nodes}))
}
async spawnNode() {
const node = await this.props.client.call('Pond.Spawn')
console.log(node)
this.setState(state => ({nodes: state.nodes.concat(node)}))
}
render() {
return (
<div>
<div>
<button onClick={this.spawnNode} disabled={!this.state.existingLoaded}>Spawn Node</button>
</div>
<div>
{
this.state.nodes.map(node => <FullNode key={node.ID} node={node} pondClient={this.props.client} />)
}
</div>
</div>
);
}
}
export default NodeList

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View File

@ -0,0 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));

116
testbed/main.go Normal file
View File

@ -0,0 +1,116 @@
package main
import (
"fmt"
"io/ioutil"
"net/http"
"os"
"os/exec"
"sync"
"sync/atomic"
"time"
"github.com/pkg/errors"
"github.com/filecoin-project/go-lotus/lib/jsonrpc"
"github.com/filecoin-project/go-lotus/node/repo"
)
const listenAddr = "127.0.0.1:2222"
type runningNode struct {
cmd *exec.Cmd
meta nodeInfo
}
type api struct {
cmds int32
running map[int32]runningNode
runningLk sync.Mutex
}
type nodeInfo struct {
Repo string
ID int32
ApiPort int32
}
func (api *api) Spawn() (nodeInfo, error) {
dir, err := ioutil.TempDir(os.TempDir(), "lotus-")
if err != nil {
return nodeInfo{}, err
}
id := atomic.AddInt32(&api.cmds, 1)
cmd := exec.Command("./lotus", "daemon", "--api", fmt.Sprintf("%d", 2500 + id))
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
cmd.Env = []string{"LOTUS_PATH=" + dir}
if err := cmd.Start(); err != nil {
return nodeInfo{}, err
}
info := nodeInfo{
Repo: dir,
ID: id,
ApiPort: 2500 + id,
}
api.runningLk.Lock()
api.running[id] = runningNode{
cmd: cmd,
meta: info,
}
api.runningLk.Unlock()
time.Sleep(time.Millisecond * 750) // TODO: Something less terrible
return info, nil
}
func (api *api) Nodes() []nodeInfo {
api.runningLk.Lock()
out := make([]nodeInfo, 0, len(api.running))
for _, node := range api.running {
out = append(out, node.meta)
}
api.runningLk.Unlock()
return out
}
func (api *api) TokenFor(id int32) (string, error) {
api.runningLk.Lock()
defer api.runningLk.Unlock()
rnd, ok := api.running[id]
if !ok {
return "", errors.New("no running node with this ID")
}
r, err := repo.NewFS(rnd.meta.Repo)
if err != nil {
return "", err
}
t, err := r.APIToken()
if err != nil {
return "", err
}
return string(t), nil
}
func main() {
rpcServer := jsonrpc.NewServer()
rpcServer.Register("Pond", &api{running: map[int32]runningNode{}})
http.Handle("/", http.FileServer(http.Dir("testbed/front/build")))
http.Handle("/rpc/v0", rpcServer)
fmt.Printf("Listening on http://%s\n", listenAddr)
http.ListenAndServe(listenAddr, nil)
}