mirror of
https://github.com/pezkuwichain/pezkuwi-telemetry.git
synced 2026-10-20 18:56:21 +00:00
Allow chains to be sorted and filtered (#440)
* Allow soak tests to generate lots of chains for testing * Style tweaks, and redo 'all chains' modal * make highlighted text readable on selected chain * cargo fmt * Update frontend/src/index.css Fix a typo Co-authored-by: Tarik Gul <47201679+TarikGul@users.noreply.github.com> * A couple more wee telemetry style tweaks * ..but make the tab animation faster * Be more defensive checking for event target * Comment out animation for now Co-authored-by: Tarik Gul <47201679+TarikGul@users.noreply.github.com>
This commit is contained in:
1 parent
b4fd955c78
commit
c00cab33c9
10 files changed
+339
-97
No files matched your search
@@ -104,36 +104,43 @@ async fn run_soak_test(opts: SoakTestOpts) {
|
|||||||
shard_ids.push(shard_id);
|
shard_ids.push(shard_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect nodes to each shard:
|
// Connect nodes to each shard for each chain:
|
||||||
let mut nodes = vec![];
|
let mut nodes = vec![];
|
||||||
|
for chain_name in chain_names(opts.chains) {
|
||||||
for &shard_id in &shard_ids {
|
for &shard_id in &shard_ids {
|
||||||
let mut conns = server
|
let conns = server
|
||||||
.get_shard(shard_id)
|
.get_shard(shard_id)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.connect_multiple_nodes(opts.nodes)
|
.connect_multiple_nodes(opts.nodes)
|
||||||
.await
|
.await
|
||||||
.expect("node connections failed");
|
.expect("node connections failed");
|
||||||
nodes.append(&mut conns);
|
nodes.push((chain_name.clone(), conns));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let genesis_hash = BlockHash::from_low_u64_be(1);
|
let first_genesis_hash = BlockHash::from_low_u64_be(1);
|
||||||
let genesis_hash_string = format!("{:0x}", genesis_hash);
|
let first_genesis_hash_string = format!("{:0x}", first_genesis_hash);
|
||||||
|
|
||||||
// Start nodes talking to the shards:
|
// Start nodes talking to the shards:
|
||||||
let bytes_in = Arc::new(AtomicUsize::new(0));
|
let bytes_in = Arc::new(AtomicUsize::new(0));
|
||||||
let ids_per_node = opts.ids_per_node;
|
let ids_per_node = opts.ids_per_node;
|
||||||
for node in nodes.into_iter().enumerate() {
|
|
||||||
let (idx, (tx, _)) = node;
|
// 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 {
|
for id in 0..ids_per_node {
|
||||||
let bytes_in = Arc::clone(&bytes_in);
|
let bytes_in = Arc::clone(&bytes_in);
|
||||||
let tx = tx.clone();
|
let tx = tx.clone();
|
||||||
|
let chain_name = chain_name.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let telemetry = test_utils::fake_telemetry::FakeTelemetry {
|
let telemetry = test_utils::fake_telemetry::FakeTelemetry {
|
||||||
block_time: Duration::from_secs(3),
|
block_time: Duration::from_secs(3),
|
||||||
node_name: format!("Node {}", (ids_per_node * idx) + id + 1),
|
node_name: format!("{} Node {}", chain_name, (ids_per_node * idx) + id + 1),
|
||||||
chain: "Polkadot".to_owned(),
|
chain: chain_name,
|
||||||
genesis_hash: genesis_hash,
|
genesis_hash: BlockHash::from_low_u64_be((i + 1) as u64),
|
||||||
message_id: id + 1,
|
message_id: id + 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -151,6 +158,7 @@ async fn run_soak_test(opts: SoakTestOpts) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Connect feeds to the core:
|
// Connect feeds to the core:
|
||||||
let mut feeds = server
|
let mut feeds = server
|
||||||
@@ -159,10 +167,10 @@ async fn run_soak_test(opts: SoakTestOpts) {
|
|||||||
.await
|
.await
|
||||||
.expect("feed connections failed");
|
.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 {
|
for (feed_tx, _) in &mut feeds {
|
||||||
feed_tx
|
feed_tx
|
||||||
.send_command("subscribe", &genesis_hash_string)
|
.send_command("subscribe", &first_genesis_hash_string)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +226,42 @@ async fn run_soak_test(opts: SoakTestOpts) {
|
|||||||
future::pending().await
|
future::pending().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return an iterator of `total` unique chain names.
|
||||||
|
fn chain_names(total: usize) -> impl Iterator<Item = String> {
|
||||||
|
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
|
/// 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
|
/// instructed by its documentation for full control over what is ran, or run
|
||||||
/// preconfigured variants.
|
/// preconfigured variants.
|
||||||
@@ -226,10 +270,14 @@ struct SoakTestOpts {
|
|||||||
/// The number of shards to run this test with
|
/// The number of shards to run this test with
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
shards: usize,
|
shards: usize,
|
||||||
/// The number of feeds to connect
|
/// The number of feeds to connect to the core
|
||||||
#[structopt(long)]
|
#[structopt(long)]
|
||||||
feeds: usize,
|
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)]
|
#[structopt(long)]
|
||||||
nodes: usize,
|
nodes: usize,
|
||||||
/// The number of different virtual nodes to connect per actual node socket connection
|
/// The number of different virtual nodes to connect per actual node socket connection
|
||||||
|
|||||||
@@ -16,64 +16,109 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.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 {
|
.AllChains-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: block;
|
|
||||||
z-index: 19;
|
z-index: 19;
|
||||||
background: rgba(0, 0, 0, 0.35);
|
background: rgba(0, 0, 0, 0.35);
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 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 {
|
.AllChains-chain {
|
||||||
padding: 0 12px;
|
padding: 10px 10px;
|
||||||
background: #b5aeae;
|
background: rgb(220,220,220);
|
||||||
color: #444;
|
color: black;
|
||||||
display: block;
|
display: inline-flex;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.5);
|
margin-right: 0.5em;
|
||||||
height: 40px;
|
margin-bottom: 0.5em;
|
||||||
line-height: 40px;
|
border-radius: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.AllChains-chain-highlighted-text {
|
||||||
|
background-color: yellow;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
.AllChains-node-count {
|
.AllChains-node-count {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
padding: 0 0.5em 0.1em;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
border-radius: 1em;
|
border-radius: 1em;
|
||||||
background: #8c8787;
|
background: #8c8787;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
text-shadow: rgba(0, 0, 0, 0.5) 0 1px 0;
|
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
line-height: 1.4em;
|
margin-left: 0.5em;
|
||||||
margin: 0 -0.3em 0 0.3em;
|
padding: 0.3em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.AllChains-chain-selected {
|
.AllChains-chain-selected {
|
||||||
background: #fff;
|
background: #e6007a;
|
||||||
color: #000;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.AllChains-chain-selected .AllChains-node-count {
|
.AllChains-chain-selected .AllChains-node-count {
|
||||||
background: #e6007a;
|
background: white;
|
||||||
|
color: #e6007a;
|
||||||
}
|
}
|
||||||
@@ -29,46 +29,152 @@ export namespace AllChains {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AllChains extends React.Component<AllChains.Props, {}> {
|
export function AllChains(props: AllChains.Props) {
|
||||||
public render() {
|
const { chains, subscribed, connection } = props;
|
||||||
const { chains, subscribed } = this.props;
|
const [filterText, setFilterText] = React.useState('');
|
||||||
const close = subscribed ? `#list/${subscribed}` : '#list';
|
const [sortBy, setSortBy] = React.useState(SortBy.NumberOfNodes);
|
||||||
|
|
||||||
return (
|
function close() {
|
||||||
<>
|
window.location.hash = subscribed ? `#list/${subscribed}` : '#list';
|
||||||
<a className="AllChains-overlay" href={close} />
|
|
||||||
<div className="AllChains">
|
|
||||||
{chains.map((chain) => this.renderChain(chain))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderChain(chain: ChainData): React.ReactNode {
|
function sortByAlphabetical() {
|
||||||
const { label, genesisHash, nodeCount } = chain;
|
setSortBy(SortBy.Alphabetical);
|
||||||
|
}
|
||||||
|
|
||||||
const className =
|
function sortByNumberOfNodes() {
|
||||||
genesisHash === this.props.subscribed
|
setSortBy(SortBy.NumberOfNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFilterText(ev: React.FormEvent<HTMLInputElement>) {
|
||||||
|
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) => (
|
||||||
|
<Chain
|
||||||
|
key={chain.genesisHash}
|
||||||
|
chain={chain}
|
||||||
|
filterText={filterText}
|
||||||
|
isSubscribed={subscribed === chain.genesisHash}
|
||||||
|
onClick={subscribeToChain(chain)}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
: 'No chains found';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="AllChains-overlay" onClick={close}>
|
||||||
|
<div className="AllChains-content" onClick={ignoreClicks}>
|
||||||
|
<div className="AllChains-controls">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Filter by chain name.."
|
||||||
|
value={filterText}
|
||||||
|
onChange={updateFilterText}
|
||||||
|
/>
|
||||||
|
<SortByControl
|
||||||
|
text="#nodes"
|
||||||
|
isActive={sortBy === SortBy.NumberOfNodes}
|
||||||
|
onClick={sortByNumberOfNodes}
|
||||||
|
/>
|
||||||
|
<SortByControl
|
||||||
|
text="A-Z"
|
||||||
|
isActive={sortBy === SortBy.Alphabetical}
|
||||||
|
onClick={sortByAlphabetical}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="AllChains-chains">{chainHtml}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div className={className} onClick={props.onClick}>
|
||||||
|
{props.text}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 AllChains-chain-selected'
|
||||||
: 'AllChains-chain';
|
: 'AllChains-chain';
|
||||||
|
|
||||||
|
const labelHtml = filterText ? labelWithFilterText(label, filterText) : label;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a key={label} className={className} onClick={onClick}>
|
||||||
key={label}
|
{labelHtml}
|
||||||
className={className}
|
|
||||||
onClick={this.subscribe.bind(this, genesisHash)}
|
|
||||||
>
|
|
||||||
{label}{' '}
|
|
||||||
<span className="AllChains-node-count" title="Node Count">
|
<span className="AllChains-node-count" title="Node Count">
|
||||||
{nodeCount}
|
{nodeCount}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async subscribe(chain: Types.GenesisHash) {
|
enum SortBy {
|
||||||
const connection = await this.props.connection;
|
Alphabetical,
|
||||||
|
NumberOfNodes,
|
||||||
|
}
|
||||||
|
|
||||||
connection.subscribe(chain);
|
function labelWithFilterText(label: string, filterText: string) {
|
||||||
|
const idx = label.toLocaleLowerCase().indexOf(filterText);
|
||||||
|
if (idx > -1) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{label.slice(0, idx)}
|
||||||
|
<span className="AllChains-chain-highlighted-text">
|
||||||
|
{label.slice(idx, idx + filterText.length)}
|
||||||
|
</span>
|
||||||
|
{label.slice(idx + filterText.length)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,17 +17,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
.Chain-Tab {
|
.Chain-Tab {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
font-size: 24px;
|
font-size: 18px;
|
||||||
line-height: 24px;
|
line-height: 20px;
|
||||||
height: 24px;
|
height: 32px;
|
||||||
width: 24px;
|
width: 32px;
|
||||||
padding: 6px;
|
|
||||||
color: #555;
|
color: #555;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 10px;
|
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
|
box-shadow: 0px 1px 3px 0px rgba(0,0,0,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.Chain-Tab:hover {
|
.Chain-Tab:hover {
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
color: #000;
|
color: #000;
|
||||||
padding: 0 76px 0 16px;
|
padding: 0 76px 0 16px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
/* min-width is 1350 - 76 - 16 to account for padding */
|
min-width: 1350px;
|
||||||
min-width: 1258px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,13 +29,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
top: 4px;
|
top: 4px;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
height: 36;
|
height: 36px;
|
||||||
line-height: 36px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
border-radius: 4px 4px 0 0;
|
border-radius: 4px 4px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,24 +69,55 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Chains-node-count {
|
.Chains-node-count {
|
||||||
padding: 0 5px;
|
padding: 0px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 20px;
|
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #e6007a;
|
color: #e6007a;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
line-height: 20px;
|
margin-left: 0.5em;
|
||||||
margin: 0 -0.5em 0 0.5em;
|
padding: 0.3em 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Chains-chain-selected {
|
/* Create a "tab background" that will rise up on hover/selection */
|
||||||
background: #fff;
|
.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;
|
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;
|
background: #393838;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
@@ -97,7 +97,7 @@ export class Chains extends React.Component<Chains.Props, {}> {
|
|||||||
className={className}
|
className={className}
|
||||||
onClick={this.subscribe.bind(this, genesisHash)}
|
onClick={this.subscribe.bind(this, genesisHash)}
|
||||||
>
|
>
|
||||||
{label}
|
<span>{label}</span>
|
||||||
<span className="Chains-node-count" title="Node Count">
|
<span className="Chains-node-count" title="Node Count">
|
||||||
{nodeCount}
|
{nodeCount}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -105,9 +105,14 @@ export class Filter extends React.Component<Filter.Props, {}> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onWindowKeyUp = (event: KeyboardEvent) => {
|
private onWindowKeyUp = (event: KeyboardEvent) => {
|
||||||
|
// Ignore if control key is being pressed
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
return;
|
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 { value } = this.state;
|
||||||
const key = event.key;
|
const key = event.key;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
.THeadCell {
|
.THeadCell {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 6px 13px;
|
padding: 8px 13px;
|
||||||
height: 23px;
|
height: 23px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
.Tile-label {
|
.Tile-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 24px;
|
top: 24px;
|
||||||
left: 100px;
|
left: 70px;
|
||||||
right: 0;
|
right: 0;
|
||||||
font-size: 0.4em;
|
font-size: 0.4em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@@ -37,7 +37,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
.Tile-content {
|
.Tile-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 16px;
|
bottom: 16px;
|
||||||
left: 100px;
|
left: 70px;
|
||||||
right: 0;
|
right: 0;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
font-size: 0.75em;
|
font-size: 0.75em;
|
||||||
@@ -46,9 +46,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
.Tile .Icon {
|
.Tile .Icon {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 20px;
|
left: 20px;
|
||||||
top: 20px;
|
top: 15px;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
padding: 0.5em;
|
padding: 0.1em;
|
||||||
border-radius: 1.25em;
|
border-radius: 1.25em;
|
||||||
border: 2px solid #e6007a;
|
border: 2px solid #e6007a;
|
||||||
color: #e6007a;
|
color: #e6007a;
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
* {
|
||||||
|
/* Use "standard"/common sense heights/widths (ie they include padding+border) */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|||||||
Reference in new issue
Block a user