diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 36fc4ee..b60b4eb 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; import { Types, SortedCollection } from '@dotstats/common'; -import { Chains, Chain, Ago, OfflineIndicator } from './components'; +import { AllChains, Chains, Chain, Ago, OfflineIndicator } from './components'; import { Connection } from './Connection'; import { PersistentObject, PersistentSet } from './persist'; -import { State, Node } from './state'; +import { State, Node, ChainData } from './state'; +import { getHashData } from './utils'; +import stable from 'stable'; import './App.css'; @@ -49,6 +51,8 @@ export default class App extends React.Component<{}, State> { this.setState({ nodes, pins }); }); + const { tab = '' } = getHashData(); + this.state = { status: 'offline', best: 0 as Types.BlockNumber, @@ -66,7 +70,7 @@ export default class App extends React.Component<{}, State> { nodes: new SortedCollection(Node.compare), settings: this.settings.raw(), pins: this.pins.get(), - tabChanged: false, + tab, }; this.connection = Connection.create(this.pins, (changes) => { @@ -79,11 +83,12 @@ export default class App extends React.Component<{}, State> { } public render() { - const { chains, timeDiff, subscribed, status } = this.state; + const { timeDiff, subscribed, status, tab } = this.state; + const chains = this.chains(); Ago.timeDiff = timeDiff; - if (chains.size === 0) { + if (chains.length === 0) { return (
@@ -92,27 +97,28 @@ export default class App extends React.Component<{}, State> { ); } + const overlay = tab === 'all-chains' + ? + : null; + return (
+ {overlay}
); } - public componentDidUpdate() { - if (this.state.tabChanged === true) { - this.setState({tabChanged: false}); - } - } - public componentWillMount() { window.addEventListener('keydown', this.onKeyPress); + window.addEventListener('hashchange', this.onHashChange); } public componentWillUnmount() { window.removeEventListener('keydown', this.onKeyPress); + window.removeEventListener('hashchange', this.onHashChange); } private onKeyPress = (event: KeyboardEvent) => { @@ -140,4 +146,30 @@ export default class App extends React.Component<{}, State> { connection.subscribe(chains[index]); }) } + + private onHashChange = (event: Event) => { + const { tab = '' } = getHashData(); + + this.setState({ tab }); + } + + private chains(): ChainData[] { + return stable + .inplace( + Array.from(this.state.chains.entries()), + (a, b) => { + if (a[0] === 'Alexander') { + return -1; + } + + if (b[0] === 'Alexander') { + return 1; + } + + return b[1] - a[1]; + } + ) + .map(([label, nodeCount]) => ({ label, nodeCount })); + } + } diff --git a/packages/frontend/src/Connection.ts b/packages/frontend/src/Connection.ts index 1db0c65..e23199f 100644 --- a/packages/frontend/src/Connection.ts +++ b/packages/frontend/src/Connection.ts @@ -81,7 +81,7 @@ export class Connection { public subscribe(chain: Types.ChainLabel) { if (this.state.subscribed != null && this.state.subscribed !== chain) { this.state = this.update({ - tabChanged: true, + tab: 'list', }); setHashData({ chain, tab: 'list' }); } else { diff --git a/packages/frontend/src/components/AllChains.css b/packages/frontend/src/components/AllChains.css new file mode 100644 index 0000000..c73d8ec --- /dev/null +++ b/packages/frontend/src/components/AllChains.css @@ -0,0 +1,50 @@ +.AllChains { + position: absolute; + z-index: 20; + top: 16px; + bottom: 16px; + left: 50%; + margin: 0 0 0 -150px; + width: 25vw; + min-width: 300px; + background: #fff; + box-shadow: 0 2px 20px rgba(0,0,0,0.35); + overflow-y: scroll; + overflow-x: hide; +} + +.AllChains-chain { + padding: 0 12px; + background: #B5AEAE; + color: #444; + display: block; + border-bottom: 1px solid rgba(255,255,255,0.5); + height: 40px; + line-height: 40px; + cursor: pointer; + font-size: 0.8em; + font-weight: bold; + position: relative; +} + +.AllChains-node-count { + display: inline-block; + padding: 0 0.5em 0.1em; + border-radius: 1em; + background: #8C8787; + color: #fff; + font-weight: normal; + text-shadow: rgba(0,0,0,0.5) 0 1px 0; + font-size: 0.9em; + line-height: 1.4em; + margin: 0 -0.3em 0 0.3em; +} + +.AllChains-chain-selected { + background: #fff; + color: #000; +} + +.AllChains-chain-selected .AllChains-node-count { + background: #E6007A; +} diff --git a/packages/frontend/src/components/AllChains.tsx b/packages/frontend/src/components/AllChains.tsx new file mode 100644 index 0000000..e5c53d4 --- /dev/null +++ b/packages/frontend/src/components/AllChains.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { Connection } from '../Connection'; +import { Types, Maybe } from '@dotstats/common'; +import { ChainData } from '../state'; + +import './AllChains.css'; + +export namespace AllChains { + export interface Props { + chains: ChainData[], + subscribed: Maybe, + connection: Promise + } +} + +export class AllChains extends React.Component { + public render() { + const { chains } = this.props; + + return ( +
+ {chains.map((chain) => this.renderChain(chain))} +
+ ); + } + + private renderChain(chain: ChainData): React.ReactNode { + const { label, nodeCount } = chain; + + const className = label === this.props.subscribed + ? 'AllChains-chain AllChains-chain-selected' + : 'AllChains-chain'; + + return ( + + {label} {nodeCount} + + ) + } + + private async subscribe(chain: Types.ChainLabel) { + const connection = await this.props.connection; + + connection.subscribe(chain); + connection.resetConsensus(); + } +} diff --git a/packages/frontend/src/components/Chain/Chain.tsx b/packages/frontend/src/components/Chain/Chain.tsx index c08ca36..0c8db16 100644 --- a/packages/frontend/src/components/Chain/Chain.tsx +++ b/packages/frontend/src/components/Chain/Chain.tsx @@ -56,13 +56,6 @@ export class Chain extends React.Component { }; } - public shouldComponentUpdate(nextProps: Chain.Props, nextState: Chain.State): boolean { - if (nextProps.appState.tabChanged === true && nextState.display === 'consensus') { - this.setDisplay('list'); - } - return true; - } - public render() { const { appState } = this.props; const { best, finalized, blockTimestamp, blockAverage } = appState; diff --git a/packages/frontend/src/components/Chains.css b/packages/frontend/src/components/Chains.css index 2d8188e..d6eced2 100644 --- a/packages/frontend/src/components/Chains.css +++ b/packages/frontend/src/components/Chains.css @@ -1,7 +1,7 @@ .Chains { background: #B5AEAE; color: #000; - padding: 0 16px; + padding: 0 76px 0 16px; height: 40px; min-width: 1318px; position: relative; @@ -25,6 +25,15 @@ border-left: 1px solid rgba(255,255,255,0.5); } +.Chains-all-chains { + display: block; + padding: 0; + margin: 0; + position: absolute; + right: 48px; + top: 6px; +} + .Chains-fork-me { display: block; padding: 0; @@ -34,7 +43,7 @@ top: 6px; } -.Chains-fork-me .Icon { +.Chains-all-chains .Icon, .Chains-fork-me .Icon { font-size: 28px; margin: 0; height: 28px; @@ -58,7 +67,6 @@ .Chains-chain-selected { background: #fff; color: #000; - z-index: 1; } .Chains-chain-selected .Chains-node-count { diff --git a/packages/frontend/src/components/Chains.tsx b/packages/frontend/src/components/Chains.tsx index 0d54d53..2891d19 100644 --- a/packages/frontend/src/components/Chains.tsx +++ b/packages/frontend/src/components/Chains.tsx @@ -2,19 +2,15 @@ import * as React from 'react'; import { Connection } from '../Connection'; import { Icon } from './Icon'; import { Types, Maybe } from '@dotstats/common'; -import stable from 'stable'; +import { ChainData } from '../state'; import githubIcon from '../icons/mark-github.svg'; +import listIcon from '../icons/three-bars.svg'; import './Chains.css'; -interface ChainData { - label: Types.ChainLabel; - nodeCount: Types.NodeCount; -} - export namespace Chains { export interface Props { - chains: Map, + chains: ChainData[], subscribed: Maybe, connection: Promise } @@ -22,11 +18,15 @@ export namespace Chains { export class Chains extends React.Component { public render() { + const allChainsHref = this.props.subscribed ? `#all-chains/${this.props.subscribed}` : `#all-chains`; + const { chains } = this.props; + return (
- { - this.chains.map((chain) => this.renderChain(chain)) - } + {chains.map((chain) => this.renderChain(chain))} + + + @@ -48,25 +48,6 @@ export class Chains extends React.Component { ) } - private get chains(): ChainData[] { - return stable - .inplace( - Array.from(this.props.chains.entries()), - (a, b) => { - if (a[0] === 'Alexander') { - return -1; - } - - if (b[0] === 'Alexander') { - return 1; - } - - return b[1] - a[1]; - } - ) - .map(([label, nodeCount]) => ({ label, nodeCount })); - } - private async subscribe(chain: Types.ChainLabel) { const connection = await this.props.connection; diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts index 93000ba..ebcf4d2 100644 --- a/packages/frontend/src/components/index.ts +++ b/packages/frontend/src/components/index.ts @@ -1,3 +1,4 @@ +export * from './AllChains'; export * from './Chains'; export * from './Chain'; export * from './List'; diff --git a/packages/frontend/src/state.ts b/packages/frontend/src/state.ts index 2b4c95f..5446c60 100644 --- a/packages/frontend/src/state.ts +++ b/packages/frontend/src/state.ts @@ -195,7 +195,7 @@ export interface State { finalized: Types.BlockNumber; consensusInfo: Types.ConsensusInfo; displayConsensusLoadingScreen: boolean; - tabChanged: boolean; + tab: string; authorities: Types.Address[]; authoritySetId: Maybe; sendFinality: boolean; @@ -211,3 +211,8 @@ export interface State { export type Update = (changes: Pick | null) => Readonly; export type UpdateBound = (changes: Pick | null) => void; + +export interface ChainData { + label: Types.ChainLabel; + nodeCount: Types.NodeCount; +}