mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-11 21:11:10 +00:00
Sort by column (#198)
* chore: Split HeaderCell out of Row.tsx * feat: toggle and highlight selected column header * feat: Fixed sorting, added stylized headers * fix: Performance and hardware column sorting * fix: Rebuild the sorted list when changing the comparator
This commit is contained in:
@@ -90,8 +90,7 @@ export namespace SortedCollection {
|
||||
}
|
||||
|
||||
export class SortedCollection<Item extends { id: number }> {
|
||||
private readonly compare: Compare<Item>;
|
||||
|
||||
private compare: Compare<Item>;
|
||||
private map = Array<Maybe<Item>>();
|
||||
private list = Array<Item>();
|
||||
private changeRef = 0;
|
||||
@@ -100,6 +99,13 @@ export class SortedCollection<Item extends { id: number }> {
|
||||
this.compare = compare;
|
||||
}
|
||||
|
||||
public setComparator(compare: Compare<Item>) {
|
||||
this.compare = compare;
|
||||
this.list = this.map.filter((item) => item != null) as Item[];
|
||||
this.list.sort(compare);
|
||||
this.changeRef += 1;
|
||||
}
|
||||
|
||||
public ref(): SortedCollection.StateRef {
|
||||
return this.changeRef as SortedCollection.StateRef;
|
||||
}
|
||||
@@ -110,6 +116,9 @@ export class SortedCollection<Item extends { id: number }> {
|
||||
this.map = this.map.concat(Array<Maybe<Item>>(Math.max(10, 1 + item.id - this.map.length)));
|
||||
}
|
||||
|
||||
// Remove old item if overriding
|
||||
this.remove(item.id);
|
||||
|
||||
this.map[item.id] = item;
|
||||
|
||||
sortedInsert(item, this.list, this.compare);
|
||||
@@ -169,6 +178,14 @@ export class SortedCollection<Item extends { id: number }> {
|
||||
}
|
||||
}
|
||||
|
||||
public mutAndMaybeSort(id: number, mutator: (item: Item) => void, sort: boolean) {
|
||||
if (sort) {
|
||||
this.mutAndSort(id, mutator);
|
||||
} else {
|
||||
this.mut(id, mutator);
|
||||
}
|
||||
}
|
||||
|
||||
public mutEach(mutator: (item: Item) => void) {
|
||||
this.list.forEach(mutator);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { Types, SortedCollection } from '@dotstats/common';
|
||||
import { Types, SortedCollection, Maybe, Compare } from '@dotstats/common';
|
||||
import { AllChains, Chains, Chain, Ago, OfflineIndicator } from './components';
|
||||
import { Row, Column } from './components/List';
|
||||
import { Connection } from './Connection';
|
||||
import { PersistentObject, PersistentSet } from './persist';
|
||||
import { Persistent, PersistentObject, PersistentSet } from './persist';
|
||||
import { State, Node, ChainData, PINNED_CHAIN } from './state';
|
||||
import { getHashData } from './utils';
|
||||
import stable from 'stable';
|
||||
@@ -14,6 +15,7 @@ export default class App extends React.Component<{}, State> {
|
||||
private chainsCache: ChainData[] = [];
|
||||
private readonly settings: PersistentObject<State.Settings>;
|
||||
private readonly pins: PersistentSet<Types.NodeName>;
|
||||
private readonly sortBy: Persistent<Maybe<number>>;
|
||||
private readonly connection: Promise<Connection>;
|
||||
|
||||
constructor(props: {}) {
|
||||
@@ -42,7 +44,12 @@ export default class App extends React.Component<{}, State> {
|
||||
uptime: false,
|
||||
networkstate: false,
|
||||
},
|
||||
(settings) => this.setState({ settings })
|
||||
(settings) => {
|
||||
const selectedColumns = this.selectedColumns(settings);
|
||||
|
||||
this.sortBy.set(null);
|
||||
this.setState({ settings, selectedColumns, sortBy: null })
|
||||
},
|
||||
);
|
||||
|
||||
this.pins = new PersistentSet<Types.NodeName>('pinned_names', (pins) => {
|
||||
@@ -53,6 +60,13 @@ export default class App extends React.Component<{}, State> {
|
||||
this.setState({ nodes, pins });
|
||||
});
|
||||
|
||||
this.sortBy = new Persistent<Maybe<number>>('sortBy', null, (sortBy) => {
|
||||
const compare = this.getComparator(sortBy);
|
||||
|
||||
this.state.nodes.setComparator(compare);
|
||||
this.setState({ sortBy });
|
||||
});
|
||||
|
||||
const { tab = '' } = getHashData();
|
||||
|
||||
this.state = {
|
||||
@@ -72,9 +86,13 @@ export default class App extends React.Component<{}, State> {
|
||||
nodes: new SortedCollection(Node.compare),
|
||||
settings: this.settings.raw(),
|
||||
pins: this.pins.get(),
|
||||
sortBy: this.sortBy.get(),
|
||||
selectedColumns: this.selectedColumns(this.settings.raw()),
|
||||
tab,
|
||||
};
|
||||
|
||||
this.state.nodes.setComparator(this.getComparator(this.sortBy.get()));
|
||||
|
||||
this.connection = Connection.create(this.pins, (changes) => {
|
||||
if (changes) {
|
||||
this.setState(changes);
|
||||
@@ -109,7 +127,7 @@ export default class App extends React.Component<{}, State> {
|
||||
<div className="App">
|
||||
<OfflineIndicator status={status} />
|
||||
<Chains chains={chains} subscribed={subscribed} connection={this.connection} />
|
||||
<Chain appState={this.state} connection={this.connection} settings={this.settings} pins={this.pins} />
|
||||
<Chain appState={this.state} connection={this.connection} settings={this.settings} pins={this.pins} sortBy={this.sortBy} />
|
||||
{overlay}
|
||||
</div>
|
||||
);
|
||||
@@ -181,4 +199,35 @@ export default class App extends React.Component<{}, State> {
|
||||
return this.chainsCache;
|
||||
}
|
||||
|
||||
private selectedColumns(settings: State.Settings): Column[] {
|
||||
return Row.columns.filter(({ setting }) => setting == null || settings[setting]);
|
||||
}
|
||||
|
||||
private getComparator(sortBy: Maybe<number>): Compare<Node> {
|
||||
const columns = this.state.selectedColumns;
|
||||
|
||||
if (sortBy != null) {
|
||||
const [index, rev] = sortBy < 0 ? [~sortBy, -1] : [sortBy, 1];
|
||||
const column = columns[index];
|
||||
|
||||
if (column != null && column.sortBy) {
|
||||
const key = column.sortBy;
|
||||
|
||||
return (a, b) => {
|
||||
const aKey = key(a);
|
||||
const bKey = key(b);
|
||||
|
||||
if (aKey < bKey) {
|
||||
return -1 * rev;
|
||||
} else if (aKey > bKey) {
|
||||
return 1 * rev;
|
||||
} else {
|
||||
return Node.compare(a, b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Node.compare;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { PersistentSet } from './persist';
|
||||
import { getHashData, setHashData } from './utils';
|
||||
import { AfgHandling } from './AfgHandling';
|
||||
import { VIS_AUTHORITIES_LIMIT } from '../../frontend/src/components/Consensus';
|
||||
import { Column } from './components/List';
|
||||
|
||||
const { Actions } = FeedMessage;
|
||||
|
||||
@@ -117,13 +118,19 @@ export class Connection {
|
||||
}
|
||||
|
||||
public handleMessages = (messages: FeedMessage.Message[]) => {
|
||||
const { nodes, chains } = this.state;
|
||||
const { nodes, chains, sortBy, selectedColumns } = this.state;
|
||||
const ref = nodes.ref();
|
||||
|
||||
const updateState: UpdateBound = (state) => { this.state = this.update(state); };
|
||||
const getState = () => this.state;
|
||||
const afg = new AfgHandling(updateState, getState);
|
||||
|
||||
let sortByColumn: Maybe<Column> = null;
|
||||
|
||||
if (sortBy != null) {
|
||||
sortByColumn = sortBy < 0 ? selectedColumns[~sortBy] : selectedColumns[sortBy];
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
switch (message.action) {
|
||||
case Actions.FeedVersion: {
|
||||
@@ -187,7 +194,7 @@ export class Connection {
|
||||
case Actions.LocatedNode: {
|
||||
const [id, lat, lon, city] = message.payload;
|
||||
|
||||
nodes.mut(id, (node) => node.updateLocation([lat, lon, city]));
|
||||
nodes.mutAndMaybeSort(id, (node) => node.updateLocation([lat, lon, city]), sortByColumn === Column.LOCATION);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -203,7 +210,11 @@ export class Connection {
|
||||
case Actions.FinalizedBlock: {
|
||||
const [id, height, hash] = message.payload;
|
||||
|
||||
nodes.mut(id, (node) => node.updateFinalized(height, hash));
|
||||
nodes.mutAndMaybeSort(
|
||||
id,
|
||||
(node) => node.updateFinalized(height, hash),
|
||||
sortByColumn === Column.FINALIZED || sortByColumn === Column.FINALIZED_HASH,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -211,7 +222,11 @@ export class Connection {
|
||||
case Actions.NodeStats: {
|
||||
const [id, nodeStats] = message.payload;
|
||||
|
||||
nodes.mut(id, (node) => node.updateStats(nodeStats));
|
||||
nodes.mutAndMaybeSort(
|
||||
id,
|
||||
(node) => node.updateStats(nodeStats),
|
||||
sortByColumn === Column.PEERS || sortByColumn === Column.TXS,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -219,7 +234,11 @@ export class Connection {
|
||||
case Actions.NodeHardware: {
|
||||
const [id, nodeHardware] = message.payload;
|
||||
|
||||
nodes.mut(id, (node) => node.updateHardware(nodeHardware));
|
||||
nodes.mutAndMaybeSort(
|
||||
id,
|
||||
(node) => node.updateHardware(nodeHardware),
|
||||
sortByColumn === Column.CPU || sortByColumn === Column.MEM || sortByColumn === Column.UPLOAD || sortByColumn === Column.DOWNLOAD,
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { Connection } from '../../Connection';
|
||||
import { Types } from '@dotstats/common';
|
||||
import { Types, Maybe } from '@dotstats/common';
|
||||
import { State as AppState } from '../../state';
|
||||
import { formatNumber, secondsWithPrecision, getHashData } from '../../utils';
|
||||
import { Tab } from './';
|
||||
import { Tile, Ago, List, Map, Settings, Consensus } from '../';
|
||||
import { PersistentObject, PersistentSet } from '../../persist';
|
||||
import { Persistent, PersistentObject, PersistentSet } from '../../persist';
|
||||
|
||||
import blockIcon from '../../icons/cube.svg';
|
||||
import finalizedIcon from '../../icons/cube-alt.svg';
|
||||
@@ -26,6 +26,7 @@ export namespace Chain {
|
||||
connection: Promise<Connection>;
|
||||
settings: PersistentObject<AppState.Settings>;
|
||||
pins: PersistentSet<Types.NodeName>;
|
||||
sortBy: Persistent<Maybe<number>>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@@ -91,7 +92,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
return <Settings settings={this.props.settings} />;
|
||||
}
|
||||
|
||||
const { appState, connection, pins } = this.props;
|
||||
const { appState, connection, pins, sortBy } = this.props;
|
||||
|
||||
if (display === 'consensus') {
|
||||
return <Consensus appState={appState} connection={connection} />;
|
||||
@@ -99,7 +100,7 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
|
||||
|
||||
return (
|
||||
display === 'list'
|
||||
? <List appState={appState} pins={pins} />
|
||||
? <List appState={appState} pins={pins} sortBy={sortBy} />
|
||||
: <Map appState={appState} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,351 @@
|
||||
import * as React from 'react';
|
||||
import { Types, Maybe, timestamp } from '@dotstats/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 cpuIcon from '../../icons/microchip-solid.svg';
|
||||
import memoryIcon from '../../icons/memory-solid.svg';
|
||||
import uploadIcon from '../../icons/cloud-upload.svg';
|
||||
import downloadIcon from '../../icons/cloud-download.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 darwiniaIcon from '../../icons/darwinia.svg';
|
||||
import turingIcon from '../../icons/turingnetwork.svg';
|
||||
import dothereumIcon from '../../icons/dothereum.svg';
|
||||
import katalchainIcon from '../../icons/katalchain.svg';
|
||||
import unknownImplementationIcon from '../../icons/question-solid.svg';
|
||||
|
||||
const ICONS = {
|
||||
'parity-polkadot': parityPolkadotIcon,
|
||||
'polkadot-js': polkadotJsIcon,
|
||||
'robonomics-node': airalabRobonomicsIcon,
|
||||
'substrate-node': paritySubstrateIcon,
|
||||
'edgeware-node': edgewareIcon,
|
||||
'joystream-node': joystreamIcon,
|
||||
'ChainX': chainXIcon,
|
||||
'ladder-node': ladderIcon,
|
||||
'cennznet-node': cennznetIcon,
|
||||
'Darwinia': darwiniaIcon,
|
||||
'Darwinia Testnet': darwiniaIcon,
|
||||
'turing-node': turingIcon,
|
||||
'dothereum': dothereumIcon,
|
||||
'katalchain': katalchainIcon,
|
||||
};
|
||||
|
||||
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 CPU: Column = {
|
||||
label: '% CPU Use',
|
||||
icon: cpuIcon,
|
||||
width: 40,
|
||||
setting: 'cpu',
|
||||
sortBy: ({ cpu }) => cpu.length < 3 ? 0 : cpu[cpu.length - 1],
|
||||
render: ({ cpu, chartstamps }) => {
|
||||
if (cpu.length < 3) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparkline width={44} height={16} stroke={1} format={formatCPU} values={cpu} stamps={chartstamps} minScale={100} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const MEM: Column = {
|
||||
label: 'Memory Use',
|
||||
icon: memoryIcon,
|
||||
width: 40,
|
||||
setting: 'mem',
|
||||
sortBy: ({ mem }) => mem.length < 3 ? 0 : mem[mem.length - 1],
|
||||
render: ({ mem, chartstamps }) => {
|
||||
if (mem.length < 3) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparkline width={44} height={16} stroke={1} format={formatMemory} values={mem} stamps={chartstamps} minScale={MEMORY_SCALE} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
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 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 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,54 @@
|
||||
import * as React from 'react';
|
||||
import { Maybe } from '@dotstats/common';
|
||||
import { Column } from './';
|
||||
import { Icon, Tooltip } from '../';
|
||||
import { Persistent } from '../../persist';
|
||||
|
||||
import sortAscIcon from '../../icons/triangle-up.svg';
|
||||
import sortDescIcon from '../../icons/triangle-down.svg';
|
||||
|
||||
export namespace HeaderCell {
|
||||
export interface Props {
|
||||
column: Column;
|
||||
index: number;
|
||||
last: number;
|
||||
sortBy: Persistent<Maybe<number>>;
|
||||
}
|
||||
}
|
||||
|
||||
export class HeaderCell extends React.Component<HeaderCell.Props, {}> {
|
||||
public render() {
|
||||
const { column, index, last } = this.props;
|
||||
const { icon, width, label } = column;
|
||||
const position = index === 0 ? 'left'
|
||||
: index === last ? 'right'
|
||||
: 'center';
|
||||
|
||||
const sortBy = this.props.sortBy.get();
|
||||
const className = column.sortBy == null ? '' : sortBy === index || sortBy === ~index ? 'HeaderCell-sorted' : 'HeaderCell-sortable';
|
||||
const i = sortBy === index ? sortAscIcon : sortBy === ~index ? sortDescIcon : icon;
|
||||
|
||||
return (
|
||||
<th className={className} style={width ? { width } : undefined} onClick={this.toggleSort}>
|
||||
<Tooltip text={label} inline={true} position={position}><Icon src={i} /></Tooltip>
|
||||
</th>
|
||||
)
|
||||
}
|
||||
|
||||
private toggleSort = () => {
|
||||
const { index, sortBy, column } = this.props;
|
||||
const sortByRaw = sortBy.get();
|
||||
|
||||
if (column.sortBy == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sortByRaw === index) {
|
||||
sortBy.set(~index);
|
||||
} else if (sortByRaw === ~index) {
|
||||
sortBy.set(null);
|
||||
} else {
|
||||
sortBy.set(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { Types, Maybe } from '@dotstats/common';
|
||||
import { Filter } from '../';
|
||||
import { State as AppState, Node } from '../../state';
|
||||
import { Row } from './';
|
||||
import { PersistentSet } from '../../persist';
|
||||
import { Persistent, PersistentSet } from '../../persist';
|
||||
import { viewport } from '../../utils'
|
||||
|
||||
const HEADER = 148;
|
||||
@@ -17,6 +17,7 @@ export namespace List {
|
||||
export interface Props {
|
||||
appState: Readonly<AppState>;
|
||||
pins: PersistentSet<Types.NodeName>;
|
||||
sortBy: Persistent<Maybe<number>>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@@ -51,10 +52,9 @@ export class List extends React.Component<List.Props, {}> {
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { settings } = this.props.appState;
|
||||
const { pins } = this.props;
|
||||
const { selectedColumns } = this.props.appState;
|
||||
const { pins, sortBy } = this.props;
|
||||
const { filter } = this.state;
|
||||
const columns = Row.columns.filter(({ setting }) => setting == null || settings[setting]);
|
||||
|
||||
let nodes = this.props.appState.nodes.sorted();
|
||||
|
||||
@@ -82,10 +82,10 @@ export class List extends React.Component<List.Props, {}> {
|
||||
<React.Fragment>
|
||||
<div className="List" style={{ height }}>
|
||||
<table>
|
||||
<Row.Header columns={columns} />
|
||||
<Row.Header columns={selectedColumns} sortBy={sortBy} />
|
||||
<tbody style={{ transform }}>
|
||||
{
|
||||
nodes.map((node) => <Row key={node.id} node={node} pins={pins} columns={columns} />)
|
||||
nodes.map((node) => <Row key={node.id} node={node} pins={pins} columns={selectedColumns} />)
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -26,6 +26,16 @@
|
||||
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;
|
||||
|
||||
@@ -1,69 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import { Types, Maybe, timestamp } from '@dotstats/common';
|
||||
import { formatNumber, getHashData, milliOrSecond, secondsWithPrecision } from '../../utils';
|
||||
import { State as AppState, Node } from '../../state';
|
||||
import { PersistentSet } from '../../persist';
|
||||
import { Truncate } from './';
|
||||
import { Ago, Icon, Tooltip, Sparkline, PolkadotIcon } from '../';
|
||||
|
||||
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 cpuIcon from '../../icons/microchip-solid.svg';
|
||||
import memoryIcon from '../../icons/memory-solid.svg';
|
||||
import uploadIcon from '../../icons/cloud-upload.svg';
|
||||
import downloadIcon from '../../icons/cloud-download.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 darwiniaIcon from '../../icons/darwinia.svg';
|
||||
import turingIcon from '../../icons/turingnetwork.svg';
|
||||
import dothereumIcon from '../../icons/dothereum.svg';
|
||||
import katalchainIcon from '../../icons/katalchain.svg';
|
||||
import unknownImplementationIcon from '../../icons/question-solid.svg';
|
||||
import { Types, Maybe } from '@dotstats/common';
|
||||
import { Node } from '../../state';
|
||||
import { Persistent, PersistentSet } from '../../persist';
|
||||
import { HeaderCell, Column } from './';
|
||||
|
||||
import './Row.css';
|
||||
|
||||
const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
|
||||
const BANDWIDTH_SCALE = 1024 * 1024;
|
||||
const MEMORY_SCALE = 2 * 1024 * 1024;
|
||||
const ICONS = {
|
||||
'parity-polkadot': parityPolkadotIcon,
|
||||
'polkadot-js': polkadotJsIcon,
|
||||
'robonomics-node': airalabRobonomicsIcon,
|
||||
'substrate-node': paritySubstrateIcon,
|
||||
'edgeware-node': edgewareIcon,
|
||||
'joystream-node': joystreamIcon,
|
||||
'ChainX': chainXIcon,
|
||||
'ladder-node': ladderIcon,
|
||||
'cennznet-node': cennznetIcon,
|
||||
'Darwinia': darwiniaIcon,
|
||||
'Darwinia Testnet': darwiniaIcon,
|
||||
'turing-node': turingIcon,
|
||||
'dothereum': dothereumIcon,
|
||||
'katalchain': katalchainIcon,
|
||||
};
|
||||
|
||||
export namespace Row {
|
||||
export interface Props {
|
||||
node: Node;
|
||||
@@ -78,278 +20,44 @@ export namespace Row {
|
||||
|
||||
interface HeaderProps {
|
||||
columns: Column[];
|
||||
sortBy: Persistent<Maybe<number>>;
|
||||
}
|
||||
|
||||
interface Column {
|
||||
label: string;
|
||||
icon: string;
|
||||
width?: number;
|
||||
setting?: keyof AppState.Settings;
|
||||
render: (node: Node) => React.ReactElement<any> | string;
|
||||
}
|
||||
|
||||
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 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}`;
|
||||
}
|
||||
|
||||
const URI_BASE = window.location.protocol === 'https:'
|
||||
? `/network_state/`
|
||||
: `http://${window.location.hostname}:8000/network_state/`;
|
||||
|
||||
export class Row extends React.Component<Row.Props, Row.State> {
|
||||
public static readonly columns: Column[] = [
|
||||
{
|
||||
label: 'Node',
|
||||
icon: nodeIcon,
|
||||
render: ({ name }) => <Truncate text={name} position="left" />
|
||||
},
|
||||
{
|
||||
label: 'Validator',
|
||||
icon: nodeValidatorIcon,
|
||||
width: 16,
|
||||
setting: 'validator',
|
||||
render: ({ validator }) => {
|
||||
return validator ? <Tooltip text={validator} copy={true}><span className="Row-validator"><PolkadotIcon account={validator} size={16} /></span></Tooltip> : '-';
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Location',
|
||||
icon: nodeLocationIcon,
|
||||
width: 140,
|
||||
setting: 'location',
|
||||
render: ({ city }) => city ? <Truncate position="left" text={city} /> : '-'
|
||||
},
|
||||
{
|
||||
label: 'Implementation',
|
||||
icon: nodeTypeIcon,
|
||||
width: 90,
|
||||
setting: 'implementation',
|
||||
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>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Network ID',
|
||||
icon: networkIdIcon,
|
||||
width: 90,
|
||||
setting: 'networkId',
|
||||
render: ({ networkId }) => networkId ? <Truncate position="left" text={networkId} /> : '-'
|
||||
},
|
||||
{
|
||||
label: 'Peer Count',
|
||||
icon: peersIcon,
|
||||
width: 26,
|
||||
setting: 'peers',
|
||||
render: ({ peers }) => `${peers}`
|
||||
},
|
||||
{
|
||||
label: 'Transactions in Queue',
|
||||
icon: transactionsIcon,
|
||||
width: 26,
|
||||
setting: 'txs',
|
||||
render: ({ txs }) => `${txs}`
|
||||
},
|
||||
{
|
||||
label: '% CPU Use',
|
||||
icon: cpuIcon,
|
||||
width: 40,
|
||||
setting: 'cpu',
|
||||
render: ({ cpu, chartstamps }) => {
|
||||
if (cpu.length < 3) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparkline width={44} height={16} stroke={1} format={formatCPU} values={cpu} stamps={chartstamps} minScale={100} />
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Memory Use',
|
||||
icon: memoryIcon,
|
||||
width: 40,
|
||||
setting: 'mem',
|
||||
render: ({ mem, chartstamps }) => {
|
||||
if (mem.length < 3) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparkline width={44} height={16} stroke={1} format={formatMemory} values={mem} stamps={chartstamps} minScale={MEMORY_SCALE} />
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Upload Bandwidth',
|
||||
icon: uploadIcon,
|
||||
width: 40,
|
||||
setting: 'upload',
|
||||
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} />
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Download Bandwidth',
|
||||
icon: downloadIcon,
|
||||
width: 40,
|
||||
setting: 'download',
|
||||
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} />
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Block',
|
||||
icon: blockIcon,
|
||||
width: 88,
|
||||
setting: 'blocknumber',
|
||||
render: ({ height }) => `#${formatNumber(height)}`
|
||||
},
|
||||
{
|
||||
label: 'Block Hash',
|
||||
icon: blockHashIcon,
|
||||
width: 154,
|
||||
setting: 'blockhash',
|
||||
render: ({ hash }) => <Truncate position="right" text={hash} copy={true} />
|
||||
},
|
||||
{
|
||||
label: 'Finalized Block',
|
||||
icon: finalizedIcon,
|
||||
width: 88,
|
||||
setting: 'finalized',
|
||||
render: ({ finalized }) => `#${formatNumber(finalized)}`
|
||||
},
|
||||
{
|
||||
label: 'Finalized Block Hash',
|
||||
icon: blockHashIcon,
|
||||
width: 154,
|
||||
setting: 'finalizedhash',
|
||||
render: ({ finalizedHash }) => <Truncate position="right" text={finalizedHash} copy={true} />
|
||||
},
|
||||
{
|
||||
label: 'Block Time',
|
||||
icon: blockTimeIcon,
|
||||
width: 80,
|
||||
setting: 'blocktime',
|
||||
render: ({ blockTime }) => `${secondsWithPrecision(blockTime/1000)}`
|
||||
},
|
||||
{
|
||||
label: 'Block Propagation Time',
|
||||
icon: propagationTimeIcon,
|
||||
width: 58,
|
||||
setting: 'blockpropagation',
|
||||
render: ({ propagationTime }) => propagationTime == null ? '∞' : milliOrSecond(propagationTime)
|
||||
},
|
||||
{
|
||||
label: 'Last Block Time',
|
||||
icon: lastTimeIcon,
|
||||
width: 100,
|
||||
setting: 'blocklasttime',
|
||||
render: ({ blockTimestamp }) => <Ago when={blockTimestamp} />
|
||||
},
|
||||
{
|
||||
label: 'Node Uptime',
|
||||
icon: uptimeIcon,
|
||||
width: 58,
|
||||
setting: 'uptime',
|
||||
render: ({ connectedAt }) => <Ago when={connectedAt} justTime={true} />
|
||||
},
|
||||
{
|
||||
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>;
|
||||
},
|
||||
},
|
||||
Column.NAME,
|
||||
Column.VALIDATOR,
|
||||
Column.LOCATION,
|
||||
Column.IMPLEMENTATION,
|
||||
Column.NETWORK_ID,
|
||||
Column.PEERS,
|
||||
Column.TXS,
|
||||
Column.CPU,
|
||||
Column.MEM,
|
||||
Column.UPLOAD,
|
||||
Column.DOWNLOAD,
|
||||
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,
|
||||
];
|
||||
|
||||
public static Header = (props: HeaderProps) => {
|
||||
const { columns } = props;
|
||||
const { columns, sortBy } = props;
|
||||
const last = columns.length - 1;
|
||||
|
||||
return (
|
||||
<thead>
|
||||
<tr className="Row-Header">
|
||||
{
|
||||
columns.map(({ icon, width, label }, index) => {
|
||||
const position = index === 0 ? 'left'
|
||||
: index === last ? 'right'
|
||||
: 'center';
|
||||
|
||||
return (
|
||||
<th key={index} style={width ? { width } : undefined}>
|
||||
<Tooltip text={label} inline={true} position={position}><Icon src={icon} /></Tooltip>
|
||||
</th>
|
||||
)
|
||||
})
|
||||
columns.map((col, index) => (
|
||||
<HeaderCell key={index} column={col} index={index} last={last} sortBy={sortBy} />
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export * from './Column';
|
||||
export * from './List';
|
||||
export * from './Truncate';
|
||||
export * from './Row';
|
||||
export * from './HeaderCell';
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Types, Maybe, SortedCollection } from '@dotstats/common';
|
||||
import { Column } from './components/List';
|
||||
|
||||
export const PINNED_CHAIN = 'Kusama CC2';
|
||||
|
||||
@@ -31,6 +32,9 @@ export class Node {
|
||||
public readonly networkId: Maybe<Types.NetworkId>;
|
||||
public readonly connectedAt: Types.Timestamp;
|
||||
|
||||
public readonly sortableName: string;
|
||||
public readonly sortableVersion: number;
|
||||
|
||||
public stale: boolean;
|
||||
public pinned: boolean;
|
||||
public peers: Types.PeerCount;
|
||||
@@ -79,6 +83,11 @@ export class Node {
|
||||
this.networkId = networkId;
|
||||
this.connectedAt = connectedAt;
|
||||
|
||||
const [major = 0, minor = 0, patch = 0] = (version || '0.0.0').split('.').map((n) => parseInt(n, 10) | 0);
|
||||
|
||||
this.sortableName = name.toLocaleLowerCase();
|
||||
this.sortableVersion = (major * 1000 + minor * 100 + patch) | 0;
|
||||
|
||||
this.updateStats(nodeStats);
|
||||
this.updateHardware(nodeHardware);
|
||||
this.updateBlock(blockDetails);
|
||||
@@ -203,6 +212,11 @@ export namespace State {
|
||||
uptime: boolean;
|
||||
networkstate: boolean;
|
||||
}
|
||||
|
||||
export interface SortBy {
|
||||
column: string;
|
||||
reverse: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export interface State {
|
||||
@@ -223,6 +237,8 @@ export interface State {
|
||||
nodes: SortedCollection<Node>;
|
||||
settings: Readonly<State.Settings>;
|
||||
pins: Readonly<Set<Types.NodeName>>;
|
||||
sortBy: Readonly<Maybe<number>>;
|
||||
selectedColumns: Column[];
|
||||
}
|
||||
|
||||
export type Update = <K extends keyof State>(changes: Pick<State, K> | null) => Readonly<State>;
|
||||
|
||||
Reference in New Issue
Block a user