diff --git a/backend/telemetry_core/tests/soak_tests.rs b/backend/telemetry_core/tests/soak_tests.rs index 24ea29d..16a297d 100644 --- a/backend/telemetry_core/tests/soak_tests.rs +++ b/backend/telemetry_core/tests/soak_tests.rs @@ -104,51 +104,59 @@ async fn run_soak_test(opts: SoakTestOpts) { shard_ids.push(shard_id); } - // Connect nodes to each shard: + // Connect nodes to each shard for each chain: let mut nodes = vec![]; - for &shard_id in &shard_ids { - let mut conns = server - .get_shard(shard_id) - .unwrap() - .connect_multiple_nodes(opts.nodes) - .await - .expect("node connections failed"); - nodes.append(&mut conns); + for chain_name in chain_names(opts.chains) { + for &shard_id in &shard_ids { + let conns = server + .get_shard(shard_id) + .unwrap() + .connect_multiple_nodes(opts.nodes) + .await + .expect("node connections failed"); + nodes.push((chain_name.clone(), conns)); + } } - let genesis_hash = BlockHash::from_low_u64_be(1); - let genesis_hash_string = format!("{:0x}", genesis_hash); + let first_genesis_hash = BlockHash::from_low_u64_be(1); + let first_genesis_hash_string = format!("{:0x}", first_genesis_hash); // Start nodes talking to the shards: let bytes_in = Arc::new(AtomicUsize::new(0)); let ids_per_node = opts.ids_per_node; - for node in nodes.into_iter().enumerate() { - let (idx, (tx, _)) = node; - for id in 0..ids_per_node { - let bytes_in = Arc::clone(&bytes_in); - let tx = tx.clone(); - tokio::spawn(async move { - let telemetry = test_utils::fake_telemetry::FakeTelemetry { - block_time: Duration::from_secs(3), - node_name: format!("Node {}", (ids_per_node * idx) + id + 1), - chain: "Polkadot".to_owned(), - genesis_hash: genesis_hash, - message_id: id + 1, - }; + // For each chain... + for (i, (chain_name, conns)) in nodes.into_iter().enumerate() { + // ...Broadcast an init message from each node with that chain name + for (j, (tx, _)) in conns.into_iter().enumerate() { + let idx = i * opts.nodes + j; + for id in 0..ids_per_node { + let bytes_in = Arc::clone(&bytes_in); + let tx = tx.clone(); + let chain_name = chain_name.clone(); - let res = telemetry - .start(|msg| async { - bytes_in.fetch_add(msg.len(), Ordering::Relaxed); - tx.unbounded_send(SentMessage::Binary(msg))?; - Ok::<_, anyhow::Error>(()) - }) - .await; + tokio::spawn(async move { + let telemetry = test_utils::fake_telemetry::FakeTelemetry { + block_time: Duration::from_secs(3), + node_name: format!("{} Node {}", chain_name, (ids_per_node * idx) + id + 1), + chain: chain_name, + genesis_hash: BlockHash::from_low_u64_be((i + 1) as u64), + message_id: id + 1, + }; - if let Err(e) = res { - log::error!("Telemetry Node #{} has died with error: {}", idx, e); - } - }); + let res = telemetry + .start(|msg| async { + bytes_in.fetch_add(msg.len(), Ordering::Relaxed); + tx.unbounded_send(SentMessage::Binary(msg))?; + Ok::<_, anyhow::Error>(()) + }) + .await; + + if let Err(e) = res { + log::error!("Telemetry Node #{} has died with error: {}", idx, e); + } + }); + } } } @@ -159,10 +167,10 @@ async fn run_soak_test(opts: SoakTestOpts) { .await .expect("feed connections failed"); - // Every feed subscribes to the chain above to recv messages about it: + // Every feed subscribes to the first chain we have started up. We ignore the rest. for (feed_tx, _) in &mut feeds { feed_tx - .send_command("subscribe", &genesis_hash_string) + .send_command("subscribe", &first_genesis_hash_string) .unwrap(); } @@ -218,6 +226,42 @@ async fn run_soak_test(opts: SoakTestOpts) { future::pending().await } +/// Return an iterator of `total` unique chain names. +fn chain_names(total: usize) -> impl Iterator { + static CHAIN_STARTS: [&'static str; 5] = ["Polkadot", "Kusama", "Khala", "Wibble", "Moonbase"]; + static CHAIN_ENDS: [&'static str; 6] = ["", " Testnet", " Main", "-Dev", "Alpha", "Beta"]; + + let mut count = 0; + let mut s_n = 0; + let mut e_n = 0; + + std::iter::from_fn(move || { + if count == total { + return None; + } + + let mut res = format!("{}{}", CHAIN_STARTS[s_n], CHAIN_ENDS[e_n]); + + let suffix = count / (CHAIN_STARTS.len() * CHAIN_ENDS.len()); + if suffix > 0 { + res.push(' '); + res.push_str(&suffix.to_string()); + } + + s_n += 1; + count += 1; + if s_n == CHAIN_STARTS.len() { + s_n = 0; + e_n += 1; + if e_n == CHAIN_ENDS.len() { + e_n = 0; + } + } + + Some(res) + }) +} + /// General arguments that are used to start a soak test. Run `soak_test` as /// instructed by its documentation for full control over what is ran, or run /// preconfigured variants. @@ -226,10 +270,14 @@ struct SoakTestOpts { /// The number of shards to run this test with #[structopt(long)] shards: usize, - /// The number of feeds to connect + /// The number of feeds to connect to the core #[structopt(long)] feeds: usize, - /// The number of nodes to connect to each feed + /// The number of chains that nodes will pretend to belong to + #[structopt(long, default_value = "1")] + chains: usize, + /// The number of nodes to connect to each shard * chain combo. + /// If we have 10 chains and 4 shards, setting this to 1 will connect `10 x 4 x 1 = 40` nodes. #[structopt(long)] nodes: usize, /// The number of different virtual nodes to connect per actual node socket connection diff --git a/frontend/src/components/AllChains.css b/frontend/src/components/AllChains.css index 1ede8ae..5000d43 100644 --- a/frontend/src/components/AllChains.css +++ b/frontend/src/components/AllChains.css @@ -16,64 +16,109 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -.AllChains { - position: fixed; - 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-overlay { position: fixed; - display: block; z-index: 19; background: rgba(0, 0, 0, 0.35); left: 0; right: 0; top: 0; bottom: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.AllChains-content { + max-height: calc(100vh - 2em); + max-width: calc(100vw - 2em); + border-radius: 4px; + width: 600px; + overflow: auto; + background-color: white; + display: flex; + flex-direction: column; +} + +.AllChains-controls { + padding-bottom: 0.5em; + display: flex; + align-items: center; + padding: 0.5em; + border-bottom: 1px solid rgba(0,0,0,0.1); +} + +.AllChains-controls input { + border: 1px solid rgba(0,0,0,0.5); + border-radius: 4px; + padding: 0.5em; + flex-grow: 1; + min-width: 100px; +} + +.AllChains-controls-sortby { + padding: 0.4em 0.5em; + margin-left: 0.5em; + border-radius: 4px; + cursor: pointer; + user-select: none; + font-weight: bold; + font-size: 0.9em; + border: 1px solid black; +} + +.AllChains-controls-sortby-active { + background-color: #e6007a; + border-color: #e6007a; + color: white; +} + +.AllChains-chains { + flex-grow: 1; + overflow: auto; + padding: 0.5em; } .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; + padding: 10px 10px; + background: rgb(220,220,220); + color: black; + display: inline-flex; + margin-right: 0.5em; + margin-bottom: 0.5em; + border-radius: 4px; cursor: pointer; font-size: 0.8em; font-weight: bold; position: relative; + align-items: center; + justify-content: center; +} + +.AllChains-chain-highlighted-text { + background-color: yellow; + color: black; } .AllChains-node-count { - display: inline-block; - padding: 0 0.5em 0.1em; + display: inline-flex; + align-items: center; + justify-content: center; 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; + margin-left: 0.5em; + padding: 0.3em 0.5em; } .AllChains-chain-selected { - background: #fff; - color: #000; + background: #e6007a; + color: white; } .AllChains-chain-selected .AllChains-node-count { - background: #e6007a; + background: white; + color: #e6007a; } diff --git a/frontend/src/components/AllChains.tsx b/frontend/src/components/AllChains.tsx index f0389b6..bca620c 100644 --- a/frontend/src/components/AllChains.tsx +++ b/frontend/src/components/AllChains.tsx @@ -29,46 +29,152 @@ export namespace AllChains { } } -export class AllChains extends React.Component { - public render() { - const { chains, subscribed } = this.props; - const close = subscribed ? `#list/${subscribed}` : '#list'; +export function AllChains(props: AllChains.Props) { + const { chains, subscribed, connection } = props; + const [filterText, setFilterText] = React.useState(''); + const [sortBy, setSortBy] = React.useState(SortBy.NumberOfNodes); + function close() { + window.location.hash = subscribed ? `#list/${subscribed}` : '#list'; + } + + function sortByAlphabetical() { + setSortBy(SortBy.Alphabetical); + } + + function sortByNumberOfNodes() { + setSortBy(SortBy.NumberOfNodes); + } + + function updateFilterText(ev: React.FormEvent) { + ev.stopPropagation(); + setFilterText(ev.currentTarget.value); + } + + function ignoreClicks(ev: React.MouseEvent) { + ev.stopPropagation(); + } + + function subscribeToChain(chain: ChainData) { + return () => { + connection.then((c) => c.subscribe(chain.genesisHash)); + close(); + }; + } + + const lowercaseFilterText = filterText.toLocaleLowerCase(); + const filteredChains = chains.filter((chain) => { + return chain.label.toLocaleLowerCase().includes(lowercaseFilterText); + }); + + // The default sort is equal to the main display, so only sort the nodes + // if we want to sort alphabetically: + if (sortBy === SortBy.Alphabetical) { + filteredChains.sort((a, b) => a.label.localeCompare(b.label)); + } + + const chainHtml = + filteredChains.length > 0 + ? filteredChains.map((chain) => ( + + )) + : 'No chains found'; + + return ( +
+
+
+ + + +
+
{chainHtml}
+
+
+ ); +} + +type SortByControlProps = { + text: string; + isActive: boolean; + onClick: () => void; +}; + +function SortByControl(props: SortByControlProps) { + const className = props.isActive + ? 'AllChains-controls-sortby AllChains-controls-sortby-active' + : 'AllChains-controls-sortby'; + + return ( +
+ {props.text} +
+ ); +} + +type ChainProps = { + chain: ChainData; + filterText: string; + isSubscribed: boolean; + onClick: () => void; +}; + +function Chain({ chain, isSubscribed, onClick, filterText }: ChainProps) { + const { label, nodeCount } = chain; + + const className = isSubscribed + ? 'AllChains-chain AllChains-chain-selected' + : 'AllChains-chain'; + + const labelHtml = filterText ? labelWithFilterText(label, filterText) : label; + + return ( + + {labelHtml} + + {nodeCount} + + + ); +} + +enum SortBy { + Alphabetical, + NumberOfNodes, +} + +function labelWithFilterText(label: string, filterText: string) { + const idx = label.toLocaleLowerCase().indexOf(filterText); + if (idx > -1) { return ( <> - -
- {chains.map((chain) => this.renderChain(chain))} -
+ {label.slice(0, idx)} + + {label.slice(idx, idx + filterText.length)} + + {label.slice(idx + filterText.length)} ); - } - - private renderChain(chain: ChainData): React.ReactNode { - const { label, genesisHash, nodeCount } = chain; - - const className = - genesisHash === this.props.subscribed - ? 'AllChains-chain AllChains-chain-selected' - : 'AllChains-chain'; - - return ( -
- {label}{' '} - - {nodeCount} - - - ); - } - - private async subscribe(chain: Types.GenesisHash) { - const connection = await this.props.connection; - - connection.subscribe(chain); + } else { + return label; } } diff --git a/frontend/src/components/Chain/Tab.css b/frontend/src/components/Chain/Tab.css index 6292728..4182f2b 100644 --- a/frontend/src/components/Chain/Tab.css +++ b/frontend/src/components/Chain/Tab.css @@ -17,17 +17,18 @@ along with this program. If not, see . */ .Chain-Tab { - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; margin-right: 5px; - font-size: 24px; - line-height: 24px; - height: 24px; - width: 24px; - padding: 6px; + font-size: 18px; + line-height: 20px; + height: 32px; + width: 32px; color: #555; cursor: pointer; - padding: 10px; border-radius: 40px; + box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.2); } .Chain-Tab:hover { diff --git a/frontend/src/components/Chains.css b/frontend/src/components/Chains.css index 697971a..6ced3cd 100644 --- a/frontend/src/components/Chains.css +++ b/frontend/src/components/Chains.css @@ -21,8 +21,7 @@ along with this program. If not, see . color: #000; padding: 0 76px 0 16px; height: 40px; - /* min-width is 1350 - 76 - 16 to account for padding */ - min-width: 1258px; + min-width: 1350px; position: relative; } @@ -30,13 +29,15 @@ along with this program. If not, see . top: 4px; padding: 0 12px; color: #fff; - display: inline-block; + display: inline-flex; + align-items: center; + justify-content: center; margin-right: 4px; - height: 36; - line-height: 36px; + height: 36px; cursor: pointer; font-size: 0.8em; position: relative; + z-index: 0; border-radius: 4px 4px 0 0; } @@ -68,24 +69,55 @@ along with this program. If not, see . } .Chains-node-count { - padding: 0 5px; + padding: 0px; display: inline-block; - height: 20px; border-radius: 20px; background: #fff; color: #e6007a; font-size: 0.9em; - line-height: 20px; - margin: 0 -0.5em 0 0.5em; + margin-left: 0.5em; + padding: 0.3em 0.5em; } -.Chains-chain-selected { - background: #fff; +/* Create a "tab background" that will rise up on hover/selection */ +.Chains-chain::before { + content: ''; + background-color: white; + border-radius: 4px 4px 0 0; + position: absolute; + z-index: -1; + bottom: 0; + left: 0; + right: 0; + height: 0px; + /* + To animate the tab height changes, we can uncomment this line: + + transition: height ease-in-out 0.2s; + */ +} + +/* Animate the tab background to rise up slightly on hover */ +.Chains-chain:hover::before { + height: 4px; +} + +.Chains-chain.Chains-chain-selected { color: #393838; - font-weight: bold; + /* + Instead of making the font bold, which changes the container width and + causes some wobbling, apply a tiny text shadow to "bold" it without the + width change: + */ + text-shadow: -0.06ex 0 #393838, 0.06ex 0 #393838; } -.Chains-chain-selected .Chains-node-count { +/* Animate the tab background to rise up all the way on selection */ +.Chains-chain.Chains-chain-selected::before { + height: 36px; +} + +.Chains-chain.Chains-chain-selected .Chains-node-count { background: #393838; color: #fff; } diff --git a/frontend/src/components/Chains.tsx b/frontend/src/components/Chains.tsx index c245d5c..19f4109 100644 --- a/frontend/src/components/Chains.tsx +++ b/frontend/src/components/Chains.tsx @@ -97,7 +97,7 @@ export class Chains extends React.Component { className={className} onClick={this.subscribe.bind(this, genesisHash)} > - {label} + {label} {nodeCount} diff --git a/frontend/src/components/Filter.tsx b/frontend/src/components/Filter.tsx index fc5c15e..291903e 100644 --- a/frontend/src/components/Filter.tsx +++ b/frontend/src/components/Filter.tsx @@ -105,9 +105,14 @@ export class Filter extends React.Component { }; private onWindowKeyUp = (event: KeyboardEvent) => { + // Ignore if control key is being pressed if (event.ctrlKey) { return; } + // Ignore events dispatched to other elements that want to use it + if (['INPUT', 'TEXTAREA'].includes((event.target as any)?.tagName)) { + return; + } const { value } = this.state; const key = event.key; diff --git a/frontend/src/components/List/THead.css b/frontend/src/components/List/THead.css index 8e5063d..3891da8 100644 --- a/frontend/src/components/List/THead.css +++ b/frontend/src/components/List/THead.css @@ -22,7 +22,7 @@ along with this program. If not, see . .THeadCell { text-align: left; - padding: 6px 13px; + padding: 8px 13px; height: 23px; } diff --git a/frontend/src/components/Tile.css b/frontend/src/components/Tile.css index 9281a06..bf7afa8 100644 --- a/frontend/src/components/Tile.css +++ b/frontend/src/components/Tile.css @@ -28,7 +28,7 @@ along with this program. If not, see . .Tile-label { position: absolute; top: 24px; - left: 100px; + left: 70px; right: 0; font-size: 0.4em; text-transform: uppercase; @@ -37,7 +37,7 @@ along with this program. If not, see . .Tile-content { position: absolute; bottom: 16px; - left: 100px; + left: 70px; right: 0; font-weight: 300; font-size: 0.75em; @@ -46,9 +46,9 @@ along with this program. If not, see . .Tile .Icon { position: absolute; left: 20px; - top: 20px; + top: 15px; font-size: 0.8em; - padding: 0.5em; + padding: 0.1em; border-radius: 1.25em; border: 2px solid #e6007a; color: #e6007a; diff --git a/frontend/src/index.css b/frontend/src/index.css index ec22f5f..32ad2f0 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -16,6 +16,11 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +* { + /* Use "standard"/common sense heights/widths (ie they include padding+border) */ + box-sizing: border-box; +} + body { margin: 0; padding: 0;