import * as React from 'react'; import Identicon from 'polkadot-identicon'; import { Types, Maybe, timestamp } from '@dotstats/common'; import { formatNumber, milliOrSecond, secondsWithPrecision } from '../../utils'; import { State as AppState, Node } from '../../state'; import { PersistentSet } from '../../persist'; import { SEMVER_PATTERN } from './'; import { Ago, Icon, Tooltip, Sparkline } 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 peersIcon from '../../icons/broadcast.svg'; import transactionsIcon from '../../icons/inbox.svg'; import blockIcon from '../../icons/package.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 parityPolkadotIcon from '../../icons/dot.svg'; import paritySubstrateIcon from '../../icons/substrate.svg'; import unknownImplementationIcon from '../../icons/question-solid.svg'; import './Row.css'; interface RowProps { node: Node; settings: AppState.Settings; pins: PersistentSet; }; interface HeaderProps { settings: AppState.Settings; }; interface Column { label: string; icon: string; width?: number; setting?: keyof AppState.Settings; render: (node: Node) => React.ReactElement | string; } function Truncate(props: { text: string, position?: 'left' | 'right' | 'center' }): React.ReactElement { const { text, position } = props; return (
{text}
); } 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): 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 formatCPU(cpu: number, stamp: Maybe): string { const ago = stamp ? ` (${formatStamp(stamp)})` : ''; const fractionDigits = cpu > 100 ? 0 : cpu > 10 ? 1 : cpu > 1 ? 2 : 3; return `${cpu.toFixed(fractionDigits)}%${ago}`; } export default class Row extends React.Component { public static readonly columns: Column[] = [ { label: 'Node', icon: nodeIcon, render: ({ name }) => }, { label: 'Validator', icon: nodeValidatorIcon, width: 26, setting: 'validator', render: ({ validator }) => { return validator ? : '-'; } }, { label: 'Location', icon: nodeLocationIcon, width: 140, setting: 'location', render: ({ city }) => city ? : '-' }, { label: 'Implementation', icon: nodeTypeIcon, width: 90, setting: 'implementation', render: ({ implementation, version }) => { const [semver] = version.match(SEMVER_PATTERN) || [version]; const implIcon = implementation === 'parity-polkadot' ? parityPolkadotIcon : implementation === 'substrate-node' ? paritySubstrateIcon : unknownImplementationIcon; return ( {semver} ); } }, { 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 ( ); } }, { label: 'Memory Use', icon: memoryIcon, width: 40, setting: 'mem', render: ({ mem, chartstamps }) => { if (mem.length < 3) { return '-'; } return ( ); } }, { label: 'Block', icon: blockIcon, width: 88, setting: 'blocknumber', render: ({ height }) => `#${formatNumber(height)}` }, { label: 'Block Hash', icon: blockHashIcon, width: 154, setting: 'blockhash', render: ({ hash }) => }, { 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 }) => }, ]; public static Header = (props: HeaderProps) => { const { settings } = props; const columns = Row.columns.filter(({ setting }) => setting == null || settings[setting]); const last = columns.length - 1; return ( { columns.map(({ icon, width, label }, index) => { const position = index === 0 ? 'left' : index === last ? 'right' : 'center'; return ( ) }) } ) } public render() { const { node, settings } = this.props; let className = 'Node-Row'; if (node.propagationTime != null) { className += ' Node-Row-synced'; } if (node.pinned) { className += ' Node-Row-pinned'; } return ( { Row.columns .filter(({ setting }) => setting == null || settings[setting]) .map(({ render }, index) => {render(node)}) } ); } public toggle = () => { const { pins, node } = this.props; if (node.pinned) { pins.delete(node.name) } else { pins.add(node.name); } } }