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