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:
James Wilson
2022-01-10 14:57:18 +00:00
committed by GitHub
parent b4fd955c78
commit c00cab33c9
10 changed files with 374 additions and 132 deletions
+76 -31
View File
@@ -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/>.
*/
.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;
}
+142 -36
View File
@@ -29,46 +29,152 @@ export namespace AllChains {
}
}
export class AllChains extends React.Component<AllChains.Props, {}> {
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<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';
const labelHtml = filterText ? labelWithFilterText(label, filterText) : label;
return (
<a key={label} className={className} onClick={onClick}>
{labelHtml}
<span className="AllChains-node-count" title="Node Count">
{nodeCount}
</span>
</a>
);
}
enum SortBy {
Alphabetical,
NumberOfNodes,
}
function labelWithFilterText(label: string, filterText: string) {
const idx = label.toLocaleLowerCase().indexOf(filterText);
if (idx > -1) {
return (
<>
<a className="AllChains-overlay" href={close} />
<div className="AllChains">
{chains.map((chain) => this.renderChain(chain))}
</div>
{label.slice(0, idx)}
<span className="AllChains-chain-highlighted-text">
{label.slice(idx, idx + filterText.length)}
</span>
{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 (
<a
key={label}
className={className}
onClick={this.subscribe.bind(this, genesisHash)}
>
{label}{' '}
<span className="AllChains-node-count" title="Node Count">
{nodeCount}
</span>
</a>
);
}
private async subscribe(chain: Types.GenesisHash) {
const connection = await this.props.connection;
connection.subscribe(chain);
} else {
return label;
}
}
+8 -7
View File
@@ -17,17 +17,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.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 {
+45 -13
View File
@@ -21,8 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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 <https://www.gnu.org/licenses/>.
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 <https://www.gnu.org/licenses/>.
}
.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;
}
+1 -1
View File
@@ -97,7 +97,7 @@ export class Chains extends React.Component<Chains.Props, {}> {
className={className}
onClick={this.subscribe.bind(this, genesisHash)}
>
{label}
<span>{label}</span>
<span className="Chains-node-count" title="Node Count">
{nodeCount}
</span>
+5
View File
@@ -105,9 +105,14 @@ export class Filter extends React.Component<Filter.Props, {}> {
};
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;
+1 -1
View File
@@ -22,7 +22,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
.THeadCell {
text-align: left;
padding: 6px 13px;
padding: 8px 13px;
height: 23px;
}
+4 -4
View File
@@ -28,7 +28,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
.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 <https://www.gnu.org/licenses/>.
.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 <https://www.gnu.org/licenses/>.
.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;
+5
View File
@@ -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/>.
*/
* {
/* Use "standard"/common sense heights/widths (ie they include padding+border) */
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;