mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-06-13 08:01:02 +00:00
Restructure the js app (#243)
* prettier * linter * add prettier, and format the code * remove common, merge it with frontend * refactor the app * better lint and code fix * travis for the frontend app * travis build script Signed-off-by: Daniel Maricic <daniel@woss.io> * lint and build * update the README.md Signed-off-by: Daniel Maricic <daniel@woss.io> * change the commands to reflect refactor Signed-off-by: Daniel Maricic <daniel@woss.io> * prettier and tslint are friends Signed-off-by: Daniel Maricic <daniel@woss.io> * code that wasn't linted properly before Signed-off-by: Daniel Maricic <daniel@woss.io> * prettier rc got deleted * workgin on making the travis pass Signed-off-by: Daniel Maricic <daniel@woss.io> * travis build please? Signed-off-by: Daniel Maricic <daniel@woss.io> * update readme.md Signed-off-by: Daniel Maricic <daniel@woss.io> * dockerfile deleted from fronted - out of scope Signed-off-by: Daniel Maricic <daniel@woss.io> * remove Signed-off-by: Daniel Maricic <daniel@woss.io> * tsconfig Signed-off-by: Daniel Maricic <daniel@woss.io> * found the reason why EOL wasn't happening Signed-off-by: Daniel Maricic <daniel@woss.io> * type for the event in the ConnectionInput as suggested * strictnullCheck to true * noImplicitAny * noUnusedParams * AfgHandling * update * fix Location.tsx * Few minor fixes * remove connection input and revert to original * esnext fixes the imports for icons and non default `* as ` * update to the tsconfig.test.json don't use commonjs please * fixed wrong comment for TIMEOUT_BASE * return totem.svg and type decraration of maybe Signed-off-by: Daniel Maricic <daniel@woss.io> Co-authored-by: Will <w.kopp@kigroup.de>
This commit is contained in:
@@ -0,0 +1,536 @@
|
||||
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 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 readIcon from '../../icons/arrow-up.svg';
|
||||
import writeIcon from '../../icons/arrow-down.svg';
|
||||
import databaseIcon from '../../icons/database.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 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 unknownImplementationIcon from '../../icons/question-solid.svg';
|
||||
|
||||
const ICONS = {
|
||||
'parity-polkadot': parityPolkadotIcon,
|
||||
'polkadot-js': polkadotJsIcon,
|
||||
'airalab-robonomics': airalabRobonomicsIcon,
|
||||
'substrate-node': paritySubstrateIcon,
|
||||
'edgeware-node': edgewareIcon,
|
||||
'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,
|
||||
'bifrost-node': bifrostIcon,
|
||||
'totem-meccano-node': totemIcon,
|
||||
Totem: totemIcon,
|
||||
};
|
||||
|
||||
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 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 DB_CACHE: Column = {
|
||||
label: 'Database Cache Size',
|
||||
icon: databaseIcon,
|
||||
width: 40,
|
||||
setting: 'dbCacheSize',
|
||||
sortBy: ({ dbCacheSize }) =>
|
||||
dbCacheSize.length < 3 ? 0 : dbCacheSize[dbCacheSize.length - 1],
|
||||
render: ({ dbCacheSize, chartstamps }) => {
|
||||
if (dbCacheSize.length < 3) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparkline
|
||||
width={44}
|
||||
height={16}
|
||||
stroke={1}
|
||||
format={formatBytes}
|
||||
values={dbCacheSize}
|
||||
stamps={chartstamps}
|
||||
minScale={MEMORY_SCALE}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DISK_READ: Column = {
|
||||
label: 'Disk Read',
|
||||
icon: readIcon,
|
||||
width: 40,
|
||||
setting: 'diskRead',
|
||||
sortBy: ({ diskRead }) =>
|
||||
diskRead.length < 3 ? 0 : diskRead[diskRead.length - 1],
|
||||
render: ({ diskRead, chartstamps }) => {
|
||||
if (diskRead.length < 3) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparkline
|
||||
width={44}
|
||||
height={16}
|
||||
stroke={1}
|
||||
format={formatBandwidth}
|
||||
values={diskRead}
|
||||
stamps={chartstamps}
|
||||
minScale={MEMORY_SCALE}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DISK_WRITE: Column = {
|
||||
label: 'Disk Write',
|
||||
icon: writeIcon,
|
||||
width: 40,
|
||||
setting: 'diskWrite',
|
||||
sortBy: ({ diskWrite }) =>
|
||||
diskWrite.length < 3 ? 0 : diskWrite[diskWrite.length - 1],
|
||||
render: ({ diskWrite, chartstamps }) => {
|
||||
if (diskWrite.length < 3) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
return (
|
||||
<Sparkline
|
||||
width={44}
|
||||
height={16}
|
||||
stroke={1}
|
||||
format={formatBandwidth}
|
||||
values={diskWrite}
|
||||
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,64 @@
|
||||
import * as React from 'react';
|
||||
import { Maybe } from '../../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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
.List-no-nodes {
|
||||
font-size: 30px;
|
||||
padding-top: 20vh;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.List table {
|
||||
width: 100%;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.List thead {
|
||||
background: #393838;
|
||||
}
|
||||
|
||||
.List tbody {
|
||||
font-family: monospace, sans-serif;
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
import * as React from 'react';
|
||||
import { Types, Maybe } from '../../common';
|
||||
import { Filter } from '../';
|
||||
import { State as AppState, Node } from '../../state';
|
||||
import { Row } from './';
|
||||
import { Persistent, PersistentSet } from '../../persist';
|
||||
import { viewport } from '../../utils';
|
||||
|
||||
const HEADER = 148;
|
||||
const TH_HEIGHT = 35;
|
||||
const TR_HEIGHT = 31;
|
||||
const ROW_MARGIN = 5;
|
||||
|
||||
import './List.css';
|
||||
|
||||
export namespace List {
|
||||
export interface Props {
|
||||
appState: Readonly<AppState>;
|
||||
pins: PersistentSet<Types.NodeName>;
|
||||
sortBy: Persistent<Maybe<number>>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
filter: Maybe<(node: Node) => boolean>;
|
||||
viewportHeight: number;
|
||||
listStart: number;
|
||||
listEnd: number;
|
||||
}
|
||||
}
|
||||
|
||||
export class List extends React.Component<List.Props, {}> {
|
||||
public state = {
|
||||
filter: null,
|
||||
viewportHeight: viewport().height,
|
||||
listStart: 0,
|
||||
listEnd: 0,
|
||||
};
|
||||
|
||||
private relativeTop = -1;
|
||||
private scrolling = false;
|
||||
|
||||
public componentDidMount() {
|
||||
this.onScroll();
|
||||
|
||||
window.addEventListener('resize', this.onResize);
|
||||
window.addEventListener('scroll', this.onScroll);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.onResize);
|
||||
window.removeEventListener('scroll', this.onScroll);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { selectedColumns } = this.props.appState;
|
||||
const { pins, sortBy } = this.props;
|
||||
const { filter } = this.state;
|
||||
|
||||
let nodes = this.props.appState.nodes.sorted();
|
||||
|
||||
if (filter != null) {
|
||||
nodes = nodes.filter(filter);
|
||||
|
||||
if (nodes.length === 0) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="List List-no-nodes">
|
||||
¯\_(ツ)_/¯
|
||||
<br />
|
||||
Nothing matches
|
||||
</div>
|
||||
<Filter onChange={this.onFilterChange} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const { listStart, listEnd } = this.state;
|
||||
|
||||
const height = TH_HEIGHT + nodes.length * TR_HEIGHT;
|
||||
const transform = `translateY(${listStart * TR_HEIGHT}px)`;
|
||||
|
||||
nodes = nodes.slice(listStart, listEnd);
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div className="List" style={{ height }}>
|
||||
<table>
|
||||
<Row.HEADER columns={selectedColumns} sortBy={sortBy} />
|
||||
<tbody style={{ transform }}>
|
||||
{nodes.map((node) => (
|
||||
<Row
|
||||
key={node.id}
|
||||
node={node}
|
||||
pins={pins}
|
||||
columns={selectedColumns}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<Filter onChange={this.onFilterChange} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
private onScroll = () => {
|
||||
if (this.scrolling) {
|
||||
return;
|
||||
}
|
||||
|
||||
const relativeTop = divisibleBy(
|
||||
window.scrollY - (HEADER + TR_HEIGHT),
|
||||
TR_HEIGHT * ROW_MARGIN
|
||||
);
|
||||
|
||||
if (this.relativeTop === relativeTop) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.relativeTop = relativeTop;
|
||||
this.scrolling = true;
|
||||
|
||||
window.requestAnimationFrame(this.onScrollRAF);
|
||||
};
|
||||
|
||||
private onScrollRAF = () => {
|
||||
const { relativeTop } = this;
|
||||
const { viewportHeight } = this.state;
|
||||
const top = Math.max(relativeTop, 0);
|
||||
const height =
|
||||
relativeTop < 0 ? viewportHeight + relativeTop : viewportHeight;
|
||||
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 });
|
||||
}
|
||||
|
||||
this.scrolling = false;
|
||||
};
|
||||
|
||||
private onResize = () => {
|
||||
const viewportHeight = viewport().height;
|
||||
|
||||
this.setState({ viewportHeight });
|
||||
};
|
||||
|
||||
private onFilterChange = (filter: Maybe<(node: Node) => boolean>) => {
|
||||
this.setState({ filter });
|
||||
};
|
||||
}
|
||||
|
||||
function divisibleBy(n: number, dividor: number): number {
|
||||
return n - (n % dividor);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
.Row {
|
||||
color: #b5aeae;
|
||||
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;
|
||||
}
|
||||
|
||||
.Row-pinned td:first-child {
|
||||
border-left: 3px solid #e6007a;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.Row-pinned td:last-child {
|
||||
border-right: 3px solid #e6007a;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.Row-pinned.Row-synced {
|
||||
color: #e6007a;
|
||||
}
|
||||
|
||||
.Row-stale {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.Row:hover {
|
||||
background-color: #1e1e1e;
|
||||
}
|
||||
|
||||
.Row-validator {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Row-validator:hover {
|
||||
transform: scale(2);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import * as React from 'react';
|
||||
import { Types, Maybe } from '../../common';
|
||||
import { Node } from '../../state';
|
||||
import { Persistent, PersistentSet } from '../../persist';
|
||||
import { HeaderCell, Column } from './';
|
||||
|
||||
import './Row.css';
|
||||
|
||||
export namespace Row {
|
||||
export interface Props {
|
||||
node: Node;
|
||||
pins: PersistentSet<Types.NodeName>;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
update: number;
|
||||
}
|
||||
}
|
||||
|
||||
interface HeaderProps {
|
||||
columns: Column[];
|
||||
sortBy: Persistent<Maybe<number>>;
|
||||
}
|
||||
|
||||
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.CPU,
|
||||
Column.MEM,
|
||||
Column.UPLOAD,
|
||||
Column.DOWNLOAD,
|
||||
Column.STATE_CACHE,
|
||||
Column.DB_CACHE,
|
||||
Column.DISK_READ,
|
||||
Column.DISK_WRITE,
|
||||
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, sortBy } = props;
|
||||
const last = columns.length - 1;
|
||||
|
||||
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 {
|
||||
return (
|
||||
this.props.node.id !== nextProps.node.id ||
|
||||
this.state.update !== nextState.update
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { node, columns } = this.props;
|
||||
|
||||
let className = 'Row';
|
||||
|
||||
if (node.propagationTime != null) {
|
||||
className += ' Row-synced';
|
||||
}
|
||||
|
||||
if (node.pinned) {
|
||||
className += ' Row-pinned';
|
||||
}
|
||||
|
||||
if (node.stale) {
|
||||
className += ' Row-stale';
|
||||
}
|
||||
|
||||
return (
|
||||
<tr className={className} onClick={this.toggle}>
|
||||
{columns.map(({ render }, index) => (
|
||||
<td key={index}>{render(node)}</td>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
public toggle = () => {
|
||||
const { pins, node } = this.props;
|
||||
|
||||
if (node.pinned) {
|
||||
pins.delete(node.name);
|
||||
} else {
|
||||
pins.add(node.name);
|
||||
}
|
||||
};
|
||||
|
||||
private onUpdate = () => {
|
||||
this.setState({ update: this.state.update + 1 });
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export * from './Column';
|
||||
export * from './List';
|
||||
export * from './Truncate';
|
||||
export * from './Row';
|
||||
export * from './HeaderCell';
|
||||
Reference in New Issue
Block a user