UIUX: Better node selection (#1987)

This commit is contained in:
Manolis Mandrapilias 2022-12-20 20:58:43 +01:00 committed by GitHub
parent 1f0915054c
commit 4cf86fb509
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 144 additions and 82 deletions

View file

@ -46,7 +46,7 @@ describe('Configuring nodes', () => {
cy.viewport(1200,660)
cy.visit('/')
cy.get('#node-switch-icon').click()
cy.get('#btn_new_node').click()
cy.get('[data-cy="connect-new-node-btn"]').click()
cy.get('[href="/nodes/new_node/"]').click()
cy.get('#name').clear()
cy.get('#name').type("Elements Node")
@ -83,7 +83,7 @@ describe('Configuring nodes', () => {
cy.viewport(1200,660)
cy.visit('/')
cy.get('#node-switch-icon').click()
cy.get('#default-select-node-form > .item > div').click()
cy.contains('Bitcoin Core').click()
})
})

View file

@ -4,8 +4,7 @@ describe('Operating with an Elements multisig wallet', () => {
cy.viewport(1200,660)
cy.visit('/')
cy.get('#node-switch-icon').click()
cy.get('#elements_node-select-node-form > .item > div').click()
cy.get('[value="save"]').click()
cy.contains('Elements Node').click()
// Delete wallets if existing
cy.deleteWallet("Elm Multi Segwit Wallet")

View file

@ -3,7 +3,7 @@ describe('Operating with an elements singlesig wallet', () => {
cy.viewport(1300,660)
cy.visit('/')
cy.get('#node-switch-icon').click()
cy.get('#elements_node-select-node-form > .item > div').click()
cy.contains('Elements Node').click()
// Delete Wallet if existing
cy.deleteWallet("Elm Single Segwit Hot Wallet")

View file

@ -325,7 +325,7 @@ idna==3.4 \
--hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
--hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
# via requests
importlib-metadata==4.8.1 \
importlib_metadata==4.8.1 \
--hash=sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15 \
--hash=sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1
# via -r requirements.in
@ -598,6 +598,10 @@ pytimeparse==1.1.8 \
--hash=sha256:04b7be6cc8bd9f5647a6325444926c3ac34ee6bc7e69da4367ba282f076036bd \
--hash=sha256:e86136477be924d7e670646a98561957e8ca7308d44841e21f5ddea757556a0a
# via -r requirements.in
pytz-deprecation-shim==0.1.0.post0 \
--hash=sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6 \
--hash=sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d
# via tzlocal
pytz==2022.5 \
--hash=sha256:335ab46900b1465e714b4fda4963d87363264eb662aab5e65da039c25f1f5b22 \
--hash=sha256:c4d88f472f54d615e9cd582a5004d1e5f624854a6a27a6211591c251f22a6914
@ -606,10 +610,6 @@ pytz==2022.5 \
# babel
# flask-babel
# flask-restful
pytz-deprecation-shim==0.1.0.post0 \
--hash=sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6 \
--hash=sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d
# via tzlocal
requests==2.26.0 \
--hash=sha256:6c1246513ecd5ecd4528a0906f910e8f0f9c6b8ec72030dc9fd154dc1a6efd24 \
--hash=sha256:b8aa58f8cf793ffd8782d3d8cb19e66ef36f7aba4353eec859e74678b01b07a7

View file

@ -174,6 +174,12 @@ class NodeManager:
return node
raise SpecterError("Node name %s does not exist!" % name)
def get_name_from_alias(self, alias: str) -> str:
for node in self.nodes.values():
if node.alias == alias:
return node.name
raise SpecterError("Node alias %s does not exist!" % alias)
def update_bitcoind_version(self, specter, version):
stopped_nodes = []
for node in (node for node in self.nodes.values() if not node.external_node):

View file

@ -372,5 +372,7 @@ def internal_node_logs(node_alias):
@login_required
def switch_node():
node_alias = request.form["node_alias"]
node_name = app.specter.node_manager.get_name_from_alias(node_alias)
app.specter.update_active_node(node_alias)
return redirect(url_for("nodes_endpoint.node_settings", node_alias=node_alias))
flash(_(f"Switched to use {node_name} as node."))
return redirect(url_for("index"))

View file

@ -0,0 +1 @@
<svg fill="#43ff64d9" width="32px" height="32px" viewBox="0 0 32 32" id="icon" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><title>radio-button--checked</title><path d="M16,2A14,14,0,1,0,30,16,14,14,0,0,0,16,2Zm0,26A12,12,0,1,1,28,16,12,12,0,0,1,16,28Z"/><path d="M16,10a6,6,0,1,0,6,6A6,6,0,0,0,16,10Z"/><rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" class="cls-1" width="32" height="32"/></svg>

After

Width:  |  Height:  |  Size: 461 B

View file

@ -0,0 +1 @@
<svg fill="#ffc864d9" width="32px" height="32px" viewBox="0 0 32 32" id="icon" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;}</style></defs><title>radio-button--checked</title><path d="M16,2A14,14,0,1,0,30,16,14,14,0,0,0,16,2Zm0,26A12,12,0,1,1,28,16,12,12,0,0,1,16,28Z"/><path d="M16,10a6,6,0,1,0,6,6A6,6,0,0,0,16,10Z"/><rect id="_Transparent_Rectangle_" data-name="&lt;Transparent Rectangle&gt;" class="cls-1" width="32" height="32"/></svg>

After

Width:  |  Height:  |  Size: 461 B

View file

@ -277,25 +277,25 @@ a.item:hover{
opacity: 1;
}
/* disconnected node */
svg.core .main{
svg.core {
fill: var(--network-color, #79869B);
}
svg.core.test .main{
svg.core.test {
fill: var(--test-color);
}
svg.core.testnet .main{
svg.core.testnet {
fill: var(--test-color);
}
svg.core.main .main{
svg.core.main {
fill: var(--main-color);
}
svg.core.mainnet .main{
svg.core.mainnet {
fill: var(--main-color);
}
svg.core.regtest .main{
svg.core.regtest {
fill: var(--regtest-color);
}
svg.core.signet .main{
svg.core.signet {
fill: var(--signet-color);
}
small, .small{
@ -1002,6 +1002,10 @@ table tr:hover .btn.hovering{
filter: invert(100%) sepia(0%) saturate(1%) hue-rotate(304deg) brightness(102%) contrast(101%);
}
.svg-gray {
filter: invert(75%) sepia(0%) saturate(1%) hue-rotate(304deg) brightness(102%) contrast(101%);
}
/************** Mobile styles ********************************/
#side-expand{
display: none;

View file

@ -4,8 +4,8 @@
- chain: Chain to use to colorize the Bitcoin icon
- size: Svg width and height
#}
{% macro bitcoin_svg(chain, size) -%}
<svg class="core {{ chain }}" height="{{ size }}" width="{{ size }}" viewBox="0 0 64 64" style="opacity: 0.95;">
{% macro bitcoin_svg(node, size) -%}
<svg class="core {{ node.chain }}" height="{{ size }}" width="{{ size }}" viewBox="0 0 64 64" style="opacity: 0.95;">
<g>
<path class="main" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
<path fill="#ffff" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>

View file

@ -4,8 +4,8 @@
- chain: Chain to use to colorize the Bitcoin icon
- size: Svg width and height
#}
{% macro liquid_svg(chain, size) -%}
<svg class="core {{ chain }}" height="{{ size }}" width="{{ size }}" viewBox="0 0 46 46" style="opacity: 0.95;">
{% macro liquid_svg(node, size) -%}
<svg class="core {{ node.chain }}" height="{{ size }}" width="{{ size }}" viewBox="0 0 46 46" style="opacity: 0.95;">
<style type="text/css">
.st0{fill:#46B4A5;}
.st1{fill:#FFFFFF;}

View file

@ -1,35 +1,21 @@
{% if specter.node.is_running %}
{% set node = specter.node %}
{% if node.is_running %}
<div id="active-node" onclick="showPageOverlay('bitcoin_core_info');document.getElementById('side-content').classList.remove('active');" class="item core" style="cursor: pointer;">
{% if specter.is_liquid %}
{% from "components/liquid_svg.jinja" import liquid_svg %}
{{ liquid_svg(specter.info.chain, 40) }}
{% else %}
{% from "components/bitcoin_svg.jinja" import bitcoin_svg %}
{{ bitcoin_svg(specter.info.chain, 40) }}
{% endif %}
{% include node.node_logo_template() %}
{% include "includes/sidebar/components/bitcoin_core_info.jinja" %}
<div>
<span style="max-width: 140px;">{{ specter.node.name }}</span>
<span style="max-width: 140px;">{{ node.name }}</span> <br>
<small>
{% if specter.chain %}
{% if specter.bitcoin_core_version != '' %}
v{{ specter.bitcoin_core_version }}
{% endif %}
({{specter.chain}}):
{% if specter.info["blocks"] %}
{{ specter.info.blocks }} blocks
{% else %}
{{ specter.info.chain }}
{% endif %}
{{node.chain | title}} node
{% endif %}
</small>
</div>
</div>
{% else %}
<a id="no-node-connection" class="no-connection" href="{{ url_for('nodes_endpoint.node_settings', node_alias=specter.node.alias) }}">
<a id="no-node-connection" class="no-connection" href="{{ url_for('nodes_endpoint.node_settings', node_alias=node.alias) }}">
<div style="display: flex; gap: 5px">
<span>{{ _("No node connection") }}</span>
<span>{{ _("No Bitcoin Core connection") }}</span>
<img src="{{ url_for('static', filename='img/broken-connection.svg') }}" style="width: 15px;">
</div>
<div id="configure-click" class="configure-click">{{ _("Click to configure") }}</div>

View file

@ -1,7 +1,7 @@
{% if specter.node.is_liquid %}
{% from "components/liquid_svg.jinja" import liquid_svg %}
{{ liquid_svg(specter.node.chain, 40) }}
{% if node.is_liquid %}
{% from "components/liquid_svg.jinja" import liquid_svg %}
{{ liquid_svg(node.chain, 40) }}
{% else %}
{% from "components/bitcoin_svg.jinja" import bitcoin_svg %}
{{ bitcoin_svg(specter.node.chain, 40) }}
{% from "components/bitcoin_svg.jinja" import bitcoin_svg %}
{{ bitcoin_svg(node, 40) }}
{% endif %}

View file

@ -4,72 +4,135 @@
justify-content: flex-start;
flex-grow: 1;
width: 100%;
padding: 10px 0;
padding: 10px;
display: flex;
flex-direction: row;
align-items: center;
border-left: 3px solid transparent;
border-left: 3px solid transparent;
width: 275px;
height: 60px;
cursor: pointer;
border: 1px solid transparent;
border-radius: 10px;
}
.node-list-item-selected {
border: 1px solid orange;
border-radius: 10px;
cursor: unset;
}
.node-list-item>svg {
width: 80px;
}
.node-list-item.active{
background: #394659;
}
@media (min-width: 600px){
#node_select_popup {
width: 350px;
}
}
.page_overlay_popup {
padding: '1.5em 0';
text-align: unset;
}
.node-select-popup-container {
flex-direction: column;
align-items: start;
gap: 10px;
}
.selected-icon {
width: 20px
}
.node-logo-unreachable {
margin-right: 15px;
opacity: 0.1;
}
.node-logo-running {
margin-right: 15px;
opacity: 1;
}
.node-description {
display: flex;
flex-direction: column;
}
.node-row {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.node-name {
color: white
}
.node-list-item:hover {
border: 1px dotted orange;
border-radius: 10px;
}
.node-list-item-selected:hover {
cursor: default;
border: 1px solid orange;
border-radius: 10px;
}
</style>
<div id="node_select_popup" class="hidden" style="text-align: left;">
<h1>{{ _("Node configuration") }}</h1>
<div id="node_select_popup" class="hidden">
<h1 style="align-self: center">{{ _("Node selection") }}</h1>
{% for node_name in specter.node_manager.nodes_names %}
{% set node = specter.node_manager.get_by_name(node_name) %}
<form action="{{url_for('nodes_endpoint.switch_node')}}" id="{{node.alias}}-select-node-form" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="node_alias" value="{{ node.alias }}"/>
<div class="item core node-list-item {% if node.alias == specter.node.alias %}active{% endif %}" style="cursor: pointer;"
onclick="document.getElementById('{{node.alias}}-select-node-form').submit();">
{% include node.node_logo_template() %}
<div style="width: 100%;">
{{ node.name }}
<br>
<small>
{% if node.chain %}
{{node.chain | title}}, {{ node.host }}:{{ node.port }}
{% else %}
{{ _("Node unreachable...") }}
{% endif %}
</small>
{% set selected_node = node.alias == specter.node.alias %}
{% set running = node.is_running %}
<div id="node-row" class="node-row">
<form action="{{url_for('nodes_endpoint.switch_node')}}" id="{{node.alias}}-select-node-form" method="POST">
<input type="hidden" class="csrf-token" name="csrf_token" value="{{ csrf_token() }}"/>
<input type="hidden" name="node_alias" value="{{ node.alias }}"/>
<div {% if selected_node %} class="core node-list-item node-list-item-selected" {% else %} class="core node-list-item" {% endif %} onclick="document.getElementById('{{node.alias}}-select-node-form').submit();">
<div id="node-logo" {% if running %} class="node-logo-running" {% else %} class="node-logo-unreachable" {% endif %}>
{% include node.node_logo_template() %}
</div>
<div id="node-description" class="node-description" {% if selected_node %} title="Selected node" {% else %} title="Select node" {% endif%}>
<div style="display: flex; gap: 10px">
<div class="node-name">{{ node.name }}</div>
{% if running %}
<img title="Node is running" class="selected-icon"src="{{ url_for('static', filename='img/radio-button-running-node.svg')}}">
{% endif %}
</div>
<small>
{% if running %}
{{node.chain | title}} node
{% else %}
<span style="color: grey; font-size: 12px">Node unreachable</span>
{% endif %}
</small>
</div>
</div>
<a href="{{ url_for('nodes_endpoint.node_settings', node_alias=node.alias) }}" style="text-align: right; width: 100%px;">
<img src="{{ url_for('static', filename='img/gear.svg')}}" class="svg-white" style="width: 40px; margin-right: 10px;"/>
</a>
</div>
</form>
</form>
<a title="Configure node" href="{{ url_for('nodes_endpoint.node_settings', node_alias=node.alias) }}">
<img src="{{ url_for('static', filename='img/gear.svg')}}" class="svg-gray" style="width: 40px; margin-right: 10px;"/>
</a>
</div>
{% endfor %}
<br>
{{ sidebar_btn(url_for('setup_endpoint.node_type'), 'Connect a new node', 'btn_new_node') }}
<div style="align-self: center; margin-top: 10px">
<a href="{{ url_for('setup_endpoint.node_type') }}" class="btn" data-cy="connect-new-node-btn">Connect a new node</a>
</div>
</div>
<script type="text/javascript">
function showNodeSelectPopup() {
// Show node selection popup when clicking on the arrows on the sidebar
hidePageOverlay();
if (!e) var e = window.event;
e.cancelBubble = true;
if (e.stopPropagation) {
e.stopPropagation();
}
document.getElementById('page_overlay_popup').style.padding = '1.5em 0';
document.getElementById('page_overlay_popup_cancel_button').classList.add('hidden');
document.getElementById('side-content').classList.remove('active');
showPageOverlay('node_select_popup');
// Change the default styling of the node selection popup
let pageOverlayPopup = document.getElementById('page_overlay_popup')
pageOverlayPopup.classList.add('page_overlay_popup')
let nodeSelectPopupContainer = document.getElementById('node_select_popup');
nodeSelectPopupContainer.style.display = 'flex'
nodeSelectPopupContainer.classList.add('node-select-popup-container')
}
function onCancelOverlay() {
document.getElementById('page_overlay_popup').style.padding = '1.5em';
document.getElementById('page_overlay_popup_cancel_button').classList.remove('hidden');
}
</script>