mirror of
https://github.com/cryptoadvance/specter-desktop.git
synced 2026-08-13 12:33:29 +02:00
UIUX: Better node selection (#1987)
This commit is contained in:
parent
1f0915054c
commit
4cf86fb509
14 changed files with 144 additions and 82 deletions
|
|
@ -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()
|
||||
})
|
||||
|
||||
})
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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="<Transparent Rectangle>" class="cls-1" width="32" height="32"/></svg>
|
||||
|
After Width: | Height: | Size: 461 B |
|
|
@ -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="<Transparent Rectangle>" class="cls-1" width="32" height="32"/></svg>
|
||||
|
After Width: | Height: | Size: 461 B |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"/>
|
||||
|
|
|
|||
|
|
@ -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;}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
@ -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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue