mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-12 14:41:15 +00:00
Filter nodes by name when typing (#53)
* Allow filtering of nodes by name * Tidy up
This commit is contained in:
@@ -65,6 +65,13 @@
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.Chain-no-nodes {
|
||||
font-size: 30px;
|
||||
padding-top: 20vh;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.Chain-node-list {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import * as React from 'react';
|
||||
import { Types, Maybe } from '@dotstats/common';
|
||||
import { State as AppState } from '../../state';
|
||||
import { formatNumber, secondsWithPrecision, viewport } from '../../utils';
|
||||
import { Tab } from './';
|
||||
import { Tab, Filter } from './';
|
||||
import { Tile, Node, Ago, Setting } from '../';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { PersistentObject, PersistentSet } from '../../persist';
|
||||
|
||||
import blockIcon from '../../icons/package.svg';
|
||||
import blockTimeIcon from '../../icons/history.svg';
|
||||
import lastTimeIcon from '../../icons/watch.svg';
|
||||
import listIcon from '../../icons/list-alt-regular.svg';
|
||||
import worldIcon from '../../icons/map-pin-solid.svg';
|
||||
import worldIcon from '../../icons/location.svg';
|
||||
import settingsIcon from '../../icons/settings.svg';
|
||||
|
||||
const MAP_RATIO = 800 / 350;
|
||||
const MAP_HEIGHT_ADJUST = 400 / 350;
|
||||
const HEADER = 148;
|
||||
|
||||
const ESCAPE_KEY = 27;
|
||||
const BACKSPACE_KEY = 8;
|
||||
|
||||
import './Chain.css';
|
||||
|
||||
export namespace Chain {
|
||||
@@ -30,6 +33,7 @@ export namespace Chain {
|
||||
|
||||
export interface State {
|
||||
display: Display;
|
||||
filter: Maybe<string>;
|
||||
map: {
|
||||
width: number;
|
||||
height: number;
|
||||
@@ -73,6 +77,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
|
||||
this.state = {
|
||||
display,
|
||||
filter: null,
|
||||
map: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
@@ -86,10 +91,12 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
this.calculateMapDimensions();
|
||||
|
||||
window.addEventListener('resize', this.calculateMapDimensions);
|
||||
window.addEventListener('keyup', this.onKeyPress);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.calculateMapDimensions);
|
||||
window.removeEventListener('keyup', this.onKeyPress);
|
||||
}
|
||||
|
||||
public render() {
|
||||
@@ -130,42 +137,63 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
private renderList() {
|
||||
const { settings } = this.props.appState;
|
||||
const { pins } = this.props;
|
||||
const { filter } = this.state;
|
||||
const nodeFilter = this.getNodeFilter();
|
||||
const nodes = nodeFilter ? this.nodes().filter(nodeFilter) : this.nodes();
|
||||
|
||||
if (nodeFilter && nodes.length === 0) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{filter != null ? <Filter value={filter} onChange={this.onFilterChange} /> : null}
|
||||
<div className="Chain-no-nodes">¯\_(ツ)_/¯<br />Nothing matches</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="Chain-node-list">
|
||||
<Node.Row.Header settings={settings} />
|
||||
<tbody>
|
||||
{
|
||||
this
|
||||
.nodes()
|
||||
.sort(sortNodes)
|
||||
.map((node) => <Node.Row key={node.id} node={node} settings={settings} pins={pins} />)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<React.Fragment>
|
||||
{filter != null ? <Filter value={filter} onChange={this.onFilterChange} /> : null}
|
||||
<table className="Chain-node-list">
|
||||
<Node.Row.Header settings={settings} />
|
||||
<tbody>
|
||||
{
|
||||
nodes
|
||||
.sort(sortNodes)
|
||||
.map((node) => <Node.Row key={node.id} node={node} settings={settings} pins={pins} />)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMap() {
|
||||
const { filter } = this.state;
|
||||
const nodeFilter = this.getNodeFilter();
|
||||
|
||||
return (
|
||||
<div className="Chain-map">
|
||||
{
|
||||
this.nodes().map((node) => {
|
||||
const location = node.location;
|
||||
<React.Fragment>
|
||||
{filter != null ? <Filter value={filter} onChange={this.onFilterChange} /> : null}
|
||||
<div className="Chain-map">
|
||||
{
|
||||
this.nodes().map((node) => {
|
||||
const location = node.location;
|
||||
const focused = nodeFilter == null || nodeFilter(node);
|
||||
|
||||
if (!location || location[0] == null || location[1] == null) {
|
||||
// Skip nodes with unknown location
|
||||
return null;
|
||||
}
|
||||
if (!location || location[0] == null || location[1] == null) {
|
||||
// Skip nodes with unknown location
|
||||
return null;
|
||||
}
|
||||
|
||||
const { left, top, quarter } = this.pixelPosition(location[0], location[1]);
|
||||
const position = this.pixelPosition(location[0], location[1]);
|
||||
|
||||
return (
|
||||
<Node.Location key={node.id} left={left} top={top} quarter={quarter} {...node} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
return (
|
||||
<Node.Location key={node.id} position={position} focused={focused} node={node} />
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -191,7 +219,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
);
|
||||
}
|
||||
|
||||
private nodes() {
|
||||
private nodes(): AppState.Node[] {
|
||||
return Array.from(this.props.appState.nodes.values());
|
||||
}
|
||||
|
||||
@@ -241,4 +269,36 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
|
||||
this.setState({ map: { top, left, width, height }});
|
||||
}
|
||||
|
||||
private onKeyPress = (event: KeyboardEvent) => {
|
||||
const { filter } = this.state;
|
||||
const key = event.key;
|
||||
const code = event.keyCode;
|
||||
|
||||
const escape = filter != null && code === ESCAPE_KEY;
|
||||
const backspace = filter === '' && code === BACKSPACE_KEY;
|
||||
const singleChar = filter == null && key.length === 1;
|
||||
|
||||
if (escape || backspace) {
|
||||
this.setState({ filter: null });
|
||||
} else if (singleChar) {
|
||||
this.setState({ filter: key });
|
||||
}
|
||||
}
|
||||
|
||||
private onFilterChange = (filter: string) => {
|
||||
this.setState({ filter: filter.toLowerCase() });
|
||||
}
|
||||
|
||||
private getNodeFilter(): Maybe<(node: AppState.Node) => boolean> {
|
||||
const { filter } = this.state;
|
||||
|
||||
if (filter == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filterLC = filter.toLowerCase();
|
||||
|
||||
return ({ nodeDetails }) => nodeDetails[0].indexOf(filterLC) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
.Chain-Filter {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
width: 400px;
|
||||
font-size: 30px;
|
||||
margin-left: -210px;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
.Chain-Filter input {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 400px;
|
||||
font-size: 30px;
|
||||
background: #111;
|
||||
color: #fff;
|
||||
font-family: Roboto, Helvetica, Arial, sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import './Filter.css';
|
||||
|
||||
export namespace Filter {
|
||||
export interface Props {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
}
|
||||
|
||||
export class Filter extends React.Component<Filter.Props, {}> {
|
||||
private filterInput: HTMLInputElement;
|
||||
|
||||
public componentDidMount(){
|
||||
this.filterInput.focus();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { value } = this.props;
|
||||
|
||||
return (
|
||||
<div className="Chain-Filter">
|
||||
<input ref={this.onRef} value={value} onChange={this.onChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private onRef = (el: HTMLInputElement) => {
|
||||
this.filterInput = el;
|
||||
}
|
||||
|
||||
private onChange = () => {
|
||||
const { value } = this.filterInput;
|
||||
|
||||
this.props.onChange(value);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './Chain';
|
||||
export * from './Tab';
|
||||
export * from './Filter';
|
||||
|
||||
@@ -11,10 +11,20 @@
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
transition: border-color 0.25s linear;
|
||||
}
|
||||
|
||||
.Node-Location-dimmed {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
margin-left: -1px;
|
||||
margin-top: -1px;
|
||||
z-index: 1;
|
||||
background: #bbb;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.Node-Location-ping {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
@@ -26,7 +36,7 @@
|
||||
}
|
||||
|
||||
.Node-Location-synced {
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
border-color: #d64ca8;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,12 @@ import './Location.css';
|
||||
namespace Location {
|
||||
export type Quarter = 0 | 1 | 2 | 3;
|
||||
|
||||
export interface Props {
|
||||
node: AppState.Node;
|
||||
position: Position;
|
||||
focused: boolean;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
left: number;
|
||||
top: number;
|
||||
@@ -31,13 +37,15 @@ namespace Location {
|
||||
}
|
||||
}
|
||||
|
||||
class Location extends React.Component<AppState.Node & Location.Position, Location.State> {
|
||||
class Location extends React.Component<Location.Props, Location.State> {
|
||||
public readonly state = { hover: false };
|
||||
|
||||
public render() {
|
||||
const { left, top, quarter, location } = this.props;
|
||||
const height = this.props.blockDetails[0];
|
||||
const propagationTime = this.props.blockDetails[4];
|
||||
const { node, position, focused } = this.props;
|
||||
const { left, top, quarter } = position;
|
||||
const { blockDetails, location } = node;
|
||||
const height = blockDetails[0];
|
||||
const propagationTime = blockDetails[4];
|
||||
|
||||
if (!location) {
|
||||
return null;
|
||||
@@ -45,10 +53,14 @@ class Location extends React.Component<AppState.Node & Location.Position, Locati
|
||||
|
||||
let className = `Node-Location Node-Location-quarter${quarter}`;
|
||||
|
||||
if (propagationTime != null) {
|
||||
className += ' Node-Location-synced';
|
||||
} else if (height % 2 === 1) {
|
||||
className += ' Node-Location-odd';
|
||||
if (focused) {
|
||||
if (propagationTime != null) {
|
||||
className += ' Node-Location-synced';
|
||||
} else if (height % 2 === 1) {
|
||||
className += ' Node-Location-odd';
|
||||
}
|
||||
} else {
|
||||
className += ' Node-Location-dimmed';
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -62,8 +74,9 @@ class Location extends React.Component<AppState.Node & Location.Position, Locati
|
||||
}
|
||||
|
||||
private renderDetails(location: Types.NodeLocation) {
|
||||
const [name, implementation, version, validator] = this.props.nodeDetails;
|
||||
const [height, hash, blockTime, blockTimestamp, propagationTime] = this.props.blockDetails;
|
||||
const { node } = this.props;
|
||||
const [name, implementation, version, validator] = node.nodeDetails;
|
||||
const [height, hash, blockTime, blockTimestamp, propagationTime] = node.blockDetails;
|
||||
|
||||
let validatorRow = null;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user