Skip to content
Snippets Groups Projects
Unverified Commit 2f164747 authored by Martyn Janes's avatar Martyn Janes Committed by GitHub
Browse files

Improved tooling for analysis and updated conflicts UI (#593)

* Improved tooling for analysis and updated conflicts UI

* :package: Update packr

* :package: Run packr2

* :recycle: Refactor analysis-dashboard FPC

* :rotating_light: Fix linter warnings

* :lipstick: Fix missing logo

* :arrow_up:

 Upgrade yarn dependencies

Co-authored-by: default avatarcapossele <angelocapossele@gmail.com>
parent 0ff0cb11
No related branches found
No related tags found
No related merge requests found
Showing
with 240 additions and 171 deletions
package dashboard package dashboard
import "sync" import (
"sync"
"time"
)
// activeConflictSet contains the set of the active conflicts, not yet finalized. // activeConflictSet contains the set of the active conflicts, not yet finalized.
type activeConflictSet struct { type activeConflictSet struct {
...@@ -20,7 +23,7 @@ func (cr *activeConflictSet) cleanUp() { ...@@ -20,7 +23,7 @@ func (cr *activeConflictSet) cleanUp() {
defer cr.lock.Unlock() defer cr.lock.Unlock()
for id, conflict := range cr.conflictSet { for id, conflict := range cr.conflictSet {
if conflict.isFinalized() { if conflict.isFinalized() || conflict.isOlderThan(1*time.Minute) {
delete(cr.conflictSet, id) delete(cr.conflictSet, id)
} }
} }
...@@ -59,6 +62,10 @@ func (cr *activeConflictSet) update(ID string, c conflict) { ...@@ -59,6 +62,10 @@ func (cr *activeConflictSet) update(ID string, c conflict) {
for nodeID, context := range c.NodesView { for nodeID, context := range c.NodesView {
cr.conflictSet[ID].NodesView[nodeID] = context cr.conflictSet[ID].NodesView[nodeID] = context
} }
tmp := cr.conflictSet[ID]
tmp.Modified = time.Now()
cr.conflictSet[ID] = tmp
} }
func (cr *activeConflictSet) delete(ID string) { func (cr *activeConflictSet) delete(ID string) {
......
...@@ -23,7 +23,7 @@ func TestActiveConflictsUpdate(t *testing.T) { ...@@ -23,7 +23,7 @@ func TestActiveConflictsUpdate(t *testing.T) {
} }
c.update("A", conflictA) c.update("A", conflictA)
require.Equal(t, conflictA, c.conflictSet["A"]) require.Equal(t, conflictA.NodesView, c.conflictSet["A"].NodesView)
// test second new update // test second new update
conflictB := conflict{ conflictB := conflict{
...@@ -38,7 +38,7 @@ func TestActiveConflictsUpdate(t *testing.T) { ...@@ -38,7 +38,7 @@ func TestActiveConflictsUpdate(t *testing.T) {
} }
c.update("B", conflictB) c.update("B", conflictB)
require.Equal(t, conflictB, c.conflictSet["B"]) require.Equal(t, conflictB.NodesView, c.conflictSet["B"].NodesView)
// test modify existing entry // test modify existing entry
conflictB = conflict{ conflictB = conflict{
...@@ -52,7 +52,7 @@ func TestActiveConflictsUpdate(t *testing.T) { ...@@ -52,7 +52,7 @@ func TestActiveConflictsUpdate(t *testing.T) {
}, },
} }
c.update("B", conflictB) c.update("B", conflictB)
require.Equal(t, conflictB, c.conflictSet["B"]) require.Equal(t, conflictB.NodesView, c.conflictSet["B"].NodesView)
// test entry removal // test entry removal
c.delete("B") c.delete("B")
......
package dashboard package dashboard
import "time"
// conflictSet is defined as a a map of conflict IDs and their conflict. // conflictSet is defined as a a map of conflict IDs and their conflict.
type conflictSet = map[string]conflict type conflictSet = map[string]conflict
// conflict defines the struct for the opinions of the nodes regarding a given conflict. // conflict defines the struct for the opinions of the nodes regarding a given conflict.
type conflict struct { type conflict struct {
NodesView map[string]voteContext `json:"nodesview" bson:"nodesview"` NodesView map[string]voteContext `json:"nodesview" bson:"nodesview"`
Modified time.Time `json:"modified" bson:"modified"`
} }
type voteContext struct { type voteContext struct {
...@@ -18,6 +21,7 @@ type voteContext struct { ...@@ -18,6 +21,7 @@ type voteContext struct {
func newConflict() conflict { func newConflict() conflict {
return conflict{ return conflict{
NodesView: make(map[string]voteContext), NodesView: make(map[string]voteContext),
Modified: time.Now(),
} }
} }
...@@ -52,3 +56,8 @@ func (c conflict) finalizedRatio() float64 { ...@@ -52,3 +56,8 @@ func (c conflict) finalizedRatio() float64 {
return (float64(count) / float64(len(c.NodesView))) return (float64(count) / float64(len(c.NodesView)))
} }
// isOlderThan returns true if the conflict is older (i.e., last modified time) than the given duration.
func (c conflict) isOlderThan(d time.Duration) bool {
return c.Modified.Add(d).After(time.Now())
}
...@@ -47,6 +47,8 @@ func TestCreateFPCUpdate(t *testing.T) { ...@@ -47,6 +47,8 @@ func TestCreateFPCUpdate(t *testing.T) {
} }
// check that createFPCUpdate returns a matching FPCMsg // check that createFPCUpdate returns a matching FPCMsg
require.Equal(t, want, createFPCUpdate(hbTest)) for k, v := range createFPCUpdate(hbTest).Conflicts {
require.Equal(t, want.Conflicts[k].NodesView, v.NodesView)
}
} }
node_modules
build
webpack.config.js
.eslintrc.js
*.d.ts
module.exports = {
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json"],
"tsconfigRootDir": __dirname,
"ecmaFeatures": {
"jsx": true
}
},
"settings": {
"react": {
"version": "detect"
}
},
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended"
],
"rules": {
"comma-dangle": ["error", "never"],
"eqeqeq": "error",
"brace-style": "off",
"@typescript-eslint/brace-style": [
"error",
"1tbs",
{
"allowSingleLine": false
}
],
"@typescript-eslint/no-inferrable-types": 0,
"@typescript-eslint/quotes": ["error", "double", { "avoidEscape": true }],
"@typescript-eslint/space-before-function-paren": 0,
"@typescript-eslint/semi": 1,
"@typescript-eslint/no-magic-numbers": 0,
"@typescript-eslint/strict-boolean-expressions": 0,
"@typescript-eslint/explicit-function-return-type": [
"error",
{
allowExpressions: true
}
],
"@typescript-eslint/typedef": [
"error",
{
"arrayDestructuring": false,
"arrowParameter": false,
"memberVariableDeclaration": true,
"parameter": true,
"objectDestructuring": false,
"propertyDeclaration": true,
"variableDeclaration": false
},
],
"@typescript-eslint/prefer-readonly-parameter-types": 0,
"@typescript-eslint/no-dynamic-delete": 0,
"@typescript-eslint/no-type-alias": 0,
"@typescript-eslint/explicit-member-accessibility": [
"error",
{
"overrides": {
"constructors": "off"
}
}
],
"@typescript-eslint/init-declarations": 0
}
};
\ No newline at end of file
{
"arrowParens": "always",
"semi": true,
"useTabs": false,
"tabWidth": 2,
"bracketSpacing": true,
"singleQuote": true
}
...@@ -5,22 +5,26 @@ ...@@ -5,22 +5,26 @@
"description": "GoShimmer Analysis Dashboard", "description": "GoShimmer Analysis Dashboard",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "start": "webpack-dev-server --mode development --hot --progress --colors --host 127.0.0.1 --port 9091 --open",
"start": "webpack-dev-server --mode development --hot --progress --colors --host 192.168.1.215 --port 9090 --open",
"build": "webpack -p --progress --colors", "build": "webpack -p --progress --colors",
"prettier": "prettier --write \"src/**/*.{ts,tsx,css}\"" "lint": "eslint src --ext .tsx,.ts",
"sass-lint": "sass-lint -v -c ./.sass-lint.yml ./src"
}, },
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.2.2", "@babel/core": "^7.2.2",
"@types/classnames": "^2.2.7", "@types/classnames": "^2.2.7",
"@types/glob": "^7.1.1",
"@types/react": "^16.7.20", "@types/react": "^16.7.20",
"@types/react-dom": "^16.0.11", "@types/react-dom": "^16.0.11",
"@types/react-router": "^5.1.7", "@types/react-router": "^5.1.7",
"@types/webpack": "^4.4.23", "@types/webpack": "^4.4.23",
"@typescript-eslint/eslint-plugin": "^3.2.0",
"@typescript-eslint/parser": "^3.2.0",
"babel-loader": "^8.0.5", "babel-loader": "^8.0.5",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^3.6.0", "css-loader": "^3.6.0",
"eslint": "^7.2.0",
"eslint-plugin-react": "^7.20.0",
"file-loader": "^6.0.0", "file-loader": "^6.0.0",
"html-loader": "^1.0.0-alpha.0", "html-loader": "^1.0.0-alpha.0",
"html-webpack-plugin": "^4.3.0", "html-webpack-plugin": "^4.3.0",
...@@ -35,7 +39,6 @@ ...@@ -35,7 +39,6 @@
"typescript": "^3.2.4", "typescript": "^3.2.4",
"url-loader": "^4.1.0", "url-loader": "^4.1.0",
"webpack": "^4.43.0", "webpack": "^4.43.0",
"webpack-cleanup-plugin": "^0.5.1",
"webpack-cli": "^3.3.11", "webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.1.14", "webpack-dev-server": "^3.1.14",
"webpack-hot-middleware": "^2.25.0" "webpack-hot-middleware": "^2.25.0"
......
import Autopeering from "app/components/Autopeering/Autopeering";
import Conflict from "app/components/FPC/Conflict";
import FPC from "app/components/FPC/FPC";
import { inject, observer } from "mobx-react"; import { inject, observer } from "mobx-react";
import * as React from 'react'; import React, { ReactNode } from "react";
import { hot } from "react-hot-loader/root";
import { withRouter } from "react-router";
import { Link, Redirect, Route, Switch } from "react-router-dom"; import { Link, Redirect, Route, Switch } from "react-router-dom";
import "./App.scss"; import "./App.scss";
import { AppProps } from './AppProps'; import { AppProps } from "./AppProps";
import { withRouter } from "react-router"; import Autopeering from "./components/Autopeering/Autopeering";
import Conflict from "./components/FPC/Conflict";
import FPC from "./components/FPC/FPC";
@inject("autopeeringStore") @inject("autopeeringStore")
@observer @observer
class App extends React.Component<AppProps, any> { class App extends React.Component<AppProps, unknown> {
componentDidMount(): void { public componentDidMount(): void {
this.props.autopeeringStore.connect(); this.props.autopeeringStore.connect();
} }
render() { public render(): ReactNode {
return ( return (
<div className="root"> <div className="root">
<header> <header>
...@@ -23,6 +24,11 @@ class App extends React.Component<AppProps, any> { ...@@ -23,6 +24,11 @@ class App extends React.Component<AppProps, any> {
<img src="/assets/logo-header.svg" alt="GoShimmer Analyser" /> <img src="/assets/logo-header.svg" alt="GoShimmer Analyser" />
<h1>GoShimmer Analyzer</h1> <h1>GoShimmer Analyzer</h1>
</Link> </Link>
<div className="badge-container">
{!this.props.autopeeringStore.websocketConnected &&
<div className="badge">Not connected</div>
}
</div>
<nav> <nav>
<Link to="/autopeering"> <Link to="/autopeering">
Autopeering Autopeering
...@@ -31,11 +37,6 @@ class App extends React.Component<AppProps, any> { ...@@ -31,11 +37,6 @@ class App extends React.Component<AppProps, any> {
Consensus Consensus
</Link> </Link>
</nav> </nav>
<div className="badge-container">
{!this.props.autopeeringStore.websocketConnected &&
<div className="badge">Not connected</div>
}
</div>
</header> </header>
<Switch> <Switch>
<Route path="/autopeering" component={Autopeering} /> <Route path="/autopeering" component={Autopeering} />
...@@ -49,4 +50,4 @@ class App extends React.Component<AppProps, any> { ...@@ -49,4 +50,4 @@ class App extends React.Component<AppProps, any> {
} }
} }
export default withRouter(App); export default hot(withRouter(App));
\ No newline at end of file \ No newline at end of file
import AutopeeringStore from "app/stores/AutopeeringStore"; import { AutopeeringStore } from "./stores/AutopeeringStore";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
export interface AppProps extends RouteComponentProps { export interface AppProps extends RouteComponentProps {
autopeeringStore?: AutopeeringStore; autopeeringStore: AutopeeringStore;
} }
import { shortenedIDCharCount } from "app/stores/AutopeeringStore"; import { shortenedIDCharCount } from "../../stores/AutopeeringStore";
import classNames from "classnames"; import classNames from "classnames";
import { inject, observer } from "mobx-react"; import { inject, observer } from "mobx-react";
import * as React from 'react'; import React, { ReactNode } from "react";
import "./Autopeering.scss"; import "./Autopeering.scss";
import { AutopeeringProps } from './AutopeeringProps'; import { AutopeeringProps } from "./AutopeeringProps";
import { NodeView } from "./NodeView"; import { NodeView } from "./NodeView";
@inject("autopeeringStore") @inject("autopeeringStore")
@observer @observer
export default class Autopeering extends React.Component<AutopeeringProps, any> { export default class Autopeering extends React.Component<AutopeeringProps, unknown> {
public componentDidMount(): void {
componentDidMount(): void {
this.props.autopeeringStore.start(); this.props.autopeeringStore.start();
} }
componentWillUnmount(): void { public componentWillUnmount(): void {
this.props.autopeeringStore.stop(); this.props.autopeeringStore.stop();
} }
render() { public render(): ReactNode {
let { nodeListView, search } = this.props.autopeeringStore const { nodeListView, search } = this.props.autopeeringStore;
return ( return (
<div className="auto-peering"> <div className="auto-peering">
<div className="header margin-b-m"> <div className="header margin-b-m">
...@@ -27,7 +26,7 @@ export default class Autopeering extends React.Component<AutopeeringProps, any> ...@@ -27,7 +26,7 @@ export default class Autopeering extends React.Component<AutopeeringProps, any>
<div className="row"> <div className="row">
<div className="badge neighbors"> <div className="badge neighbors">
Average number of neighbors: { Average number of neighbors: {
this.props.autopeeringStore.nodes && this.props.autopeeringStore.nodes.size > 0 ? this.props.autopeeringStore.nodes.size > 0 ?
(2 * this.props.autopeeringStore.connections.size / this.props.autopeeringStore.nodes.size).toPrecision(2).toString() (2 * this.props.autopeeringStore.connections.size / this.props.autopeeringStore.nodes.size).toPrecision(2).toString()
: 0 : 0
} }
...@@ -51,10 +50,10 @@ export default class Autopeering extends React.Component<AutopeeringProps, any> ...@@ -51,10 +50,10 @@ export default class Autopeering extends React.Component<AutopeeringProps, any>
/> />
</div> </div>
<div className="node-list"> <div className="node-list">
{nodeListView.length === 0 && search.length > 0 && ( {nodeListView.length === 0 && search.length > 0 &&
<p>There are no nodes to view with the current search parameters.</p> <p>There are no nodes to view with the current search parameters.</p>
)} }
{nodeListView.map((nodeId) => ( {nodeListView.map((nodeId) =>
<button <button
key={nodeId} key={nodeId}
onClick={() => this.props.autopeeringStore.handleNodeSelection(nodeId)} onClick={() => this.props.autopeeringStore.handleNodeSelection(nodeId)}
...@@ -66,18 +65,18 @@ export default class Autopeering extends React.Component<AutopeeringProps, any> ...@@ -66,18 +65,18 @@ export default class Autopeering extends React.Component<AutopeeringProps, any>
> >
{nodeId.substr(0, shortenedIDCharCount)} {nodeId.substr(0, shortenedIDCharCount)}
</button> </button>
))} )}
</div> </div>
</div> </div>
<div className="node-view-container"> <div className="node-view-container">
{!this.props.autopeeringStore.selectedNode && ( {!this.props.autopeeringStore.selectedNode &&
<div className="card"> <div className="card">
<p className="margin-t-t">Select a node to inspect its details.</p> <p className="margin-t-t">Select a node to inspect its details.</p>
</div> </div>
)} }
{this.props.autopeeringStore.selectedNode && ( {this.props.autopeeringStore.selectedNode &&
<NodeView {...this.props} /> <NodeView {...this.props} />
)} }
</div> </div>
</div> </div>
<div className="visualizer" id="visualizer" /> <div className="visualizer" id="visualizer" />
......
import AutopeeringStore from "app/stores/AutopeeringStore"; import { AutopeeringStore } from "../../stores/AutopeeringStore";
export interface AutopeeringProps { export interface AutopeeringProps {
autopeeringStore?: AutopeeringStore autopeeringStore: AutopeeringStore;
} }
import classNames from "classnames"; import classNames from "classnames";
import { shortenedIDCharCount } from "app/stores/AutopeeringStore"; import { shortenedIDCharCount } from "../../stores/AutopeeringStore";
import { inject, observer } from "mobx-react"; import { inject, observer } from "mobx-react";
import * as React from 'react'; import React, { ReactNode } from "react";
import "./NodeView.scss"; import "./NodeView.scss";
import { AutopeeringProps } from './AutopeeringProps'; import { AutopeeringProps } from "./AutopeeringProps";
@inject("autopeeringStore") @inject("autopeeringStore")
@observer @observer
export class NodeView extends React.Component<AutopeeringProps, any> { export class NodeView extends React.Component<AutopeeringProps, unknown> {
render() { public render(): ReactNode {
return ( return !this.props.autopeeringStore.selectedNode ? null :
<div className="card node-view"> <div className="card node-view">
<div className="card--header"> <div className="card--header">
<h3> <h3>
...@@ -20,10 +20,12 @@ export class NodeView extends React.Component<AutopeeringProps, any> { ...@@ -20,10 +20,12 @@ export class NodeView extends React.Component<AutopeeringProps, any> {
<div className="col"> <div className="col">
<label className="margin-b-t"> <label className="margin-b-t">
Incoming Incoming
<span className="badge">{this.props.autopeeringStore.selectedNodeInNeighbors.size.toString()}</span> <span className="badge">{
this.props.autopeeringStore.selectedNodeInNeighbors ?
this.props.autopeeringStore.selectedNodeInNeighbors.size.toString() : 0}</span>
</label> </label>
<div className="node-view--list"> <div className="node-view--list">
{this.props.autopeeringStore.inNeighborList.map(nodeId => ( {this.props.autopeeringStore.inNeighborList.map(nodeId =>
<button <button
key={nodeId} key={nodeId}
onClick={() => this.props.autopeeringStore.handleNodeSelection(nodeId)} onClick={() => this.props.autopeeringStore.handleNodeSelection(nodeId)}
...@@ -35,17 +37,18 @@ export class NodeView extends React.Component<AutopeeringProps, any> { ...@@ -35,17 +37,18 @@ export class NodeView extends React.Component<AutopeeringProps, any> {
> >
{nodeId.substr(0, shortenedIDCharCount)} {nodeId.substr(0, shortenedIDCharCount)}
</button> </button>
))} )}
</div> </div>
</div> </div>
<div className="col"> <div className="col">
<label className="margin-b-t"> <label className="margin-b-t">
Outgoing Outgoing
<span className="badge">{this.props.autopeeringStore.selectedNodeOutNeighbors.size.toString()}</span> <span className="badge">{this.props.autopeeringStore.selectedNodeOutNeighbors ?
this.props.autopeeringStore.selectedNodeOutNeighbors.size.toString() : 0}</span>
</label> </label>
<div className="node-view--list"> <div className="node-view--list">
{this.props.autopeeringStore.outNeighborList.map(nodeId => ( {this.props.autopeeringStore.outNeighborList.map(nodeId =>
<button <button
key={nodeId} key={nodeId}
onClick={() => this.props.autopeeringStore.handleNodeSelection(nodeId)} onClick={() => this.props.autopeeringStore.handleNodeSelection(nodeId)}
...@@ -57,11 +60,10 @@ export class NodeView extends React.Component<AutopeeringProps, any> { ...@@ -57,11 +60,10 @@ export class NodeView extends React.Component<AutopeeringProps, any> {
> >
{nodeId.substr(0, shortenedIDCharCount)} {nodeId.substr(0, shortenedIDCharCount)}
</button> </button>
))} )}
</div> </div>
</div> </div>
</div> </div>
</div> </div>;
);
} }
} }
...@@ -6,6 +6,10 @@ ...@@ -6,6 +6,10 @@
.conflict { .conflict {
margin: 40px; margin: 40px;
label {
min-width: 100px;
}
.header { .header {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
...@@ -23,14 +27,20 @@ ...@@ -23,14 +27,20 @@
.node-details { .node-details {
@include font-size(16px); @include font-size(16px);
&.like { .value {
border-right: 4px solid $success; &.historic {
border-left: 4px solid $success; opacity: 0.2;
} }
&.like {
background-color: $success;
color: $white;
}
&.dislike { &.dislike {
border-right: 4px solid $danger; background-color: $danger;
border-left: 4px solid $danger; color: $white;
}
} }
} }
......
import classNames from "classnames"; import classNames from "classnames";
import { inject, observer } from "mobx-react"; import { inject, observer } from "mobx-react";
import * as React from 'react'; import React, { ReactNode } from "react";
import "./Conflict.scss"; import "./Conflict.scss";
import { FPCProps } from './FPCProps'; import { FPCProps } from "./FPCProps";
import { Opinion } from "../../models/opinion";
@inject("fpcStore") @inject("fpcStore")
@observer @observer
export default class Conflict extends React.Component<FPCProps, any> { export default class Conflict extends React.Component<FPCProps, unknown> {
componentDidMount() { public componentDidMount(): void {
this.props.fpcStore.updateCurrentConflict(this.props.match.params.id); this.props.fpcStore.updateCurrentConflict(this.props.match.params.id);
} }
render() { public render(): ReactNode {
let { nodeConflictGrid } = this.props.fpcStore; const { nodeConflictGrid } = this.props.fpcStore;
return ( return (
<div className="conflict"> <div className="conflict">
...@@ -26,27 +27,41 @@ export default class Conflict extends React.Component<FPCProps, any> { ...@@ -26,27 +27,41 @@ export default class Conflict extends React.Component<FPCProps, any> {
</div> </div>
</div> </div>
<div className="node-grid"> <div className="node-grid">
{!nodeConflictGrid && ( {!nodeConflictGrid &&
<div className="card"> <div className="card">
<p>The node data for this conflict is no longer available.</p> <p>The node data for this conflict is no longer available.</p>
</div> </div>
)} }
{nodeConflictGrid && nodeConflictGrid.map(nodeDetails => ( {nodeConflictGrid && Object.keys(nodeConflictGrid).map(nodeID =>
<div <div
key={nodeDetails.nodeID} key={nodeID}
className={classNames( className={classNames(
"card", "card",
"node-details", "node-details"
{ like: nodeDetails.opinion === 1 },
{ dislike: nodeDetails.opinion === 2 }
)} )}
> >
<div className="details row middle"> <div className="details row middle margin-b-s">
<label>Node ID</label> <label>Node ID</label>
<span className="value">{nodeDetails.nodeID}</span> <span className="value">{nodeID}</span>
</div>
<div className="details row middle">
<label>Opinions</label>
{nodeConflictGrid[nodeID].reverse().map((opinion, idx) =>
<span key={idx} className={
classNames(
"value",
"margin-r-t",
{ "like": opinion === Opinion.like },
{ "dislike": opinion === Opinion.dislike },
{ "historic": idx !== 0 }
)
}>
{opinion === Opinion.like ? "Like" : "Dislike"}
</span>
)}
</div> </div>
</div> </div>
))} )}
</div> </div>
</div> </div>
); );
......
import { inject, observer } from "mobx-react"; import { inject, observer } from "mobx-react";
import * as React from 'react'; import React, { ReactNode } from "react";
import { CSSTransition, TransitionGroup } from 'react-transition-group';
import { CSSTransition, TransitionGroup } from "react-transition-group";
import "./FPC.scss"; import "./FPC.scss";
import FPCItem from "./FPCItem"; import FPCItem from "./FPCItem";
import { FPCProps } from './FPCProps'; import { FPCProps } from "./FPCProps";
@inject("fpcStore") @inject("fpcStore")
@observer @observer
export default class FPC extends React.Component<FPCProps, any> { export default class FPC extends React.Component<FPCProps, unknown> {
componentDidMount(): void { public componentDidMount(): void {
this.props.fpcStore.start(); this.props.fpcStore.start();
} }
componentWillUnmount(): void { public componentWillUnmount(): void {
this.props.fpcStore.stop(); this.props.fpcStore.stop();
} }
render() { public render(): ReactNode {
let { conflictGrid } = this.props.fpcStore; const { conflictGrid } = this.props.fpcStore;
return ( return (
<div className="fpc"> <div className="fpc">
<div className="header margin-b-m"> <div className="header margin-b-m">
<h2>Conflicts Overview</h2> <h2>Conflicts Overview</h2>
</div> </div>
<div className="conflict-grid"> <div className="conflict-grid">
{conflictGrid.length === 0 && ( {conflictGrid.length === 0 &&
<p>There are no conflicts to show.</p> <p>There are no conflicts to show.</p>
)} }
<TransitionGroup> <TransitionGroup>
{conflictGrid.map(conflict => ( {conflictGrid.map(conflict =>
<CSSTransition <CSSTransition
className="fpc-item" className="fpc-item"
key={conflict.conflictID} key={conflict.conflictID}
...@@ -38,7 +39,7 @@ export default class FPC extends React.Component<FPCProps, any> { ...@@ -38,7 +39,7 @@ export default class FPC extends React.Component<FPCProps, any> {
{...conflict} {...conflict}
/> />
</CSSTransition> </CSSTransition>
))} )}
</TransitionGroup> </TransitionGroup>
</div> </div>
</div> </div>
......
import { shortenedIDCharCount } from "app/stores/AutopeeringStore"; import { shortenedIDCharCount } from "../../stores/AutopeeringStore";
import { inject, observer } from "mobx-react"; import { inject, observer } from "mobx-react";
import * as React from 'react'; import React, { ReactNode } from "react";
import { Link } from 'react-router-dom';
import { Link } from "react-router-dom";
import "./FPCItem.scss"; import "./FPCItem.scss";
import { FPCItemProps } from './FPCItemProps'; import { FPCItemProps } from "./FPCItemProps";
@inject("fpcStore") @inject("fpcStore")
@observer @observer
export default class FPCItem extends React.Component<FPCItemProps, any> { export default class FPCItem extends React.Component<FPCItemProps, unknown> {
render() { public render(): ReactNode {
const total = Object.keys(this.props.nodeOpinions).length; const total = Object.keys(this.props.nodeOpinions).length;
const likes = this.props.likes ?? 0;
return ( return (
<Link <Link
to={`/consensus/conflict/${this.props.conflictID}`} to={`/consensus/conflict/${this.props.conflictID}`}
...@@ -18,11 +21,11 @@ export default class FPCItem extends React.Component<FPCItemProps, any> { ...@@ -18,11 +21,11 @@ export default class FPCItem extends React.Component<FPCItemProps, any> {
<div <div
className="percentage" className="percentage"
style={{ style={{
width: `${Math.floor((this.props.likes / total) * 200)}px` width: `${Math.floor(likes / total * 200)}px`
}} }}
/> />
<div className="label"> <div className="label">
{`${this.props.conflictID.substr(0, shortenedIDCharCount)}: ${this.props.likes} / ${total}`} {`${this.props.conflictID.substr(0, shortenedIDCharCount)}: ${likes} / ${total}`}
</div> </div>
</Link> </Link>
); );
......
import FPCStore from "app/stores/FPCStore"; import FPCStore from "../../stores/FPCStore";
export interface FPCItemProps { export interface FPCItemProps {
fpcStore?: FPCStore; fpcStore?: FPCStore;
conflictID: string; conflictID: string;
likes?: number; likes?: number;
nodeOpinions: { nodeID: string; opinion: number }[]; nodeOpinions: { [id: string]: number[] };
} }
import FPCStore from "app/stores/FPCStore"; import FPCStore from "../../stores/FPCStore";
import { RouteComponentProps } from "react-router"; import { RouteComponentProps } from "react-router";
export interface FPCProps extends RouteComponentProps<{ export interface FPCProps extends RouteComponentProps<{
id?: string; id: string;
}> { }> {
fpcStore?: FPCStore; fpcStore: FPCStore;
} }
export enum WSMsgType {
Ping,
FPC,
AddNode,
RemoveNode,
ConnectNodes,
DisconnectNodes,
}
export interface WSMessage {
type: number;
data: any;
}
type DataHandler = (data: any) => void;
let handlers = {};
export function registerHandler(msgTypeID: number, handler: DataHandler) {
handlers[msgTypeID] = handler;
}
export function unregisterHandler(msgTypeID: number) {
delete handlers[msgTypeID];
}
export function connectWebSocket(path: string, onOpen, onClose, onError) {
let loc = window.location;
let uri = 'ws:';
if (loc.protocol === 'https:') {
uri = 'wss:';
}
uri += '//' + loc.host + path;
let ws = new WebSocket(uri);
ws.onopen = onOpen;
ws.onclose = onClose;
ws.onerror = onError;
ws.onmessage = (e) => {
let msg: WSMessage = JSON.parse(e.data);
// Just a ping, do nothing
if (msg.type == WSMsgType.Ping) {
return;
}
let handler = handlers[msg.type];
if (!handler) {
return;
}
handler(msg.data);
};
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment