Turbo Render (#298)

* More responsive React updates on scroll

* `Icon`s now use shadow dom

* Faster Sparkline

* Recycle table rows

* Separate Header from Chain to avoid vdom diffing

* Separate THead from Row.HEADER to avoid vdom diffing

* Throttle rendering updates on chain tabs, also styles

* Minor tweaks and fixes

* Created components for all columns

* Wrapping up Column refactor

* Rename Row--td to Column

* Lazy `Ago`

* Update styles for faster layouting

* Minor cleanup

* Fix Connection

* Use shadow DOM in `PolkadotIcon`

* Comments and tweaks for the List component

* Faster Tooltip and Truncate

* Minor tweaks

* Tooltiped columns can now be copied

* Future-proof Connection

* Remove the <div> wrapper from Icon

* Fix dash on missing graph data

* Clean up some SVGs

* Cleanup and comments

* Localize the use of `previousKeys` to `recalculateKeys`

* Custom appState disjoint from React component state

* Make appState and appUpdate refs readonly

* Cleanup
This commit is contained in:
Maciej Hirsz
2020-11-11 13:41:01 +01:00
committed by GitHub
parent 675776c3e1
commit ebb01c1a7d
72 changed files with 1863 additions and 1118 deletions
-410
View File
@@ -1,410 +0,0 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../common';
import { State, Node } from '../../state';
import { Truncate } from './';
import { Ago, Icon, Tooltip, Sparkline, PolkadotIcon } from '../';
import {
formatNumber,
getHashData,
milliOrSecond,
secondsWithPrecision,
} from '../../utils';
export interface Column {
label: string;
icon: string;
width?: number;
setting?: keyof State.Settings;
sortBy?: (node: Node) => any;
render: (node: Node) => React.ReactElement<any> | string;
}
import nodeIcon from '../../icons/server.svg';
import nodeLocationIcon from '../../icons/location.svg';
import nodeValidatorIcon from '../../icons/shield.svg';
import nodeTypeIcon from '../../icons/terminal.svg';
import networkIdIcon from '../../icons/fingerprint.svg';
import peersIcon from '../../icons/broadcast.svg';
import transactionsIcon from '../../icons/inbox.svg';
import blockIcon from '../../icons/cube.svg';
import finalizedIcon from '../../icons/cube-alt.svg';
import blockHashIcon from '../../icons/file-binary.svg';
import blockTimeIcon from '../../icons/history.svg';
import propagationTimeIcon from '../../icons/dashboard.svg';
import lastTimeIcon from '../../icons/watch.svg';
import uploadIcon from '../../icons/cloud-upload.svg';
import downloadIcon from '../../icons/cloud-download.svg';
import stateIcon from '../../icons/git-branch.svg';
import networkIcon from '../../icons/network.svg';
import uptimeIcon from '../../icons/pulse.svg';
import externalLinkIcon from '../../icons/link-external.svg';
import parityPolkadotIcon from '../../icons/dot.svg';
import paritySubstrateIcon from '../../icons/substrate.svg';
import polkadotJsIcon from '../../icons/polkadot-js.svg';
import airalabRobonomicsIcon from '../../icons/robonomics.svg';
import chainXIcon from '../../icons/chainx.svg';
import edgewareIcon from '../../icons/edgeware.svg';
import joystreamIcon from '../../icons/joystream.svg';
import ladderIcon from '../../icons/laddernetwork.svg';
import cennznetIcon from '../../icons/cennznet.svg';
import crabIcon from '../../icons/crab.svg';
import darwiniaIcon from '../../icons/darwinia.svg';
import turingIcon from '../../icons/turingnetwork.svg';
import dothereumIcon from '../../icons/dothereum.svg';
import katalchainIcon from '../../icons/katalchain.svg';
import bifrostIcon from '../../icons/bifrost.svg';
import totemIcon from '../../icons/totem.svg';
import nodleIcon from '../../icons/nodle.svg';
import zeroIcon from '../../icons/zero.svg';
import unknownImplementationIcon from '../../icons/question-solid.svg';
const ICONS = {
'parity-polkadot': parityPolkadotIcon,
'Parity Polkadot': parityPolkadotIcon,
'polkadot-js': polkadotJsIcon,
'airalab-robonomics': airalabRobonomicsIcon,
'substrate-node': paritySubstrateIcon,
'Substrate Node': paritySubstrateIcon,
'edgeware-node': edgewareIcon,
'Edgeware Node': edgewareIcon,
'joystream-node': joystreamIcon,
ChainX: chainXIcon,
'ladder-node': ladderIcon,
'cennznet-node': cennznetIcon,
'Darwinia Crab': crabIcon,
Darwinia: darwiniaIcon,
'turing-node': turingIcon,
dothereum: dothereumIcon,
katalchain: katalchainIcon,
'bifrost-node': bifrostIcon,
'totem-meccano-node': totemIcon,
Totem: totemIcon,
'Nodle Chain Node': nodleIcon,
subzero: zeroIcon,
};
export namespace Column {
export const NAME: Column = {
label: 'Node',
icon: nodeIcon,
sortBy: ({ sortableName }) => sortableName,
render: ({ name }) => <Truncate text={name} position="left" />,
};
export const VALIDATOR: Column = {
label: 'Validator',
icon: nodeValidatorIcon,
width: 16,
setting: 'validator',
sortBy: ({ validator }) => validator || '',
render: ({ validator }) => {
return validator ? (
<Tooltip text={validator} copy={true}>
<span className="Row-validator">
<PolkadotIcon account={validator} size={16} />
</span>
</Tooltip>
) : (
'-'
);
},
};
export const LOCATION: Column = {
label: 'Location',
icon: nodeLocationIcon,
width: 140,
setting: 'location',
sortBy: ({ city }) => city || '',
render: ({ city }) =>
city ? <Truncate position="left" text={city} /> : '-',
};
export const IMPLEMENTATION: Column = {
label: 'Implementation',
icon: nodeTypeIcon,
width: 90,
setting: 'implementation',
sortBy: ({ sortableVersion }) => sortableVersion,
render: ({ implementation, version }) => {
const [semver] = version.match(SEMVER_PATTERN) || ['?.?.?'];
const implIcon = ICONS[implementation] || unknownImplementationIcon;
return (
<Tooltip text={`${implementation} v${version}`}>
<Icon src={implIcon} /> {semver}
</Tooltip>
);
},
};
export const NETWORK_ID: Column = {
label: 'Network ID',
icon: networkIdIcon,
width: 90,
setting: 'networkId',
sortBy: ({ networkId }) => networkId || '',
render: ({ networkId }) =>
networkId ? <Truncate position="left" text={networkId} /> : '-',
};
export const PEERS: Column = {
label: 'Peer Count',
icon: peersIcon,
width: 26,
setting: 'peers',
sortBy: ({ peers }) => peers,
render: ({ peers }) => `${peers}`,
};
export const TXS: Column = {
label: 'Transactions in Queue',
icon: transactionsIcon,
width: 26,
setting: 'txs',
sortBy: ({ txs }) => txs,
render: ({ txs }) => `${txs}`,
};
export const UPLOAD: Column = {
label: 'Upload Bandwidth',
icon: uploadIcon,
width: 40,
setting: 'upload',
sortBy: ({ upload }) => (upload.length < 3 ? 0 : upload[upload.length - 1]),
render: ({ upload, chartstamps }) => {
if (upload.length < 3) {
return '-';
}
return (
<Sparkline
width={44}
height={16}
stroke={1}
format={formatBandwidth}
values={upload}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
);
},
};
export const DOWNLOAD: Column = {
label: 'Download Bandwidth',
icon: downloadIcon,
width: 40,
setting: 'download',
sortBy: ({ download }) =>
download.length < 3 ? 0 : download[download.length - 1],
render: ({ download, chartstamps }) => {
if (download.length < 3) {
return '-';
}
return (
<Sparkline
width={44}
height={16}
stroke={1}
format={formatBandwidth}
values={download}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
);
},
};
export const STATE_CACHE: Column = {
label: 'State Cache Size',
icon: stateIcon,
width: 40,
setting: 'stateCacheSize',
sortBy: ({ stateCacheSize }) =>
stateCacheSize.length < 3 ? 0 : stateCacheSize[stateCacheSize.length - 1],
render: ({ stateCacheSize, chartstamps }) => {
if (stateCacheSize.length < 3) {
return '-';
}
return (
<Sparkline
width={44}
height={16}
stroke={1}
format={formatBytes}
values={stateCacheSize}
stamps={chartstamps}
minScale={MEMORY_SCALE}
/>
);
},
};
export const BLOCK_NUMBER: Column = {
label: 'Block',
icon: blockIcon,
width: 88,
setting: 'blocknumber',
sortBy: ({ height }) => height || 0,
render: ({ height }) => `#${formatNumber(height)}`,
};
export const BLOCK_HASH: Column = {
label: 'Block Hash',
icon: blockHashIcon,
width: 154,
setting: 'blockhash',
sortBy: ({ hash }) => hash || '',
render: ({ hash }) => <Truncate position="right" text={hash} copy={true} />,
};
export const FINALIZED: Column = {
label: 'Finalized Block',
icon: finalizedIcon,
width: 88,
setting: 'finalized',
sortBy: ({ finalized }) => finalized || 0,
render: ({ finalized }) => `#${formatNumber(finalized)}`,
};
export const FINALIZED_HASH: Column = {
label: 'Finalized Block Hash',
icon: blockHashIcon,
width: 154,
setting: 'finalizedhash',
sortBy: ({ finalizedHash }) => finalizedHash || '',
render: ({ finalizedHash }) => (
<Truncate position="right" text={finalizedHash} copy={true} />
),
};
export const BLOCK_TIME: Column = {
label: 'Block Time',
icon: blockTimeIcon,
width: 80,
setting: 'blocktime',
sortBy: ({ blockTime }) => (blockTime == null ? Infinity : blockTime),
render: ({ blockTime }) => `${secondsWithPrecision(blockTime / 1000)}`,
};
export const BLOCK_PROPAGATION: Column = {
label: 'Block Propagation Time',
icon: propagationTimeIcon,
width: 58,
setting: 'blockpropagation',
sortBy: ({ propagationTime }) =>
propagationTime == null ? Infinity : propagationTime,
render: ({ propagationTime }) =>
propagationTime == null ? '∞' : milliOrSecond(propagationTime),
};
export const BLOCK_LAST_TIME: Column = {
label: 'Last Block Time',
icon: lastTimeIcon,
width: 100,
setting: 'blocklasttime',
sortBy: ({ blockTimestamp }) => blockTimestamp || 0,
render: ({ blockTimestamp }) => <Ago when={blockTimestamp} />,
};
export const UPTIME: Column = {
label: 'Node Uptime',
icon: uptimeIcon,
width: 58,
setting: 'uptime',
sortBy: ({ connectedAt }) => connectedAt || 0,
render: ({ connectedAt }) => <Ago when={connectedAt} justTime={true} />,
};
export const NETWORK_STATE: Column = {
label: 'NetworkState',
icon: networkIcon,
width: 16,
setting: 'networkstate',
render: ({ id }) => {
const chainLabel = getHashData().chain;
if (!chainLabel) {
return '-';
}
const uri = `${URI_BASE}${encodeURIComponent(chainLabel)}/${id}/`;
return (
<a href={uri} target="_blank">
<Icon src={externalLinkIcon} />
</a>
);
},
};
}
const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
const BANDWIDTH_SCALE = 1024 * 1024;
const MEMORY_SCALE = 2 * 1024 * 1024;
const URI_BASE =
window.location.protocol === 'https:'
? `/network_state/`
: `http://${window.location.hostname}:8000/network_state/`;
function formatStamp(stamp: Types.Timestamp): string {
const passed = ((timestamp() - stamp) / 1000) | 0;
const hours = (passed / 3600) | 0;
const minutes = ((passed % 3600) / 60) | 0;
const seconds = passed % 60 | 0;
return hours
? `${hours}h ago`
: minutes
? `${minutes}m ago`
: `${seconds}s ago`;
}
function formatMemory(kbs: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
const mbs = (kbs / 1024) | 0;
if (mbs >= 1000) {
return `${(mbs / 1024).toFixed(1)} GB${ago}`;
} else {
return `${mbs} MB${ago}`;
}
}
function formatBytes(bytes: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB${ago}`;
} else if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB${ago}`;
} else if (bytes >= 1000) {
return `${(bytes / 1024).toFixed(1)} kB${ago}`;
} else {
return `${bytes} B${ago}`;
}
}
function formatBandwidth(bps: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
if (bps >= 1024 * 1024) {
return `${(bps / (1024 * 1024)).toFixed(1)} MB/s${ago}`;
} else if (bps >= 1000) {
return `${(bps / 1024).toFixed(1)} kB/s${ago}`;
} else {
return `${bps | 0} B/s${ago}`;
}
}
function formatCPU(cpu: number, stamp: Maybe<Types.Timestamp>): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
const fractionDigits = cpu > 100 ? 0 : cpu > 10 ? 1 : cpu > 1 ? 2 : 3;
return `${cpu.toFixed(fractionDigits)}%${ago}`;
}
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate, Tooltip } from '../../';
import icon from '../../../icons/file-binary.svg';
export class BlockHashColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Block Hash';
public static readonly icon = icon;
public static readonly width = 154;
public static readonly setting = 'blockhash';
public static readonly sortBy = ({ hash }: Node) => hash || '';
private data: Maybe<string>;
private copy: Maybe<Tooltip.CopyCallback>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.hash;
}
render() {
const { hash } = this.props.node;
this.data = hash;
return (
<td className="Column" onClick={this.onClick}>
<Tooltip text={hash} position="right" copy={this.onCopy} />
<Truncate text={hash} chars={16} />
</td>
);
}
private onCopy = (copy: Tooltip.CopyCallback) => {
this.copy = copy;
};
private onClick = (event: React.MouseEvent) => {
event.stopPropagation();
if (this.copy != null) {
this.copy();
}
};
}
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { formatNumber } from '../../../utils';
import icon from '../../../icons/cube.svg';
export class BlockNumberColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Block';
public static readonly icon = icon;
public static readonly width = 88;
public static readonly setting = 'blocknumber';
public static readonly sortBy = ({ height }: Node) => height;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.height;
}
render() {
const { height } = this.props.node;
this.data = height;
return <td className="Column">{`#${formatNumber(height)}`}</td>;
}
}
@@ -0,0 +1,31 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { milliOrSecond } from '../../../utils';
import icon from '../../../icons/dashboard.svg';
export class BlockPropagationColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Block Propagation Time';
public static readonly icon = icon;
public static readonly width = 58;
public static readonly setting = 'blockpropagation';
public static readonly sortBy = ({ propagationTime }: Node) =>
propagationTime == null ? Infinity : propagationTime;
private data: Maybe<number>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.propagationTime;
}
render() {
const { propagationTime } = this.props.node;
const print =
propagationTime == null ? '∞' : milliOrSecond(propagationTime);
this.data = propagationTime;
return <td className="Column">{print}</td>;
}
}
@@ -0,0 +1,30 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { secondsWithPrecision } from '../../../utils';
import icon from '../../../icons/history.svg';
export class BlockTimeColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Block Time';
public static readonly icon = icon;
public static readonly width = 80;
public static readonly setting = 'blocktime';
public static readonly sortBy = ({ blockTime }: Node) =>
blockTime == null ? Infinity : blockTime;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.blockTime;
}
render() {
const { blockTime } = this.props.node;
this.data = blockTime;
return (
<td className="Column">{`${secondsWithPrecision(blockTime / 1000)}`}</td>
);
}
}
@@ -0,0 +1,43 @@
.Column {
text-align: left;
padding: 6px 13px;
height: 19px;
position: relative;
white-space: nowrap;
}
.Column-truncate {
position: absolute;
left: 0;
right: 0;
top: 0;
padding: 6px 13px;
overflow: hidden;
text-overflow: ellipsis;
}
.Column-Tooltip {
position: initial !important;
padding: inherit !important;
}
.Column-validator {
display: block;
width: 16px;
height: 16px;
cursor: pointer;
}
.Column-validator:hover {
transform: scale(2);
}
.Column--a {
color: inherit;
text-decoration: none;
}
.Column--a:hover {
text-decoration: underline;
}
@@ -0,0 +1,102 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../../common';
import { Node } from '../../../state';
import './Column.css';
import {
NameColumn,
ValidatorColumn,
LocationColumn,
ImplementationColumn,
NetworkIdColumn,
PeersColumn,
TxsColumn,
UploadColumn,
DownloadColumn,
StateCacheColumn,
BlockNumberColumn,
BlockHashColumn,
FinalizedBlockColumn,
FinalizedHashColumn,
BlockTimeColumn,
BlockPropagationColumn,
LastBlockColumn,
UptimeColumn,
NetworkStateColumn,
} from './';
export type Column =
| typeof NameColumn
| typeof ValidatorColumn
| typeof LocationColumn
| typeof ImplementationColumn
| typeof NetworkIdColumn
| typeof PeersColumn
| typeof TxsColumn
| typeof UploadColumn
| typeof DownloadColumn
| typeof StateCacheColumn
| typeof BlockNumberColumn
| typeof BlockHashColumn
| typeof FinalizedBlockColumn
| typeof FinalizedHashColumn
| typeof BlockTimeColumn
| typeof BlockPropagationColumn
| typeof LastBlockColumn
| typeof UptimeColumn
| typeof NetworkStateColumn;
export namespace Column {
export interface Props {
node: Node;
}
export function formatBytes(
bytes: number,
stamp: Maybe<Types.Timestamp>
): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB${ago}`;
} else if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB${ago}`;
} else if (bytes >= 1000) {
return `${(bytes / 1024).toFixed(1)} kB${ago}`;
} else {
return `${bytes} B${ago}`;
}
}
export function formatBandwidth(
bps: number,
stamp: Maybe<Types.Timestamp>
): string {
const ago = stamp ? ` (${formatStamp(stamp)})` : '';
if (bps >= 1024 * 1024) {
return `${(bps / (1024 * 1024)).toFixed(1)} MB/s${ago}`;
} else if (bps >= 1000) {
return `${(bps / 1024).toFixed(1)} kB/s${ago}`;
} else {
return `${bps | 0} B/s${ago}`;
}
}
}
export const BANDWIDTH_SCALE = 1024 * 1024;
function formatStamp(stamp: Types.Timestamp): string {
const passed = ((timestamp() - stamp) / 1000) | 0;
const hours = (passed / 3600) | 0;
const minutes = ((passed % 3600) / 60) | 0;
const seconds = passed % 60 | 0;
return hours
? `${hours}h ago`
: minutes
? `${minutes}m ago`
: `${seconds}s ago`;
}
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../../common';
import { Column, BANDWIDTH_SCALE } from './';
import { Node } from '../../../state';
import { Sparkline } from '../../';
import icon from '../../../icons/cloud-download.svg';
export class DownloadColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Download Bandwidth';
public static readonly icon = icon;
public static readonly width = 40;
public static readonly setting = 'download';
public static readonly sortBy = ({ download }: Node) =>
download.length < 3 ? 0 : download[download.length - 1];
private data: Array<number> = [];
public shouldComponentUpdate(nextProps: Column.Props) {
// Diffing by ref, as data is an immutable array
return this.data !== nextProps.node.download;
}
render() {
const { download, chartstamps } = this.props.node;
this.data = download;
if (download.length < 3) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Sparkline
width={44}
height={16}
stroke={1}
format={Column.formatBandwidth}
values={download}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
</td>
);
}
}
@@ -0,0 +1,27 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { formatNumber } from '../../../utils';
import icon from '../../../icons/cube-alt.svg';
export class FinalizedBlockColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Finalized Block';
public static readonly icon = icon;
public static readonly width = 88;
public static readonly setting = 'finalized';
public static readonly sortBy = ({ finalized }: Node) => finalized || 0;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.finalized;
}
render() {
const { finalized } = this.props.node;
this.data = finalized;
return <td className="Column">{`#${formatNumber(finalized)}`}</td>;
}
}
@@ -0,0 +1,47 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate, Tooltip } from '../../';
import icon from '../../../icons/file-binary.svg';
export class FinalizedHashColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Finalized Block Hash';
public static readonly icon = icon;
public static readonly width = 154;
public static readonly setting = 'finalizedhash';
public static readonly sortBy = ({ finalizedHash }: Node) =>
finalizedHash || '';
private data: Maybe<string>;
private copy: Maybe<Tooltip.CopyCallback>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.finalizedHash;
}
render() {
const { finalizedHash } = this.props.node;
this.data = finalizedHash;
return (
<td className="Column" onClick={this.onClick}>
<Tooltip text={finalizedHash} position="right" copy={this.onCopy} />
<Truncate text={finalizedHash} chars={16} />
</td>
);
}
private onCopy = (copy: Tooltip.CopyCallback) => {
this.copy = copy;
};
private onClick = (event: React.MouseEvent) => {
event.stopPropagation();
if (this.copy != null) {
this.copy();
}
};
}
@@ -0,0 +1,91 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Tooltip, Icon } from '../../';
import icon from '../../../icons/terminal.svg';
import parityPolkadotIcon from '../../../icons/dot.svg';
import paritySubstrateIcon from '../../../icons/substrate.svg';
import polkadotJsIcon from '../../../icons/polkadot-js.svg';
import airalabRobonomicsIcon from '../../../icons/robonomics.svg';
import chainXIcon from '../../../icons/chainx.svg';
import edgewareIcon from '../../../icons/edgeware.svg';
import joystreamIcon from '../../../icons/joystream.svg';
import ladderIcon from '../../../icons/laddernetwork.svg';
import cennznetIcon from '../../../icons/cennznet.svg';
import crabIcon from '../../../icons/crab.svg';
import darwiniaIcon from '../../../icons/darwinia.svg';
import turingIcon from '../../../icons/turingnetwork.svg';
import dothereumIcon from '../../../icons/dothereum.svg';
import katalchainIcon from '../../../icons/katalchain.svg';
import bifrostIcon from '../../../icons/bifrost.svg';
import totemIcon from '../../../icons/totem.svg';
import nodleIcon from '../../../icons/nodle.svg';
import zeroIcon from '../../../icons/zero.svg';
const ICONS = {
'parity-polkadot': parityPolkadotIcon,
'Parity Polkadot': parityPolkadotIcon,
'polkadot-js': polkadotJsIcon,
'airalab-robonomics': airalabRobonomicsIcon,
'substrate-node': paritySubstrateIcon,
'Substrate Node': paritySubstrateIcon,
'edgeware-node': edgewareIcon,
'Edgeware Node': edgewareIcon,
'joystream-node': joystreamIcon,
ChainX: chainXIcon,
'ladder-node': ladderIcon,
'cennznet-node': cennznetIcon,
'Darwinia Crab': crabIcon,
Darwinia: darwiniaIcon,
'turing-node': turingIcon,
dothereum: dothereumIcon,
katalchain: katalchainIcon,
'bifrost-node': bifrostIcon,
'totem-meccano-node': totemIcon,
Totem: totemIcon,
'Nodle Chain Node': nodleIcon,
subzero: zeroIcon,
};
const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
export class ImplementationColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Implementation';
public static readonly icon = icon;
public static readonly width = 90;
public static readonly setting = 'implementation';
public static readonly sortBy = ({ sortableVersion }: Node) =>
sortableVersion;
private implementation: string;
private version: string;
public shouldComponentUpdate(nextProps: Column.Props) {
if (this.props.node === nextProps.node) {
// Implementation can't change unless we got a new node
return false;
}
return (
this.implementation !== nextProps.node.implementation ||
this.version !== nextProps.node.version
);
}
render() {
const { implementation, version } = this.props.node;
this.implementation = implementation;
this.version = version;
const [semver] = version.match(SEMVER_PATTERN) || ['?.?.?'];
const implIcon = ICONS[implementation] || paritySubstrateIcon;
return (
<td className="Column">
<Tooltip text={`${implementation} v${version}`} />
<Icon src={implIcon} /> {semver}
</td>
);
}
}
@@ -0,0 +1,32 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Ago } from '../../';
import icon from '../../../icons/watch.svg';
export class LastBlockColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Last Block Time';
public static readonly icon = icon;
public static readonly width = 100;
public static readonly setting = 'blocklasttime';
public static readonly sortBy = ({ blockTimestamp }: Node) =>
blockTimestamp || 0;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.blockTimestamp;
}
render() {
const { blockTimestamp } = this.props.node;
this.data = blockTimestamp;
return (
<td className="Column">
<Ago when={blockTimestamp} />
</td>
);
}
}
@@ -0,0 +1,37 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate, Tooltip } from '../../';
import icon from '../../../icons/location.svg';
export class LocationColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Location';
public static readonly icon = icon;
public static readonly width = 140;
public static readonly setting = 'location';
public static readonly sortBy = ({ city }: Node) => city || '';
private data: Maybe<string>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.city;
}
render() {
const { city } = this.props.node;
this.data = city;
if (!city) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Tooltip text={city} position="left" />
<Truncate text={city} chars={14} />
</td>
);
}
}
@@ -0,0 +1,29 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate, Tooltip } from '../../';
import icon from '../../../icons/server.svg';
export class NameColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Node';
public static readonly icon = icon;
public static readonly setting = null;
public static readonly width = null;
public static readonly sortBy = ({ sortableName }: Node) => sortableName;
public shouldComponentUpdate(nextProps: Column.Props) {
// Node name only changes when the node does
return this.props.node !== nextProps.node;
}
render() {
const { name } = this.props.node;
return (
<td className="Column">
<Tooltip text={name} position="left" />
<Truncate text={name} />
</td>
);
}
}
@@ -0,0 +1,38 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Truncate } from '../../';
import { Tooltip } from '../../';
import icon from '../../../icons/fingerprint.svg';
export class NetworkIdColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Network ID';
public static readonly icon = icon;
public static readonly width = 90;
public static readonly setting = 'networkId';
public static readonly sortBy = ({ networkId }: Node) => networkId || '';
private data: Maybe<string>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.networkId;
}
render() {
const { networkId } = this.props.node;
this.data = networkId;
if (!networkId) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Tooltip text={networkId} position="left" />
<Truncate text={networkId} chars={10} />
</td>
);
}
}
@@ -0,0 +1,44 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Icon } from '../../';
import icon from '../../../icons/network.svg';
import externalLinkIcon from '../../../icons/link-external.svg';
import { getHashData } from '../../../utils';
const URI_BASE =
window.location.protocol === 'https:'
? `/network_state/`
: `http://${window.location.hostname}:8000/network_state/`;
export class NetworkStateColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Network State';
public static readonly icon = icon;
public static readonly width = 16;
public static readonly setting = 'networkstate';
public static readonly sortBy = null;
public shouldComponentUpdate(nextProps: Column.Props) {
// Network state link changes when the node does
return this.props.node !== nextProps.node;
}
render() {
const { id } = this.props.node;
const chainLabel = getHashData().chain;
if (!chainLabel) {
return <td className="Column">-</td>;
}
const uri = `${URI_BASE}${encodeURIComponent(chainLabel)}/${id}/`;
return (
<td className="Column">
<a className="Column--a" href={uri} target="_blank">
<Icon src={externalLinkIcon} />
</a>
</td>
);
}
}
@@ -0,0 +1,26 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import icon from '../../../icons/broadcast.svg';
export class PeersColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Peer Count';
public static readonly icon = icon;
public static readonly width = 26;
public static readonly setting = 'peers';
public static readonly sortBy = ({ peers }: Node) => peers;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.peers;
}
render() {
const { peers } = this.props.node;
this.data = peers;
return <td className="Column">{peers}</td>;
}
}
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../../common';
import { Column, BANDWIDTH_SCALE } from './';
import { Node } from '../../../state';
import { Sparkline } from '../../';
import icon from '../../../icons/git-branch.svg';
export class StateCacheColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'State Cache Size';
public static readonly icon = icon;
public static readonly width = 40;
public static readonly setting = 'stateCacheSize';
public static readonly sortBy = ({ stateCacheSize }: Node) =>
stateCacheSize.length < 3 ? 0 : stateCacheSize[stateCacheSize.length - 1];
private data: Array<number> = [];
public shouldComponentUpdate(nextProps: Column.Props) {
// Diffing by ref, as data is an immutable array
return this.data !== nextProps.node.stateCacheSize;
}
render() {
const { stateCacheSize, chartstamps } = this.props.node;
this.data = stateCacheSize;
if (stateCacheSize.length < 3) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Sparkline
width={44}
height={16}
stroke={1}
format={Column.formatBytes}
values={stateCacheSize}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
</td>
);
}
}
@@ -0,0 +1,26 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import icon from '../../../icons/inbox.svg';
export class TxsColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Transactions in Queue';
public static readonly icon = icon;
public static readonly width = 26;
public static readonly setting = 'txs';
public static readonly sortBy = ({ txs }: Node) => txs;
private data = 0;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.txs;
}
render() {
const { txs } = this.props.node;
this.data = txs;
return <td className="Column">{txs}</td>;
}
}
@@ -0,0 +1,46 @@
import * as React from 'react';
import { Types, Maybe, timestamp } from '../../../common';
import { Column, BANDWIDTH_SCALE } from './';
import { Node } from '../../../state';
import { Sparkline } from '../../';
import icon from '../../../icons/cloud-upload.svg';
export class UploadColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Upload Bandwidth';
public static readonly icon = icon;
public static readonly width = 40;
public static readonly setting = 'upload';
public static readonly sortBy = ({ upload }: Node) =>
upload.length < 3 ? 0 : upload[upload.length - 1];
private data: Array<number> = [];
public shouldComponentUpdate(nextProps: Column.Props) {
// Diffing by ref, as data is an immutable array
return this.data !== nextProps.node.upload;
}
render() {
const { upload, chartstamps } = this.props.node;
this.data = upload;
if (upload.length < 3) {
return <td className="Column">-</td>;
}
return (
<td className="Column">
<Sparkline
width={44}
height={16}
stroke={1}
format={Column.formatBandwidth}
values={upload}
stamps={chartstamps}
minScale={BANDWIDTH_SCALE}
/>
</td>
);
}
}
@@ -0,0 +1,28 @@
import * as React from 'react';
import { Column } from './';
import { Node } from '../../../state';
import { Ago } from '../../';
import icon from '../../../icons/pulse.svg';
export class UptimeColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Node Uptime';
public static readonly icon = icon;
public static readonly width = 58;
public static readonly setting = 'uptime';
public static readonly sortBy = ({ connectedAt }: Node) => connectedAt || 0;
public shouldComponentUpdate(nextProps: Column.Props) {
// Uptime only changes when the node does
return this.props.node !== nextProps.node;
}
render() {
const { connectedAt } = this.props.node;
return (
<td className="Column">
<Ago when={connectedAt} justTime={true} />
</td>
);
}
}
@@ -0,0 +1,54 @@
import * as React from 'react';
import { Maybe } from '../../../common';
import { Column } from './';
import { Node } from '../../../state';
import { Tooltip, PolkadotIcon } from '../../';
import icon from '../../../icons/shield.svg';
export class ValidatorColumn extends React.Component<Column.Props, {}> {
public static readonly label = 'Validator';
public static readonly icon = icon;
public static readonly width = 16;
public static readonly setting = 'validator';
public static readonly sortBy = ({ validator }: Node) => validator || '';
private data: Maybe<string>;
private copy: Maybe<Tooltip.CopyCallback>;
public shouldComponentUpdate(nextProps: Column.Props) {
return this.data !== nextProps.node.validator;
}
render() {
const { validator } = this.props.node;
this.data = validator;
if (!validator) {
return <td className="Column">-</td>;
}
return (
<td className="Column" onClick={this.onClick}>
<Tooltip text={validator} copy={this.onCopy} />
<PolkadotIcon
className="Column-validator"
account={validator}
size={16}
/>
</td>
);
}
private onCopy = (copy: Tooltip.CopyCallback) => {
this.copy = copy;
};
private onClick = (event: React.MouseEvent) => {
event.stopPropagation();
if (this.copy != null) {
this.copy();
}
};
}
@@ -0,0 +1,20 @@
export * from './Column';
export * from './NameColumn';
export * from './ValidatorColumn';
export * from './LocationColumn';
export * from './ImplementationColumn';
export * from './NetworkIdColumn';
export * from './PeersColumn';
export * from './TxsColumn';
export * from './UploadColumn';
export * from './DownloadColumn';
export * from './StateCacheColumn';
export * from './BlockNumberColumn';
export * from './BlockHashColumn';
export * from './FinalizedBlockColumn';
export * from './FinalizedHashColumn';
export * from './BlockTimeColumn';
export * from './BlockPropagationColumn';
export * from './LastBlockColumn';
export * from './UptimeColumn';
export * from './NetworkStateColumn';
+6 -8
View File
@@ -11,15 +11,13 @@
font-weight: 300;
}
.List table {
.List-padding {
padding: 0;
margin: 0;
}
.List--table {
width: 100%;
border-spacing: 0;
}
.List thead {
background: #393838;
}
.List tbody {
font-family: monospace, sans-serif;
}
+82 -19
View File
@@ -1,8 +1,8 @@
import * as React from 'react';
import { Types, Maybe } from '../../common';
import { Filter } from '../';
import { State as AppState, Node } from '../../state';
import { Row } from './';
import { State as AppState, Update as AppUpdate, Node } from '../../state';
import { Row, THead } from './';
import { Persistent, PersistentSet } from '../../persist';
import { viewport } from '../../utils';
@@ -16,6 +16,7 @@ import './List.css';
export namespace List {
export interface Props {
appState: Readonly<AppState>;
appUpdate: AppUpdate;
pins: PersistentSet<Types.NodeName>;
sortBy: Persistent<Maybe<number>>;
}
@@ -23,20 +24,24 @@ export namespace List {
export interface State {
filter: Maybe<(node: Node) => boolean>;
viewportHeight: number;
listStart: number;
listEnd: number;
}
}
// Helper for readability, used as `key` prop for each `Row`
// of the `List`, so that we can maximize re-using DOM elements.
type Key = number;
export class List extends React.Component<List.Props, {}> {
public state = {
filter: null,
viewportHeight: viewport().height,
listStart: 0,
listEnd: 0,
};
private listStart = 0;
private listEnd = 0;
private relativeTop = -1;
private nextKey: Key = 0;
private previousKeys = new Map<Types.NodeId, Key>();
public componentDidMount() {
this.onScroll();
@@ -53,7 +58,7 @@ export class List extends React.Component<List.Props, {}> {
public render() {
const { pins, sortBy, appState } = this.props;
const { selectedColumns } = appState;
const { filter, listStart, listEnd } = this.state;
const { filter } = this.state;
let nodes = appState.nodes.sorted();
@@ -76,23 +81,26 @@ export class List extends React.Component<List.Props, {}> {
// to rendering view, so we put the whole list in focus
appState.nodes.setFocus(0, nodes.length);
} else {
appState.nodes.setFocus(listStart, listEnd);
appState.nodes.setFocus(this.listStart, this.listEnd);
}
const height = TH_HEIGHT + nodes.length * TR_HEIGHT;
const transform = `translateY(${listStart * TR_HEIGHT}px)`;
const top = this.listStart * TR_HEIGHT;
nodes = nodes.slice(listStart, listEnd);
nodes = nodes.slice(this.listStart, this.listEnd);
const keys = this.recalculateKeys(nodes);
return (
<React.Fragment>
<>
<div className="List" style={{ height }}>
<table>
<Row.HEADER columns={selectedColumns} sortBy={sortBy} />
<tbody style={{ transform }}>
{nodes.map((node) => (
<table className="List--table">
<THead columns={selectedColumns} sortBy={sortBy} />
<tbody>
<tr className="List-padding" style={{ height: `${top}px` }} />
{nodes.map((node, i) => (
<Row
key={node.id}
key={keys[i]}
node={node}
pins={pins}
columns={selectedColumns}
@@ -102,10 +110,63 @@ export class List extends React.Component<List.Props, {}> {
</table>
</div>
<Filter onChange={this.onFilterChange} />
</React.Fragment>
</>
);
}
// Get an array of keys for each `Node` in viewport in order.
//
// * If a `Node` was previously rendered, it will keep its `Key`.
//
// * If a `Node` is new to the viewport, it will get a `Key` of
// another `Node` that was removed from the viewport, or a new one.
private recalculateKeys(nodes: Array<Node>): Array<Key> {
// First we find all keys for `Node`s which didn't change from
// last render.
const keptKeys: Array<Maybe<Key>> = nodes.map(({ id }) => {
const key = this.previousKeys.get(id);
if (key != null) {
this.previousKeys.delete(id);
}
return key;
});
// Array of all unused keys
const unusedKeys = Array.from(this.previousKeys.values());
let search = 0;
// Clear the map so we can set new values
this.previousKeys.clear();
// Filling in blanks and re-populate previousKeys
return keptKeys.map((key: Maybe<Key>, i) => {
const id = nodes[i].id;
// `Node` was previously in viewport
if (key != null) {
this.previousKeys.set(id, key);
return key;
}
// Recycle the next unused key
if (search < unusedKeys.length) {
const unused = unusedKeys[search++];
this.previousKeys.set(id, unused);
return unused;
}
// No unused keys left, generate a new key
const newKey = this.nextKey++;
this.previousKeys.set(id, newKey);
return newKey;
});
}
private onScroll = () => {
const relativeTop = divisibleBy(
window.scrollY - (HEADER + TR_HEIGHT),
@@ -125,8 +186,10 @@ export class List extends React.Component<List.Props, {}> {
const listStart = Math.max(((top / TR_HEIGHT) | 0) - ROW_MARGIN, 0);
const listEnd = listStart + ROW_MARGIN * 2 + Math.ceil(height / TR_HEIGHT);
if (listStart !== this.state.listStart || listEnd !== this.state.listEnd) {
this.setState({ listStart, listEnd });
if (listStart !== this.listStart || listEnd !== this.listEnd) {
this.listStart = listStart;
this.listEnd = listEnd;
this.props.appUpdate({});
}
};
+1 -62
View File
@@ -3,56 +3,6 @@
cursor: pointer;
}
.Row a {
color: inherit;
text-decoration: none;
}
.Row a:hover {
text-decoration: underline;
}
.Row-Header th,
.Row td {
text-align: left;
padding: 6px 13px;
height: 19px;
}
.Row td {
position: relative;
}
.Row-Header th {
height: 23px;
}
.Row-Header th.HeaderCell-sortable {
cursor: pointer;
}
.Row-Header th.HeaderCell-sorted {
cursor: pointer;
background: #e6007a;
color: #fff;
}
.Row .Row-truncate {
position: absolute;
left: 0;
right: 0;
top: 0;
padding: inherit;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.Row .Row-Tooltip {
position: initial;
padding: inherit;
}
.Row-synced {
color: #fff;
}
@@ -72,20 +22,9 @@
}
.Row-stale {
font-style: italic;
color: #555;
}
.Row:hover {
background-color: #1e1e1e;
}
.Row-validator {
display: block;
width: 16px;
height: 16px;
cursor: pointer;
}
.Row-validator:hover {
transform: scale(2);
}
+49 -66
View File
@@ -2,7 +2,28 @@ import * as React from 'react';
import { Types, Maybe } from '../../common';
import { Node } from '../../state';
import { Persistent, PersistentSet } from '../../persist';
import { HeaderCell, Column } from './';
import {
Column,
NameColumn,
ValidatorColumn,
LocationColumn,
ImplementationColumn,
NetworkIdColumn,
PeersColumn,
TxsColumn,
UploadColumn,
DownloadColumn,
StateCacheColumn,
BlockNumberColumn,
BlockHashColumn,
FinalizedBlockColumn,
FinalizedHashColumn,
BlockTimeColumn,
BlockPropagationColumn,
LastBlockColumn,
UptimeColumn,
NetworkStateColumn,
} from './';
import './Row.css';
@@ -25,75 +46,41 @@ interface HeaderProps {
export class Row extends React.Component<Row.Props, Row.State> {
public static readonly columns: Column[] = [
Column.NAME,
Column.VALIDATOR,
Column.LOCATION,
Column.IMPLEMENTATION,
Column.NETWORK_ID,
Column.PEERS,
Column.TXS,
Column.UPLOAD,
Column.DOWNLOAD,
Column.STATE_CACHE,
Column.BLOCK_NUMBER,
Column.BLOCK_HASH,
Column.FINALIZED,
Column.FINALIZED_HASH,
Column.BLOCK_TIME,
Column.BLOCK_PROPAGATION,
Column.BLOCK_LAST_TIME,
Column.UPTIME,
Column.NETWORK_STATE,
NameColumn,
ValidatorColumn,
LocationColumn,
ImplementationColumn,
NetworkIdColumn,
PeersColumn,
TxsColumn,
UploadColumn,
DownloadColumn,
StateCacheColumn,
BlockNumberColumn,
BlockHashColumn,
FinalizedBlockColumn,
FinalizedHashColumn,
BlockTimeColumn,
BlockPropagationColumn,
LastBlockColumn,
UptimeColumn,
NetworkStateColumn,
];
public static HEADER = (props: HeaderProps) => {
const { columns, sortBy } = props;
const last = columns.length - 1;
private renderedChangeRef = 0;
return (
<thead>
<tr className="Row-Header">
{columns.map((col, index) => (
<HeaderCell
key={index}
column={col}
index={index}
last={last}
sortBy={sortBy}
/>
))}
</tr>
</thead>
);
};
public state = { update: 0 };
public componentDidMount() {
const { node } = this.props;
node.subscribe(this.onUpdate);
}
public componentWillUnmount() {
const { node } = this.props;
node.unsubscribe(this.onUpdate);
}
public shouldComponentUpdate(
nextProps: Row.Props,
nextState: Row.State
): boolean {
public shouldComponentUpdate(nextProps: Row.Props): boolean {
return (
this.props.node.id !== nextProps.node.id ||
this.state.update !== nextState.update
this.renderedChangeRef !== nextProps.node.changeRef
);
}
public render() {
const { node, columns } = this.props;
this.renderedChangeRef = node.changeRef;
let className = 'Row';
if (node.propagationTime != null) {
@@ -110,9 +97,9 @@ export class Row extends React.Component<Row.Props, Row.State> {
return (
<tr className={className} onClick={this.toggle}>
{columns.map(({ render }, index) => (
<td key={index}>{render(node)}</td>
))}
{columns.map((col, index) =>
React.createElement(col, { node, key: index })
)}
</tr>
);
}
@@ -126,8 +113,4 @@ export class Row extends React.Component<Row.Props, Row.State> {
pins.add(node.name);
}
};
private onUpdate = () => {
this.setState({ update: this.state.update + 1 });
};
}
+24
View File
@@ -0,0 +1,24 @@
.THead {
background: #393838;
}
.THeadCell {
text-align: left;
padding: 6px 13px;
height: 23px;
}
.THeadCell-sortable {
cursor: pointer;
}
.THeadCell-sorted {
cursor: pointer;
background: #e6007a;
color: #fff;
}
.THeadCell-container {
position: relative;
display: inline-block;
}
+50
View File
@@ -0,0 +1,50 @@
import * as React from 'react';
import { Maybe } from '../../common';
import { Column, THeadCell } from './';
import { Persistent } from '../../persist';
import './THead.css';
export namespace THead {
export interface Props {
columns: Column[];
sortBy: Persistent<Maybe<number>>;
}
}
export class THead extends React.Component<THead.Props, {}> {
private sortBy: Maybe<number>;
constructor(props: THead.Props) {
super(props);
this.sortBy = props.sortBy.get();
}
public shouldComponentUpdate(nextProps: THead.Props) {
return this.sortBy !== nextProps.sortBy.get();
}
public render() {
const { columns, sortBy } = this.props;
const last = columns.length - 1;
this.sortBy = sortBy.get();
return (
<thead>
<tr className="THead">
{columns.map((col, index) => (
<THeadCell
key={index}
column={col}
index={index}
last={last}
sortBy={sortBy}
/>
))}
</tr>
</thead>
);
}
}
@@ -7,7 +7,7 @@ import { Persistent } from '../../persist';
import sortAscIcon from '../../icons/triangle-up.svg';
import sortDescIcon from '../../icons/triangle-down.svg';
export namespace HeaderCell {
export namespace THeadCell {
export interface Props {
column: Column;
index: number;
@@ -16,7 +16,7 @@ export namespace HeaderCell {
}
}
export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
export class THeadCell extends React.Component<THeadCell.Props, {}> {
public render() {
const { column, index, last } = this.props;
const { icon, width, label } = column;
@@ -25,10 +25,10 @@ export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
const sortBy = this.props.sortBy.get();
const className =
column.sortBy == null
? ''
? 'THeadCell'
: sortBy === index || sortBy === ~index
? 'HeaderCell-sorted'
: 'HeaderCell-sortable';
? 'THeadCell THeadCell-sorted'
: 'THeadCell THeadCell-sortable';
const i =
sortBy === index ? sortAscIcon : sortBy === ~index ? sortDescIcon : icon;
@@ -38,9 +38,10 @@ export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
style={width ? { width } : undefined}
onClick={this.toggleSort}
>
<Tooltip text={label} inline={true} position={position}>
<span className="THeadCell-container">
<Tooltip text={label} position={position} />
<Icon src={i} />
</Tooltip>
</span>
</th>
);
}
-38
View File
@@ -1,38 +0,0 @@
import * as React from 'react';
import { Tooltip } from '../';
export namespace Truncate {
export interface Props {
text: string;
copy?: boolean;
position?: Tooltip.Props['position'];
}
}
export class Truncate extends React.Component<Truncate.Props, {}> {
public render() {
const { text, position, copy } = this.props;
if (!text) {
return '-';
}
return (
<Tooltip
text={text}
position={position}
copy={copy}
className="Row-Tooltip"
>
<div className="Row-truncate">{text}</div>
</Tooltip>
);
}
public shouldComponentUpdate(nextProps: Truncate.Props): boolean {
return (
this.props.text !== nextProps.text ||
this.props.position !== nextProps.position
);
}
}
+2 -2
View File
@@ -1,5 +1,5 @@
export * from './Column';
export * from './List';
export * from './Truncate';
export * from './Row';
export * from './HeaderCell';
export * from './THeadCell';
export * from './THead';