dashboard, cmd: minor UI fix, include commit hash

This commit is contained in:
Kurkó Mihály 2018-01-11 23:15:03 +02:00
parent 011ab3665a
commit 24880501bb
9 changed files with 93 additions and 145 deletions

View file

@ -158,7 +158,7 @@ func makeFullNode(ctx *cli.Context) *node.Node {
utils.RegisterEthService(stack, &cfg.Eth) utils.RegisterEthService(stack, &cfg.Eth)
if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) { if ctx.GlobalBool(utils.DashboardEnabledFlag.Name) {
utils.RegisterDashboardService(stack, &cfg.Dashboard) utils.RegisterDashboardService(stack, &cfg.Dashboard, gitCommit)
} }
// Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode // Whisper must be explicitly enabled by specifying at least 1 whisper flag or in dev mode
shhEnabled := enableWhisper(ctx) shhEnabled := enableWhisper(ctx)

View file

@ -1103,9 +1103,9 @@ func RegisterEthService(stack *node.Node, cfg *eth.Config) {
} }
// RegisterDashboardService adds a dashboard to the stack. // RegisterDashboardService adds a dashboard to the stack.
func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config) { func RegisterDashboardService(stack *node.Node, cfg *dashboard.Config, commit string) {
stack.Register(func(ctx *node.ServiceContext) (node.Service, error) { stack.Register(func(ctx *node.ServiceContext) (node.Service, error) {
return dashboard.New(cfg) return dashboard.New(cfg, commit)
}) })
} }

View file

@ -26819,37 +26819,28 @@ var _bundleJs = []byte((((((((((`!function(modules) {
return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps), return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps),
Constructor; Constructor;
}; };
}(), _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function(obj) { }(), _react = __webpack_require__(0), _react2 = _interopRequireDefault(_react), _withStyles = __webpack_require__(10), _withStyles2 = _interopRequireDefault(_withStyles), _Header = __webpack_require__(459), _Header2 = _interopRequireDefault(_Header), _Body = __webpack_require__(489), _Body2 = _interopRequireDefault(_Body), _Footer = __webpack_require__(805), _Footer2 = _interopRequireDefault(_Footer), _Common = __webpack_require__(77), deepUpdate = function deepUpdate(prev, update, updater) {
return typeof obj; if (void 0 === update) return prev;
} : function(obj) { if ("function" == typeof updater) return updater(prev, update);
return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; var updated = {};
}, _react = __webpack_require__(0), _react2 = _interopRequireDefault(_react), _withStyles = __webpack_require__(10), _withStyles2 = _interopRequireDefault(_withStyles), _Header = __webpack_require__(459), _Header2 = _interopRequireDefault(_Header), _Body = __webpack_require__(489), _Body2 = _interopRequireDefault(_Body), _Footer = __webpack_require__(805), _Footer2 = _interopRequireDefault(_Footer), _Common = __webpack_require__(77), deepCopy = function deepCopy(prev) {
var copied = Array.isArray(prev) ? [] : {};
return Object.keys(prev).forEach(function(key) { return Object.keys(prev).forEach(function(key) {
var c = prev[key]; updated[key] = deepUpdate(prev[key], update[key], updater[key]);
copied[key] = "object" === (void 0 === c ? "undefined" : _typeof(c)) ? deepCopy(c) : c;
}), copied;
}, deepUpdate = function deepUpdate(prev, update, handler) {
if (void 0 === update) return deepCopy(prev);
if ("function" == typeof handler) return handler(prev, update);
var updated = Array.isArray(prev) ? [] : {};
return Object.keys(prev).forEach(function(key) {
void 0 === update[key] ? updated[key] = deepCopy(prev[key]) : updated[key] = deepUpdate(prev[key], update[key], handler[key]);
}), updated; }), updated;
}, shouldUpdate = function shouldUpdate(msg, handler) { }, shouldUpdate = function shouldUpdate(msg, updater) {
var su = {}; var su = {};
return Object.keys(msg).forEach(function(key) { return Object.keys(msg).forEach(function(key) {
su[key] = "object" !== _typeof(msg[key]) || "function" == typeof handler[key] || shouldUpdate(msg[key], handler[key]); su[key] = "function" == typeof updater[key] || shouldUpdate(msg[key], updater[key]);
}), su; }), su;
}, appender200 = function(limit) { }, appender = function(limit) {
return function(prev, update) { return function(prev, update) {
return [].concat(_toConsumableArray(prev), _toConsumableArray(update)).slice(-limit); return [].concat(_toConsumableArray(prev), _toConsumableArray(update)).slice(-limit);
}; };
}(200), replacer = function(prev, update) { }, replacer = function(prev, update) {
return update; return update;
}, defaultContent = { }, defaultContent = {
general: { general: {
version: "-" version: "-",
gitCommit: "-"
}, },
home: { home: {
memory: [], memory: [],
@ -26862,20 +26853,21 @@ var _bundleJs = []byte((((((((((`!function(modules) {
logs: { logs: {
log: [] log: []
} }
}, handlers = { }, updaters = {
general: { general: {
version: replacer version: replacer,
gitCommit: replacer
}, },
home: { home: {
memory: appender200, memory: appender(200),
traffic: appender200 traffic: appender(200)
}, },
chain: null, chain: null,
txpool: null, txpool: null,
network: null, network: null,
system: null, system: null,
logs: { logs: {
log: appender200 log: appender(200)
} }
}, styles = function(theme) { }, styles = function(theme) {
return { return {
@ -26902,15 +26894,16 @@ var _bundleJs = []byte((((((((((`!function(modules) {
}); });
}, server.onmessage = function(event) { }, server.onmessage = function(event) {
var msg = JSON.parse(event.data); var msg = JSON.parse(event.data);
msg && _this.update(msg); if (!msg) return void console.error("Incoming message is " + msg);
console.log(msg), _this.update(msg);
}, server.onclose = function() { }, server.onclose = function() {
setTimeout(_this.reconnect, 3e3); setTimeout(_this.reconnect, 3e3);
}; };
}, _this.update = function(msg) { }, _this.update = function(msg) {
_this.setState(function(prevState) { _this.setState(function(prevState) {
return { return {
content: deepUpdate(prevState.content, msg, handlers), content: deepUpdate(prevState.content, msg, updaters),
shouldUpdate: shouldUpdate(msg, handlers) shouldUpdate: shouldUpdate(msg, updaters)
}; };
}); });
}, _this.changeContent = function(newActive) { }, _this.changeContent = function(newActive) {
@ -38146,13 +38139,7 @@ var _bundleJs = []byte((((((((((`!function(modules) {
Object.defineProperty(exports, "__esModule", { Object.defineProperty(exports, "__esModule", {
value: !0 value: !0
}); });
var _extends = Object.assign || function(target) { var _createClass = function() {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) Object.prototype.hasOwnProperty.call(source, key) && (target[key] = source[key]);
}
return target;
}, _createClass = function() {
function defineProperties(target, props) { function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) { for (var i = 0; i < props.length; i++) {
var descriptor = props[i]; var descriptor = props[i];
@ -38164,13 +38151,9 @@ var _bundleJs = []byte((((((((((`!function(modules) {
return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps), return protoProps && defineProperties(Constructor.prototype, protoProps), staticProps && defineProperties(Constructor, staticProps),
Constructor; Constructor;
}; };
}(), _react = __webpack_require__(0), _react2 = _interopRequireDefault(_react), _withStyles = __webpack_require__(10), _withStyles2 = _interopRequireDefault(_withStyles), _AppBar = __webpack_require__(235), _AppBar2 = _interopRequireDefault(_AppBar), _Toolbar = __webpack_require__(236), _Toolbar2 = _interopRequireDefault(_Toolbar), _Transition = __webpack_require__(76), _Transition2 = _interopRequireDefault(_Transition), _IconButton = __webpack_require__(237), _IconButton2 = _interopRequireDefault(_IconButton), _Typography = __webpack_require__(158), _Typography2 = _interopRequireDefault(_Typography), _ChevronLeft = __webpack_require__(244), _ChevronLeft2 = _interopRequireDefault(_ChevronLeft), _Common = __webpack_require__(77), arrowDefault = { }(), _react = __webpack_require__(0), _react2 = _interopRequireDefault(_react), _withStyles = __webpack_require__(10), _withStyles2 = _interopRequireDefault(_withStyles), _AppBar = __webpack_require__(235), _AppBar2 = _interopRequireDefault(_AppBar), _Toolbar = __webpack_require__(236), _Toolbar2 = _interopRequireDefault(_Toolbar), _Transition = __webpack_require__(76), _IconButton = (_interopRequireDefault(_Transition),
transition: "transform " + _Common.DURATION + "ms" __webpack_require__(237)), _Typography = (_interopRequireDefault(_IconButton), __webpack_require__(158)), _Typography2 = _interopRequireDefault(_Typography), _ChevronLeft = __webpack_require__(244), _Common = (_interopRequireDefault(_ChevronLeft),
}, arrowTransition = { __webpack_require__(77)), styles = (_Common.DURATION, function(theme) {
entered: {
transform: "rotate(180deg)"
}
}, styles = function(theme) {
return { return {
header: { header: {
backgroundColor: theme.palette.background.appBar, backgroundColor: theme.palette.background.appBar,
@ -38185,21 +38168,9 @@ var _bundleJs = []byte((((((((((`!function(modules) {
paddingLeft: theme.spacing.unit paddingLeft: theme.spacing.unit
} }
}; };
}, Footer = function(_Component) { }), Footer = function(_Component) {
function Footer() { function Footer() {
var _ref, _temp, _this, _ret; return _classCallCheck(this, Footer), _possibleConstructorReturn(this, (Footer.__proto__ || Object.getPrototypeOf(Footer)).apply(this, arguments));
_classCallCheck(this, Footer);
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) args[_key] = arguments[_key];
return _temp = _this = _possibleConstructorReturn(this, (_ref = Footer.__proto__ || Object.getPrototypeOf(Footer)).call.apply(_ref, [ this ].concat(args))),
_this.changeSideBar = function() {
_this.props.opened ? _this.props.closeSideBar() : _this.props.openSideBar();
}, _this.arrowButton = function(transitionState) {
return _react2.default.createElement(_IconButton2.default, {
onClick: _this.changeSideBar
}, _react2.default.createElement(_ChevronLeft2.default, {
style: _extends({}, arrowDefault, arrowTransition[transitionState])
}));
}, _ret = _temp, _possibleConstructorReturn(_this, _ret);
} }
return _inherits(Footer, _Component), _createClass(Footer, [ { return _inherits(Footer, _Component), _createClass(Footer, [ {
key: "shouldComponentUpdate", key: "shouldComponentUpdate",
@ -38209,24 +38180,21 @@ var _bundleJs = []byte((((((((((`!function(modules) {
}, { }, {
key: "render", key: "render",
value: function() { value: function() {
var _props = this.props, classes = _props.classes, opened = _props.opened; var classes = this.props.classes;
return _react2.default.createElement(_AppBar2.default, { return _react2.default.createElement(_AppBar2.default, {
position: "static", position: "static",
className: classes.header className: classes.header
}, _react2.default.createElement(_Toolbar2.default, { }, _react2.default.createElement(_Toolbar2.default, {
className: classes.toolbar className: classes.toolbar
}, _react2.default.createElement(_Transition2.default, { }, _react2.default.createElement(_Typography2.default, {
mountOnEnter: !0,
in: opened,
timeout: {
enter: _Common.DURATION
}
}, this.arrowButton), _react2.default.createElement(_Typography2.default, {
type: "title", type: "title",
color: "inherit", color: "inherit",
noWrap: !0,
className: classes.mainText className: classes.mainText
}, this.props.general.version))); }, this.props.general.version), _react2.default.createElement(_Typography2.default, {
type: "title",
color: "inherit",
className: classes.mainText
}, this.props.general.gitCommit)));
} }
} ]), Footer; } ]), Footer;
}(_react.Component); }(_react.Component);

View file

@ -26,22 +26,8 @@ import Footer from './Footer';
import {MENU} from './Common'; import {MENU} from './Common';
import type {Content} from '../types/content'; import type {Content} from '../types/content';
// deepCopy retrieves a copy of the given object, copying recursively in all depth.
// It is used at the state update, because React doesn't handle the object nesting well.
// References are prohibited, since the state needs to be immutable.
// NOTE: maybe there is better solution.
const deepCopy = (prev: mixed) => {
const copied = Array.isArray(prev) ? [] : {};
Object.keys(prev).forEach((key) => {
const c: mixed = prev[key]
copied[key] = typeof c === 'object' ? deepCopy(c) : c;
});
return copied;
};
// deepUpdate updates an object corresponding to the given update data, which has // deepUpdate updates an object corresponding to the given update data, which has
// the shape of the same structure as the original object. handler also has the same // the shape of the same structure as the original object. updater also has the same
// structure, except that it contains functions where the original data needs to be // structure, except that it contains functions where the original data needs to be
// updated. These functions are used to handle the update. // updated. These functions are used to handle the update.
// //
@ -49,41 +35,41 @@ const deepCopy = (prev: mixed) => {
// the generalization of the message handling. The only necessary thing is to set a // the generalization of the message handling. The only necessary thing is to set a
// handler function for every path of the state in order to maximize the flexibility // handler function for every path of the state in order to maximize the flexibility
// of the update. // of the update.
const deepUpdate = (prev: mixed, update: mixed, handler: mixed) => { const deepUpdate = (prev: Object, update: Object, updater: Object) => {
if (typeof update === 'undefined') { if (typeof update === 'undefined') {
return deepCopy(prev); // TODO (kurkomisi): originally this was deep copy, investigate it.
return prev;
} }
if (typeof handler === 'function') { if (typeof updater === 'function') {
return handler(prev, update); return updater(prev, update);
} }
const updated = Array.isArray(prev) ? [] : {}; const updated = {};
Object.keys(prev).forEach((key) => { Object.keys(prev).forEach((key) => {
if (typeof update[key] === 'undefined') { updated[key] = deepUpdate(prev[key], update[key], updater[key]);
updated[key] = deepCopy(prev[key]);
} else {
updated[key] = deepUpdate(prev[key], update[key], handler[key]);
}
}); });
return updated; return updated;
}; };
// shouldUpdate retrieves the structure of a message. It is used to prevent unnecessary render // shouldUpdate returns the structure of a message. It is used to prevent unnecessary render
// method triggerings. In the affected component's shouldComponentUpdate method it can be checked // method triggerings. In the affected component's shouldComponentUpdate method it can be checked
// whether the involved data was changed or not by checking the message structure. // whether the involved data was changed or not by checking the message structure.
const shouldUpdate = (msg: mixed, handler: mixed) => { //
// We could return the message itself too, but it's safer not to give access to it.
const shouldUpdate = (msg: Object, updater: Object) => {
const su = {}; const su = {};
console.log(msg);
Object.keys(msg).forEach((key) => { Object.keys(msg).forEach((key) => {
su[key] = typeof msg[key] === 'object' && typeof handler[key] !== 'function' ? shouldUpdate(msg[key], handler[key]) : true; su[key] = typeof updater[key] !== 'function' ? shouldUpdate(msg[key], updater[key]) : true;
}); });
return su; return su;
}; };
// appender is a state update generalization function, which appends the update data // appender is a state update generalization function, which appends the update data
// to the existing data. limit defines the maximum allowed size of the created array. // to the existing data. limit defines the maximum allowed size of the created array.
const appender = <T>(limit: number) => (prev: Array<T>, update: Array<T>) => [...prev, ...update].slice(-limit); const appender = <T>(limit: number) => (prev: Array<T>, update: Array<T>) => [...prev, ...update].slice(-limit);
// appender200 is an appender function with limit 200.
const appender200 = appender(200);
// replacer is a state update generalization function, which replaces the original data. // replacer is a state update generalization function, which replaces the original data.
const replacer = <T>(prev: T, update: T) => update; const replacer = <T>(prev: T, update: T) => update;
@ -91,6 +77,7 @@ const replacer = <T>(prev: T, update: T) => update;
const defaultContent: Content = { const defaultContent: Content = {
general: { general: {
version: '-', version: '-',
gitCommit: '-',
}, },
home: { home: {
memory: [], memory: [],
@ -104,25 +91,28 @@ const defaultContent: Content = {
log: [], log: [],
}, },
}; };
// handlers contains the state update generalization functions for each path of the state.
// updaters contains the state update generalization functions for each path of the state.
// TODO (kurkomisi): Define a tricky type which embraces the content and the handlers. // TODO (kurkomisi): Define a tricky type which embraces the content and the handlers.
const handlers = { const updaters = {
general: { general: {
version: replacer, version: replacer,
gitCommit: replacer,
}, },
home: { home: {
memory: appender200, memory: appender(200),
traffic: appender200, traffic: appender(200),
}, },
chain: null, chain: null,
txpool: null, txpool: null,
network: null, network: null,
system: null, system: null,
logs: { logs: {
log: appender200, log: appender(200),
}, },
}; };
// styles retrieves the styles for the Dashboard component.
// styles returns the styles for the Dashboard component.
const styles = theme => ({ const styles = theme => ({
dashboard: { dashboard: {
display: 'flex', display: 'flex',
@ -134,15 +124,18 @@ const styles = theme => ({
overflow: 'hidden', overflow: 'hidden',
}, },
}); });
export type Props = { export type Props = {
classes: Object, classes: Object,
}; };
type State = { type State = {
active: string, // active menu active: string, // active menu
sideBar: boolean, // true if the sidebar is opened sideBar: boolean, // true if the sidebar is opened
content: Content, // the visualized data content: Content, // the visualized data
shouldUpdate: Object // labels for the components, which need to rerender based on the incoming message shouldUpdate: Object // labels for the components, which need to rerender based on the incoming message
}; };
// Dashboard is the main component, which renders the whole page, makes connection with the server and // Dashboard is the main component, which renders the whole page, makes connection with the server and
// listens for messages. When there is an incoming message, updates the page's content correspondingly. // listens for messages. When there is an incoming message, updates the page's content correspondingly.
class Dashboard extends Component<Props, State> { class Dashboard extends Component<Props, State> {
@ -171,6 +164,7 @@ class Dashboard extends Component<Props, State> {
server.onmessage = (event) => { server.onmessage = (event) => {
const msg: $Shape<Content> = JSON.parse(event.data); const msg: $Shape<Content> = JSON.parse(event.data);
if (!msg) { if (!msg) {
console.error(`Incoming message is ${msg}`);
return; return;
} }
this.update(msg); this.update(msg);
@ -183,8 +177,8 @@ class Dashboard extends Component<Props, State> {
// update updates the content corresponding to the incoming message. // update updates the content corresponding to the incoming message.
update = (msg: $Shape<Content>) => { update = (msg: $Shape<Content>) => {
this.setState(prevState => ({ this.setState(prevState => ({
content: deepUpdate(prevState.content, msg, handlers), content: deepUpdate(prevState.content, msg, updaters),
shouldUpdate: shouldUpdate(msg, handlers), shouldUpdate: shouldUpdate(msg, updaters),
})); }));
}; };

View file

@ -53,9 +53,6 @@ const styles = theme => ({
}); });
export type Props = { export type Props = {
classes: Object, classes: Object,
opened: boolean,
openSideBar: () => {},
closeSideBar: () => {},
}; };
// TODO (kurkomisi): If the structure is appropriate, make an abstraction of the common parts with the Header. // TODO (kurkomisi): If the structure is appropriate, make an abstraction of the common parts with the Header.
// Footer renders the header of the dashboard. // Footer renders the header of the dashboard.
@ -64,39 +61,18 @@ class Footer extends Component<Props> {
return typeof nextProps.shouldUpdate.logs !== 'undefined'; return typeof nextProps.shouldUpdate.logs !== 'undefined';
} }
// changeSideBar opens or closes the sidebar corresponding to the previous state.
changeSideBar = () => {
if (this.props.opened) {
this.props.closeSideBar();
} else {
this.props.openSideBar();
}
};
// arrowButton is connected to the sidebar; changes its state.
arrowButton = (transitionState: string) => (
<IconButton onClick={this.changeSideBar}>
<ChevronLeftIcon
style={{
...arrowDefault,
...arrowTransition[transitionState],
}}
/>
</IconButton>
);
render() { render() {
const {classes, opened} = this.props; // The classes property is injected by withStyles(). const {classes} = this.props; // The classes property is injected by withStyles().
return ( return (
<AppBar position="static" className={classes.header}> <AppBar position="static" className={classes.header}>
<Toolbar className={classes.toolbar}> <Toolbar className={classes.toolbar}>
<Transition mountOnEnter in={opened} timeout={{enter: DURATION}}> <Typography type="title" color="inherit" className={classes.mainText}>
{this.arrowButton}
</Transition>
<Typography type="title" color="inherit" noWrap className={classes.mainText}>
{this.props.general.version} {this.props.general.version}
</Typography> </Typography>
<Typography type="title" color="inherit" className={classes.mainText}>
{this.props.general.gitCommit}
</Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
); );

View file

@ -28,6 +28,7 @@ export type Content = {
export type General = { export type General = {
version: string, version: string,
gitCommit: string,
}; };
export type Home = { export type Home = {

View file

@ -55,6 +55,7 @@ type Dashboard struct {
listener net.Listener listener net.Listener
conns map[uint32]*client // Currently live websocket connections conns map[uint32]*client // Currently live websocket connections
charts *HomeMessage charts *HomeMessage
gitCommit string
lock sync.RWMutex // Lock protecting the dashboard's internals lock sync.RWMutex // Lock protecting the dashboard's internals
quit chan chan error // Channel used for graceful exit quit chan chan error // Channel used for graceful exit
@ -69,7 +70,7 @@ type client struct {
} }
// New creates a new dashboard instance with the given configuration. // New creates a new dashboard instance with the given configuration.
func New(config *Config) (*Dashboard, error) { func New(config *Config, gitCommit string) (*Dashboard, error) {
return &Dashboard{ return &Dashboard{
conns: make(map[uint32]*client), conns: make(map[uint32]*client),
config: config, config: config,
@ -78,6 +79,7 @@ func New(config *Config) (*Dashboard, error) {
Memory: ChartEntries{}, Memory: ChartEntries{},
Traffic: ChartEntries{}, Traffic: ChartEntries{},
}, },
gitCommit: gitCommit,
}, nil }, nil
} }
@ -201,10 +203,16 @@ func (db *Dashboard) apiHandler(conn *websocket.Conn) {
} }
} }
}() }()
versionMeta := ""
if len(params.VersionMeta) > 0 {
versionMeta = fmt.Sprintf(" (%s)", params.VersionMeta)
}
// Send the past data. // Send the past data.
client.msg <- Message{ client.msg <- Message{
General: &GeneralMessage{ General: &GeneralMessage{
Version: params.Version, Version: fmt.Sprintf("Geth v%d.%d.%d%s", params.VersionMajor, params.VersionMinor, params.VersionPatch, versionMeta),
GitCommit: fmt.Sprintf("Commit %s", db.gitCommit),
}, },
Home: &HomeMessage{ Home: &HomeMessage{
Memory: db.charts.Memory, Memory: db.charts.Memory,

View file

@ -30,6 +30,7 @@ type Message struct {
type GeneralMessage struct { type GeneralMessage struct {
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
GitCommit string `json:"gitCommit,omitempty"`
} }
type HomeMessage struct { type HomeMessage struct {

BIN
geth Executable file

Binary file not shown.