Filter nodes by name when typing (#53)

* Allow filtering of nodes by name
* Tidy up
This commit is contained in:
Maciej Hirsz
2018-09-25 19:56:14 +02:00
committed by GitHub
parent df56e33bf6
commit 2e483a595f
7 changed files with 198 additions and 42 deletions
@@ -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;