Sparklines (#65)

This commit is contained in:
Maciej Hirsz
2018-09-28 13:47:39 +02:00
committed by GitHub
parent 250e0db51e
commit f9078c6979
19 changed files with 351 additions and 34 deletions
@@ -252,6 +252,10 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
}
private onKeyPress = (event: KeyboardEvent) => {
if (event.ctrlKey) {
return;
}
const { filter } = this.state;
const key = event.key;
const code = event.keyCode;
@@ -12,7 +12,7 @@ export namespace Filter {
export class Filter extends React.Component<Filter.Props, {}> {
private filterInput: HTMLInputElement;
public componentDidMount(){
public componentDidMount() {
this.filterInput.focus();
}
@@ -23,6 +23,11 @@
text-overflow: ellipsis;
}
.Node-Row .Node-Row-Tooltip {
position: initial;
padding: inherit;
}
.Node-Row-synced {
color: #fff;
}
+69 -18
View File
@@ -5,7 +5,7 @@ import { formatNumber, milliOrSecond, secondsWithPrecision } from '../../utils';
import { State as AppState, Node } from '../../state';
import { PersistentSet } from '../../persist';
import { SEMVER_PATTERN } from './';
import { Ago, Icon } from '../';
import { Ago, Icon, Tooltip, Sparkline } from '../';
import nodeIcon from '../../icons/server.svg';
import nodeLocationIcon from '../../icons/location.svg';
@@ -45,10 +45,33 @@ interface Column {
render: (node: Node) => React.ReactElement<any> | string;
}
function Truncate(props: { text: string }): React.ReactElement<any> {
const { text } = props;
function Truncate(props: { text: string, position?: 'left' | 'right' | 'center' }): React.ReactElement<any> {
const { text, position } = props;
return <div className="Node-Row-truncate" title={text}>{text}</div>;
return (
<Tooltip text={text} position={position} className="Node-Row-Tooltip">
<div className="Node-Row-truncate">{text}</div>
</Tooltip>
);
}
function formatMemory(kbs: number): string {
const mbs = kbs / 1024 | 0;
if (mbs >= 1000) {
return `${(mbs / 1024).toFixed(1)} GB`;
} else {
return `${mbs} MB`;
}
}
function formatCPU(cpu: number): string {
const fractionDigits = cpu > 100 ? 0
: cpu > 10 ? 1
: cpu > 1 ? 2
: 3;
return `${cpu.toFixed(fractionDigits)}%`;
}
export default class Row extends React.Component<RowProps, {}> {
@@ -56,7 +79,7 @@ export default class Row extends React.Component<RowProps, {}> {
{
label: 'Node',
icon: nodeIcon,
render: ({ name }) => <Truncate text={name} />
render: ({ name }) => <Truncate text={name} position="left" />
},
{
label: 'Validator',
@@ -64,7 +87,7 @@ export default class Row extends React.Component<RowProps, {}> {
width: 26,
setting: 'validator',
render: ({ validator }) => {
return validator ? <span className="Node-Row-validator" title={validator}><Identicon id={validator} size={16} /></span> : '-';
return validator ? <Tooltip text={validator}><span className="Node-Row-validator"><Identicon id={validator} size={16} /></span></Tooltip> : '-';
}
},
{
@@ -72,7 +95,7 @@ export default class Row extends React.Component<RowProps, {}> {
icon: nodeLocationIcon,
width: 140,
setting: 'location',
render: ({ city }) => city ? <Truncate text={city} /> : '-'
render: ({ city }) => city ? <Truncate position="left" text={city} /> : '-'
},
{
label: 'Implementation',
@@ -85,7 +108,11 @@ export default class Row extends React.Component<RowProps, {}> {
: implementation === 'substrate-node' ? paritySubstrateIcon
: unknownImplementationIcon;
return <span title={`${implementation} v${version}`}><Icon src={implIcon} /> {semver}</span>;
return (
<Tooltip text={`${implementation} v${version}`}>
<Icon src={implIcon} /> {semver}
</Tooltip>
);
}
},
{
@@ -105,16 +132,32 @@ export default class Row extends React.Component<RowProps, {}> {
{
label: '% CPU Use',
icon: cpuIcon,
width: 26,
width: 40,
setting: 'cpu',
render: ({ cpu }) => cpu ? `${cpu.toFixed(1)}%` : '-'
render: ({ cpu }) => {
if (cpu.length < 3) {
return '-';
}
return (
<Sparkline width={48} height={16} stroke={1} format={formatCPU} values={cpu} />
);
}
},
{
label: 'Memory Use',
icon: memoryIcon,
width: 26,
width: 40,
setting: 'mem',
render: ({ mem }) => mem ? <span title={`${mem}kb`}>{mem / 1024 | 0}mb</span> : '-'
render: ({ mem }) => {
if (mem.length < 3) {
return '-';
}
return (
<Sparkline width={48} height={16} stroke={1} format={formatMemory} values={mem} />
);
}
},
{
label: 'Block',
@@ -128,7 +171,7 @@ export default class Row extends React.Component<RowProps, {}> {
icon: blockHashIcon,
width: 154,
setting: 'blockhash',
render: ({ hash }) => <Truncate text={hash} />
render: ({ hash }) => <Truncate position="right" text={hash} />
},
{
label: 'Block Time',
@@ -155,16 +198,24 @@ export default class Row extends React.Component<RowProps, {}> {
public static Header = (props: HeaderProps) => {
const { settings } = props;
const columns = Row.columns.filter(({ setting }) => setting == null || settings[setting]);
const last = columns.length - 1;
return (
<thead>
<tr className="Node-Row-Header">
{
Row.columns
.filter(({ setting }) => setting == null || settings[setting])
.map(({ icon, width, label }, index) => (
<th key={index} style={width ? { width } : undefined}><Icon src={icon} alt={label} /></th>
))
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>
)
})
}
</tr>
</thead>
@@ -0,0 +1,6 @@
.Sparkline {
fill: currentcolor; /* rgba(255,255,255,0.5); */
fill-opacity: 0.5;
stroke: currentcolor;
margin: 0 -14px 0 -4px;
}
@@ -0,0 +1,68 @@
import * as React from 'react';
import sparkline from "@fnando/sparkline";
import { Tooltip } from './';
import './Sparkline.css';
export namespace Sparkline {
export interface Props {
stroke: number;
width: number;
height: number;
values: number[];
format?: (value: number) => string;
}
}
export class Sparkline extends React.Component<Sparkline.Props, {}> {
private el: SVGSVGElement;
private update: Tooltip.UpdateCallback;
public componentDidMount() {
sparkline(this.el, this.props.values, {
interactive: true,
onmousemove: this.onMouseMove,
});
}
public shouldComponentUpdate(nextProps: Sparkline.Props): boolean {
const { stroke, width, height, format } = this.props;
if (stroke !== nextProps.stroke || width !== nextProps.width || height !== nextProps.height || format !== nextProps.format) {
return true;
}
if (this.props.values !== nextProps.values) {
sparkline(this.el, nextProps.values, {
interactive: true,
onmousemove: this.onMouseMove,
});
}
return false;
}
public render() {
const { stroke, width, height } = this.props;
return (
<Tooltip text="-" onInit={this.onTooltipInit}>
<svg className="Sparkline" ref={this.onRef} width={width} height={height} strokeWidth={stroke} />
</Tooltip>
);
}
private onRef = (el: SVGSVGElement) => {
this.el = el;
}
private onTooltipInit = (update: Tooltip.UpdateCallback) => {
this.update = update;
}
private onMouseMove = (event: MouseEvent, data: { value: number }) => {
const { format } = this.props;
const str = format ? format(data.value) : `${data.value}`;
this.update(str);
}
}
@@ -0,0 +1,83 @@
.Tooltip {
background: #000;
color: #fff;
font-family: Roboto, Helvetica, Arial, sans-serif;
font-size: 13px;
font-weight: 400;
padding: 3px 5px;
border-radius: 2px;
position: absolute;
white-space: nowrap;
top: -32px;
left: 50%;
transform: translateX(-50%);
display: none;
box-shadow: 0 2px 10px rgba(0,0,0,0.5);
}
.Tooltip::after {
content: ' ';
width: 0;
height: 0;
display: block;
position: absolute;
left: 50%;
bottom: -6px;
margin-left: -6px;
border-top: 6px #000 solid;
border-left: 6px transparent solid;
border-right: 6px transparent solid;
}
.Tooltip-left {
left: 10px;
transform: none;
}
.Tooltip-left::after {
left: 3px;
margin: 0;
}
.Tooltip-right {
left: initial;
right: 10px;
transform: none;
}
.Tooltip-right::after {
left: initial;
right: 3px;
margin: 0;
}
.Tooltip-container {
position: relative;
}
.Tooltip-container-inline {
display: inline-block;
}
.Tooltip-container-inline .Tooltip-left {
left: 0;
}
.Tooltip-container-inline .Tooltip-right {
right: 0;
}
.Tooltip-container:hover .Tooltip {
display: block;
animation: show 0.15s forwards;
}
@keyframes show {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@@ -0,0 +1,59 @@
import * as React from 'react';
import './Tooltip.css';
export namespace Tooltip {
export interface Props {
text: string;
inline?: boolean;
className?: string;
position?: 'left' | 'right' | 'center';
onInit?: (update: UpdateCallback) => void;
}
export type UpdateCallback = (text: string) => void;
}
export class Tooltip extends React.Component<Tooltip.Props, {}> {
private el: HTMLDivElement;
public componentDidMount() {
if (this.props.onInit) {
this.props.onInit(this.update);
}
}
public render() {
const { text, inline, className, position } = this.props;
let containerClass = 'Tooltip-container';
let tooltipClass = 'Tooltip';
if (className) {
containerClass += ' ' + className;
}
if (inline) {
containerClass += ' Tooltip-container-inline';
}
if (position && position !== 'center') {
tooltipClass += ` Tooltip-${position}`;
}
return (
<div className={containerClass}>
<div className={tooltipClass} ref={this.onRef}>{text}</div>
{this.props.children}
</div>
);
}
private onRef = (el: HTMLDivElement) => {
this.el = el;
}
private update = (text: string) => {
this.el.textContent = text;
}
}
@@ -5,6 +5,8 @@ export * from './Tile';
export * from './Ago';
export * from './OfflineIndicator';
export * from './Setting';
export * from './Sparkline';
export * from './Tooltip';
import * as Node from './Node';