New List, Map, and Settings components (#86)

This commit is contained in:
Maciej Hirsz
2018-10-12 17:34:49 +02:00
committed by GitHub
parent 2ed9061d23
commit 97b3d1ec23
20 changed files with 405 additions and 289 deletions
@@ -32,53 +32,3 @@
color: #fff; color: #fff;
box-shadow: rgba(0,0,0,0.5) 0 3px 30px; box-shadow: rgba(0,0,0,0.5) 0 3px 30px;
} }
.Chain-map {
min-width: 1350px;
background: url('../../assets/world-map.svg') no-repeat;
background-size: contain;
background-position: center;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.Chain-no-nodes {
font-size: 30px;
padding-top: 20vh;
text-align: center;
font-weight: 300;
}
.Chain-node-list {
width: 100%;
border-spacing: 0;
}
.Chain-node-list thead {
background: #3c3c3b;
}
.Chain-node-list tbody {
font-family: monospace, sans-serif;
}
.Chain-settings {
text-align: center;
}
.Chain-settings-category {
text-align: left;
width: 500px;
margin: 0 auto;
padding: 2em 0;
}
.Chain-settings-category h2 {
padding: 0;
margin: 0 0 0.5em 0;
font-size: 20px;
font-weight: 100;
}
+18 -161
View File
@@ -1,9 +1,9 @@
import * as React from 'react'; import * as React from 'react';
import { Types, Maybe } from '@dotstats/common'; import { Types, Maybe } from '@dotstats/common';
import { State as AppState, Node as NodeState } from '../../state'; import { State as AppState, Node as NodeState } from '../../state';
import { formatNumber, secondsWithPrecision, viewport, getHashData } from '../../utils'; import { formatNumber, secondsWithPrecision, getHashData } from '../../utils';
import { Tab, Filter } from './'; import { Tab, Filter } from './';
import { Tile, Node, Ago, Setting } from '../'; import { Tile, Ago, List, Map, Settings } from '../';
import { PersistentObject, PersistentSet } from '../../persist'; import { PersistentObject, PersistentSet } from '../../persist';
import blockIcon from '../../icons/package.svg'; import blockIcon from '../../icons/package.svg';
@@ -13,12 +13,6 @@ import listIcon from '../../icons/list-alt-regular.svg';
import worldIcon from '../../icons/location.svg'; import worldIcon from '../../icons/location.svg';
import settingsIcon from '../../icons/settings.svg'; import settingsIcon from '../../icons/settings.svg';
const MAP_RATIO = 800 / 350;
const MAP_HEIGHT_ADJUST = 400 / 350;
const HEADER = 148;
const TH_HEIGHT = 35;
const TR_HEIGHT = 31;
const ESCAPE_KEY = 27; const ESCAPE_KEY = 27;
import './Chain.css'; import './Chain.css';
@@ -35,12 +29,6 @@ export namespace Chain {
export interface State { export interface State {
display: Display; display: Display;
filter: Maybe<string>; filter: Maybe<string>;
map: {
width: number;
height: number;
top: number;
left: number;
}
} }
} }
@@ -62,30 +50,21 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
this.state = { this.state = {
display, display,
filter: null, filter: null,
map: {
width: 0,
height: 0,
top: 0,
left: 0
}
}; };
} }
public componentWillMount() { public componentWillMount() {
this.onResize();
window.addEventListener('resize', this.onResize);
window.addEventListener('keyup', this.onKeyUp); window.addEventListener('keyup', this.onKeyUp);
} }
public componentWillUnmount() { public componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
window.removeEventListener('keyup', this.onKeyUp); window.removeEventListener('keyup', this.onKeyUp);
} }
public render() { public render() {
const { best, blockTimestamp, blockAverage } = this.props.appState; const { appState } = this.props;
const currentTab = this.state.display; const { best, blockTimestamp, blockAverage } = appState;
const { display: currentTab } = this.state;
return ( return (
<div className="Chain"> <div className="Chain">
@@ -101,159 +80,37 @@ export class Chain extends React.Component<Chain.Props, Chain.State> {
</div> </div>
<div className="Chain-content-container"> <div className="Chain-content-container">
<div className="Chain-content"> <div className="Chain-content">
{ {this.renderContent()}
currentTab === 'list'
? this.renderList()
: currentTab === 'map'
? this.renderMap()
: this.renderSettings()
}
</div> </div>
</div> </div>
</div> </div>
); );
} }
private setDisplay = (display: Chain.Display) => { private renderContent() {
this.setState({ display }); const { display, filter } = this.state;
};
private renderList() { if (display === 'settings') {
const { settings } = this.props.appState; return <Settings settings={this.props.settings} />;
const { pins } = this.props;
const { filter } = this.state;
const nodeFilter = this.getNodeFilter();
const nodes = nodeFilter ? this.nodes().filter(nodeFilter) : this.nodes();
const columns = Node.Row.columns.filter(({ setting }) => setting == null || settings[setting]);
if (nodeFilter && nodes.length === 0) {
return (
<React.Fragment>
<Filter value={filter} onChange={this.onFilterChange} />
<div className="Chain-no-nodes">¯\_()_/¯<br />Nothing matches</div>
</React.Fragment>
);
} }
const height = TH_HEIGHT + nodes.length * TR_HEIGHT; const { appState, pins } = this.props;
return (
<div style={{ height }}>
<Filter value={filter} onChange={this.onFilterChange} />
<table className="Chain-node-list">
<Node.Row.Header columns={columns} />
<tbody>
{
nodes.map((node) => <Node.Row key={node.id} node={node} pins={pins} columns={columns} />)
}
</tbody>
</table>
</div>
);
}
private renderMap() {
const { filter } = this.state;
const nodeFilter = this.getNodeFilter();
return ( return (
<React.Fragment> <React.Fragment>
{filter != null ? <Filter value={filter} onChange={this.onFilterChange} /> : null} <Filter value={filter} onChange={this.onFilterChange} />
<div className="Chain-map">
{ {
this.nodes().map((node) => { display === 'list'
const { lat, lon } = node; ? <List filter={this.getNodeFilter()} appState={appState} pins={pins} />
const focused = nodeFilter == null || nodeFilter(node); : <Map filter={this.getNodeFilter()} appState={appState} />
if (lat == null || lon == null) {
// Skip nodes with unknown location
return null;
}
const position = this.pixelPosition(lat, lon);
return (
<Node.Location key={node.id} position={position} focused={focused} node={node} />
);
})
} }
</div>
</React.Fragment> </React.Fragment>
); );
} }
private renderSettings() { private setDisplay = (display: Chain.Display) => {
const { settings } = this.props; this.setState({ display });
};
return (
<div className="Chain-settings">
<div className="Chain-settings-category">
<h2>Visible Columns</h2>
{
Node.Row.columns
.map(({ label, icon, setting }, index) => {
if (!setting) {
return null;
}
return <Setting key={index} setting={setting} settings={settings} icon={icon} label={label} />;
})
}
</div>
</div>
);
}
private nodes(): NodeState[] {
return this.props.appState.nodes.sorted();
}
private pixelPosition(lat: Types.Latitude, lon: Types.Longitude): Node.Location.Position {
const { map } = this.state;
// Longitude ranges -180 (west) to +180 (east)
// Latitude ranges +90 (north) to -90 (south)
const left = Math.round(((180 + lon) / 360) * map.width + map.left);
const top = Math.round(((90 - lat) / 180) * map.height + map.top) * MAP_HEIGHT_ADJUST;
let quarter: Node.Location.Quarter = 0;
if (lon > 0) {
quarter = (quarter | 1) as Node.Location.Quarter;
}
if (lat < 0) {
quarter = (quarter | 2) as Node.Location.Quarter;
}
return { left, top, quarter };
}
private onResize: () => void = () => {
const vp = viewport();
vp.width = Math.max(1350, vp.width);
vp.height -= HEADER;
const ratio = vp.width / vp.height;
let top = 0;
let left = 0;
let width = 0;
let height = 0;
if (ratio >= MAP_RATIO) {
width = Math.round(vp.height * MAP_RATIO);
height = Math.round(vp.height);
left = (vp.width - width) / 2;
} else {
width = Math.round(vp.width);
height = Math.round(vp.width / MAP_RATIO);
top = (vp.height - height) / 2;
}
this.setState({ map: { top, left, width, height }});
}
private onKeyUp = (event: KeyboardEvent) => { private onKeyUp = (event: KeyboardEvent) => {
if (event.ctrlKey) { if (event.ctrlKey) {
@@ -0,0 +1,66 @@
.Chain-header {
width: 100%;
height: 108px;
overflow: hidden;
background: #fff;
color: #000;
min-width: 1350px;
position: relative;
}
.Chain-tabs {
position: absolute;
right: 5px;
bottom: 10px;
width: 200px;
text-align: right;
}
.Chain-content-container {
position: absolute;
left: 0;
right: 0;
bottom: 0;
top: 148px;
}
.Chain-content {
width: 100%;
min-width: 1350px;
min-height: 100%;
background: #222;
color: #fff;
box-shadow: rgba(0,0,0,0.5) 0 3px 30px;
}
.Chain-map {
min-width: 1350px;
background: url('../../assets/world-map.svg') no-repeat;
background-size: contain;
background-position: center;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.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: #3c3c3b;
}
.List tbody {
font-family: monospace, sans-serif;
}
@@ -0,0 +1,54 @@
import * as React from 'react';
import { Types, Maybe } from '@dotstats/common';
import { State as AppState, Node } from '../../state';
import { Row } from './';
import { PersistentSet } from '../../persist';
// const HEADER = 148;
const TH_HEIGHT = 35;
const TR_HEIGHT = 31;
import './List.css';
export namespace List {
export interface Props {
filter: Maybe<(node: Node) => boolean>;
appState: Readonly<AppState>;
pins: PersistentSet<Types.NodeName>;
}
}
export class List extends React.Component<List.Props, {}> {
public render() {
const { settings } = this.props.appState;
const { pins, filter } = this.props;
const columns = Row.columns.filter(({ setting }) => setting == null || settings[setting]);
let nodes = this.props.appState.nodes.sorted();
if (filter != null) {
nodes = nodes.filter(filter);
if (nodes.length === 0) {
return (
<div className="List List-no-nodes">¯\_()_/¯<br />Nothing matches</div>
);
}
}
const height = TH_HEIGHT + nodes.length * TR_HEIGHT;
return (
<div className="List" style={{ height }}>
<table>
<Row.Header columns={columns} />
<tbody>
{
nodes.map((node) => <Row key={node.id} node={node} pins={pins} columns={columns} />)
}
</tbody>
</table>
</div>
);
}
}
@@ -1,23 +1,23 @@
.Node-Row { .Row {
color: #999; color: #999;
cursor: pointer; cursor: pointer;
} }
.Node-Row-Header th, .Node-Row td { .Row-Header th, .Row td {
text-align: left; text-align: left;
padding: 6px 13px; padding: 6px 13px;
height: 19px; height: 19px;
} }
.Node-Row td { .Row td {
position: relative; position: relative;
} }
.Node-Row-Header th { .Row-Header th {
height: 23px; height: 23px;
} }
.Node-Row .Node-Row-truncate { .Row .Row-truncate {
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;
@@ -28,40 +28,40 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.Node-Row .Node-Row-Tooltip { .Row .Row-Tooltip {
position: initial; position: initial;
padding: inherit; padding: inherit;
} }
.Node-Row-synced { .Row-synced {
color: #fff; color: #fff;
} }
.Node-Row-pinned td:first-child { .Row-pinned td:first-child {
border-left: 3px solid #d64ca8; border-left: 3px solid #d64ca8;
padding-left: 10px; padding-left: 10px;
} }
.Node-Row-pinned td:last-child { .Row-pinned td:last-child {
border-right: 3px solid #d64ca8; border-right: 3px solid #d64ca8;
padding-right: 10px; padding-right: 10px;
} }
.Node-Row-pinned.Node-Row-synced { .Row-pinned.Row-synced {
color: #d64ca8; color: #d64ca8;
} }
.Node-Row:hover { .Row:hover {
background-color: #161616; background-color: #161616;
} }
.Node-Row-validator { .Row-validator {
display: block; display: block;
width: 16px; width: 16px;
height: 16px; height: 16px;
cursor: pointer; cursor: pointer;
} }
.Node-Row-validator:hover { .Row-validator:hover {
transform: scale(2); transform: scale(2);
} }
@@ -4,7 +4,7 @@ import { Types, Maybe, timestamp } from '@dotstats/common';
import { formatNumber, milliOrSecond, secondsWithPrecision } from '../../utils'; import { formatNumber, milliOrSecond, secondsWithPrecision } from '../../utils';
import { State as AppState, Node } from '../../state'; import { State as AppState, Node } from '../../state';
import { PersistentSet } from '../../persist'; import { PersistentSet } from '../../persist';
import { SEMVER_PATTERN, Truncate } from './'; import { Truncate } from './';
import { Ago, Icon, Tooltip, Sparkline } from '../'; import { Ago, Icon, Tooltip, Sparkline } from '../';
import nodeIcon from '../../icons/server.svg'; import nodeIcon from '../../icons/server.svg';
@@ -27,19 +27,23 @@ import unknownImplementationIcon from '../../icons/question-solid.svg';
import './Row.css'; import './Row.css';
interface RowProps { const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
node: Node;
pins: PersistentSet<Types.NodeName>;
columns: Column[];
};
interface RowState { export namespace Row {
update: number; export interface Props {
}; node: Node;
pins: PersistentSet<Types.NodeName>;
columns: Column[];
}
export interface State {
update: number;
}
}
interface HeaderProps { interface HeaderProps {
columns: Column[]; columns: Column[];
}; }
interface Column { interface Column {
label: string; label: string;
@@ -82,7 +86,7 @@ function formatCPU(cpu: number, stamp: Maybe<Types.Timestamp>): string {
return `${cpu.toFixed(fractionDigits)}%${ago}`; return `${cpu.toFixed(fractionDigits)}%${ago}`;
} }
export default class Row extends React.Component<RowProps, RowState> { export class Row extends React.Component<Row.Props, Row.State> {
public static readonly columns: Column[] = [ public static readonly columns: Column[] = [
{ {
label: 'Node', label: 'Node',
@@ -95,7 +99,7 @@ export default class Row extends React.Component<RowProps, RowState> {
width: 16, width: 16,
setting: 'validator', setting: 'validator',
render: ({ validator }) => { render: ({ validator }) => {
return validator ? <Tooltip text={validator} copy={true}><span className="Node-Row-validator"><Identicon id={validator} size={16} /></span></Tooltip> : '-'; return validator ? <Tooltip text={validator} copy={true}><span className="Row-validator"><Identicon id={validator} size={16} /></span></Tooltip> : '-';
} }
}, },
{ {
@@ -210,7 +214,7 @@ export default class Row extends React.Component<RowProps, RowState> {
return ( return (
<thead> <thead>
<tr className="Node-Row-Header"> <tr className="Row-Header">
{ {
columns.map(({ icon, width, label }, index) => { columns.map(({ icon, width, label }, index) => {
const position = index === 0 ? 'left' const position = index === 0 ? 'left'
@@ -243,21 +247,21 @@ export default class Row extends React.Component<RowProps, RowState> {
node.unsubscribe(this.onUpdate); node.unsubscribe(this.onUpdate);
} }
public shouldComponentUpdate(nextProps: RowProps, nextState: RowState): boolean { public shouldComponentUpdate(nextProps: Row.Props, nextState: Row.State): boolean {
return this.props.node.id !== nextProps.node.id || this.state.update !== nextState.update; return this.props.node.id !== nextProps.node.id || this.state.update !== nextState.update;
} }
public render() { public render() {
const { node, columns } = this.props; const { node, columns } = this.props;
let className = 'Node-Row'; let className = 'Row';
if (node.propagationTime != null) { if (node.propagationTime != null) {
className += ' Node-Row-synced'; className += ' Row-synced';
} }
if (node.pinned) { if (node.pinned) {
className += ' Node-Row-pinned'; className += ' Row-pinned';
} }
return ( return (
@@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Tooltip } from '../'; import { Tooltip } from '../';
namespace Truncate { export namespace Truncate {
export interface Props { export interface Props {
text: string; text: string;
copy?: boolean; copy?: boolean;
@@ -9,13 +9,13 @@ namespace Truncate {
} }
} }
class Truncate extends React.Component<Truncate.Props, {}> { export class Truncate extends React.Component<Truncate.Props, {}> {
public render() { public render() {
const { text, position, copy } = this.props; const { text, position, copy } = this.props;
return ( return (
<Tooltip text={text} position={position} copy={copy} className="Node-Row-Tooltip"> <Tooltip text={text} position={position} copy={copy} className="Row-Tooltip">
<div className="Node-Row-truncate">{text}</div> <div className="Row-truncate">{text}</div>
</Tooltip> </Tooltip>
); );
} }
@@ -24,5 +24,3 @@ class Truncate extends React.Component<Truncate.Props, {}> {
return this.props.text !== nextProps.text || this.props.position !== nextProps.position; return this.props.text !== nextProps.text || this.props.position !== nextProps.position;
} }
} }
export default Truncate;
@@ -0,0 +1,3 @@
export * from './List';
export * from './Truncate';
export * from './Row';
@@ -1,5 +1,5 @@
.Node-Location { .Location {
width: 6px; width: 6px;
height: 6px; height: 6px;
background: transparent; background: transparent;
@@ -15,7 +15,7 @@
transition: border-color 0.25s linear; transition: border-color 0.25s linear;
} }
.Node-Location-dimmed { .Location-dimmed {
width: 2px; width: 2px;
height: 2px; height: 2px;
margin-left: -1px; margin-left: -1px;
@@ -25,34 +25,34 @@
border: none; border: none;
} }
.Node-Location-ping { .Location-ping {
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
display: none; display: none;
} }
.Node-Location-odd { .Location-odd {
border-color: #bbb; border-color: #bbb;
} }
.Node-Location-synced { .Location-synced {
z-index: 3; z-index: 3;
border-color: #d64ca8; border-color: #d64ca8;
} }
.Node-Location-synced .Node-Location-ping { .Location-synced .Location-ping {
border: 1px solid #fff; border: 1px solid #fff;
border-radius: 30px; border-radius: 30px;
display: block; display: block;
animation: ping 1s forwards; animation: ping 1s forwards;
} }
.Node-Location:hover { .Location:hover {
z-index: 4; z-index: 4;
border-color: #fff; border-color: #fff;
} }
.Node-Location-details { .Location-details {
min-width: 335px; min-width: 335px;
position: absolute; position: absolute;
font-family: monospace, sans-serif; font-family: monospace, sans-serif;
@@ -62,38 +62,38 @@
border-collapse: collapse; border-collapse: collapse;
} }
.Node-Location-quarter0 .Node-Location-details { .Location-quarter0 .Location-details {
left: 16px; left: 16px;
top: -4px; top: -4px;
} }
.Node-Location-quarter1 .Node-Location-details { .Location-quarter1 .Location-details {
right: 16px; right: 16px;
top: -4px; top: -4px;
} }
.Node-Location-quarter2 .Node-Location-details { .Location-quarter2 .Location-details {
left: 16px; left: 16px;
bottom: -4px; bottom: -4px;
} }
.Node-Location-quarter3 .Node-Location-details { .Location-quarter3 .Location-details {
right: 16px; right: 16px;
bottom: -4px; bottom: -4px;
} }
.Node-Location-details td { .Location-details td {
text-align: left; text-align: left;
padding: 0.5em 1em; padding: 0.5em 1em;
} }
.Node-Location-details td:nth-child(odd) { .Location-details td:nth-child(odd) {
width: 16px; width: 16px;
text-align: center; text-align: center;
padding-right: 0.2em; padding-right: 0.2em;
} }
.Node-Location-details td:nth-child(even) { .Location-details td:nth-child(even) {
padding-left: 0.2em; padding-left: 0.2em;
} }
@@ -117,7 +117,7 @@
} }
} }
.Node-Location-validator { .Location-validator {
display: inline-block; display: inline-block;
width: 16px; width: 16px;
height: 16px; height: 16px;
@@ -16,7 +16,7 @@ import lastTimeIcon from '../../icons/watch.svg';
import './Location.css'; import './Location.css';
namespace Location { export namespace Location {
export type Quarter = 0 | 1 | 2 | 3; export type Quarter = 0 | 1 | 2 | 3;
export interface Props { export interface Props {
@@ -36,7 +36,7 @@ namespace Location {
} }
} }
class Location extends React.Component<Location.Props, Location.State> { export class Location extends React.Component<Location.Props, Location.State> {
public readonly state = { hover: false }; public readonly state = { hover: false };
public render() { public render() {
@@ -48,16 +48,16 @@ class Location extends React.Component<Location.Props, Location.State> {
return null; return null;
} }
let className = `Node-Location Node-Location-quarter${quarter}`; let className = `Location Location-quarter${quarter}`;
if (focused) { if (focused) {
if (propagationTime != null) { if (propagationTime != null) {
className += ' Node-Location-synced'; className += ' Location-synced';
} else if (height % 2 === 1) { } else if (height % 2 === 1) {
className += ' Node-Location-odd'; className += ' Location-odd';
} }
} else { } else {
className += ' Node-Location-dimmed'; className += ' Location-dimmed';
} }
return ( return (
@@ -65,7 +65,7 @@ class Location extends React.Component<Location.Props, Location.State> {
{ {
this.state.hover ? this.renderDetails() : null this.state.hover ? this.renderDetails() : null
} }
<div className="Node-Location-ping" /> <div className="Location-ping" />
</div> </div>
); );
} }
@@ -92,14 +92,14 @@ class Location extends React.Component<Location.Props, Location.State> {
<td><Icon src={nodeValidatorIcon} alt="Node" /></td> <td><Icon src={nodeValidatorIcon} alt="Node" /></td>
<td colSpan={5}> <td colSpan={5}>
{trimHash(validator, 30)} {trimHash(validator, 30)}
<span className="Node-Location-validator"><Identicon id={validator} size={16} /></span> <span className="Location-validator"><Identicon id={validator} size={16} /></span>
</td> </td>
</tr> </tr>
); );
} }
return ( return (
<table className="Node-Location-details Node-Location-details"> <table className="Location-details Location-details">
<tbody> <tbody>
<tr> <tr>
<td><Icon src={nodeIcon} alt="Node" /></td><td colSpan={5}>{name}</td> <td><Icon src={nodeIcon} alt="Node" /></td><td colSpan={5}>{name}</td>
@@ -138,5 +138,3 @@ class Location extends React.Component<Location.Props, Location.State> {
this.setState({ hover: false }); this.setState({ hover: false });
} }
} }
export default Location;
@@ -0,0 +1,11 @@
.Map {
min-width: 1350px;
background: url('../../assets/world-map.svg') no-repeat;
background-size: contain;
background-position: center;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
@@ -0,0 +1,118 @@
import * as React from 'react';
import { Types, Maybe } from '@dotstats/common';
import { State as AppState, Node } from '../../state';
import { Location } from './';
import { viewport } from '../../utils';
const MAP_RATIO = 800 / 350;
const MAP_HEIGHT_ADJUST = 400 / 350;
const HEADER = 148;
import './Map.css';
export namespace Map {
export interface Props {
filter: Maybe<(node: Node) => boolean>;
appState: Readonly<AppState>;
}
export interface State {
width: number;
height: number;
top: number;
left: number;
}
}
export class Map extends React.Component<Map.Props, Map.State> {
public state = {
width: 0,
height: 0,
top: 0,
left: 0
}
public componentWillMount() {
this.onResize();
window.addEventListener('resize', this.onResize);
}
public componentWillUnmount() {
window.removeEventListener('resize', this.onResize);
}
public render() {
const { filter, appState } = this.props;
const nodes = appState.nodes.sorted();
return (
<div className="Map">
{
nodes.map((node) => {
const { lat, lon } = node;
const focused = filter == null || filter(node);
if (lat == null || lon == null) {
// Skip nodes with unknown location
return null;
}
const position = this.pixelPosition(lat, lon);
return (
<Location key={node.id} position={position} focused={focused} node={node} />
);
})
}
</div>
);
}
private pixelPosition(lat: Types.Latitude, lon: Types.Longitude): Location.Position {
const { state } = this;
// Longitude ranges -180 (west) to +180 (east)
// Latitude ranges +90 (north) to -90 (south)
const left = Math.round(((180 + lon) / 360) * state.width + state.left);
const top = Math.round(((90 - lat) / 180) * state.height + state.top) * MAP_HEIGHT_ADJUST;
let quarter: Location.Quarter = 0;
if (lon > 0) {
quarter = (quarter | 1) as Location.Quarter;
}
if (lat < 0) {
quarter = (quarter | 2) as Location.Quarter;
}
return { left, top, quarter };
}
private onResize: () => void = () => {
const vp = viewport();
vp.width = Math.max(1350, vp.width);
vp.height -= HEADER;
const ratio = vp.width / vp.height;
let top = 0;
let left = 0;
let width = 0;
let height = 0;
if (ratio >= MAP_RATIO) {
width = Math.round(vp.height * MAP_RATIO);
height = Math.round(vp.height);
left = (vp.width - width) / 2;
} else {
width = Math.round(vp.width);
height = Math.round(vp.width / MAP_RATIO);
top = (vp.height - height) / 2;
}
this.setState({ top, left, width, height });
}
}
@@ -0,0 +1,2 @@
export * from './Map';
export * from './Location';
@@ -1,7 +0,0 @@
import Row from './Row';
import Location from './Location';
import Truncate from './Truncate';
export { Row, Location, Truncate };
export const SEMVER_PATTERN = /^\d+\.\d+\.\d+/;
@@ -1,7 +1,7 @@
import * as React from 'react'; import * as React from 'react';
import { Icon } from './'; import { Icon } from '../';
import { State } from '../state'; import { State } from '../../state';
import { PersistentObject } from '../persist'; import { PersistentObject } from '../../persist';
import './Setting.css'; import './Setting.css';
@@ -0,0 +1,17 @@
.Settings {
text-align: center;
}
.Settings-category {
text-align: left;
width: 500px;
margin: 0 auto;
padding: 2em 0;
}
.Settings-category h2 {
padding: 0;
margin: 0 0 0.5em 0;
font-size: 20px;
font-weight: 100;
}
@@ -0,0 +1,45 @@
import * as React from 'react';
import { Maybe } from '@dotstats/common';
import { State as AppState } from '../../state';
import { Setting } from './';
import { Row } from '../List';
import { PersistentObject } from '../../persist';
import './Settings.css';
export namespace Settings {
export type Display = 'list' | 'map' | 'settings';
export interface Props {
settings: PersistentObject<AppState.Settings>;
}
export interface State {
display: Display;
filter: Maybe<string>;
}
}
export class Settings extends React.Component<Settings.Props, {}> {
public render() {
const { settings } = this.props;
return (
<div className="Settings">
<div className="Settings-category">
<h2>Visible Columns</h2>
{
Row.columns
.map(({ label, icon, setting }, index) => {
if (!setting) {
return null;
}
return <Setting key={index} setting={setting} settings={settings} icon={icon} label={label} />;
})
}
</div>
</div>
);
}
}
@@ -0,0 +1,2 @@
export * from './Settings';
export * from './Setting';
+3 -5
View File
@@ -1,13 +1,11 @@
export * from './Chains'; export * from './Chains';
export * from './Chain'; export * from './Chain';
export * from './List';
export * from './Map';
export * from './Settings';
export * from './Icon'; export * from './Icon';
export * from './Tile'; export * from './Tile';
export * from './Ago'; export * from './Ago';
export * from './OfflineIndicator'; export * from './OfflineIndicator';
export * from './Setting';
export * from './Sparkline'; export * from './Sparkline';
export * from './Tooltip'; export * from './Tooltip';
import * as Node from './Node';
export { Node };