Begin implementing Pond webui
This commit is contained in:
parent
663cdbe167
commit
4cf09f724b
23
testbed/front/.gitignore
vendored
Normal file
23
testbed/front/.gitignore
vendored
Normal 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
12826
testbed/front/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
33
testbed/front/package.json
Normal file
33
testbed/front/package.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
14
testbed/front/public/index.html
Normal file
14
testbed/front/public/index.html
Normal 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>
|
15
testbed/front/public/manifest.json
Normal file
15
testbed/front/public/manifest.json
Normal 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
11
testbed/front/src/App.css
Normal 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
36
testbed/front/src/App.js
Normal 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
|
9
testbed/front/src/App.test.js
Normal file
9
testbed/front/src/App.test.js
Normal 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);
|
||||||
|
});
|
80
testbed/front/src/FullNode.js
Normal file
80
testbed/front/src/FullNode.js
Normal 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
|
40
testbed/front/src/NodeList.js
Normal file
40
testbed/front/src/NodeList.js
Normal 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
|
13
testbed/front/src/index.css
Normal file
13
testbed/front/src/index.css
Normal 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;
|
||||||
|
}
|
6
testbed/front/src/index.js
Normal file
6
testbed/front/src/index.js
Normal 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
116
testbed/main.go
Normal 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)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user