Skip to content

Commit

Permalink
Merge #388: Add PeerDetails page
Browse files Browse the repository at this point in the history
02d3121 qml: Add PeerDetails page (johnny9)
1ed2188 qml: Introduce KeyValueRow control (jarolrod)
052b5be qml: Introduce minus icon (jarolrod)

Pull request description:

  Alternative to #387 which expands to think about a reusable row component, allowing to load any element(s) to the value field, not hardcoding in a height of 21 for a row (but still ensuring it's a minimum of 21), proper icon usage for N/A cases, dummy net indicators in place

  TODO:
  - Ban buttons
  - Hover hints
  - Disconnected state instead of flying back to the peers table
  - Smart text fixups

  <img width="752" alt="Screenshot 2024-02-28 at 3 53 43 AM" src="https://github.com/bitcoin-core/gui-qml/assets/23396902/7b3b8f7f-f490-4233-a540-736e34760116">

  Link to github actions build artifacts.

  [![Build Artifacts](https://img.shields.io/badge/Build%20Artifacts-green
  )]()

ACKs for top commit:
  MarnixCroes:
    tACK 02d3121
  D33r-Gee:
    tACK 02d3121
  pablomartin4btc:
    utACK 02d3121

Tree-SHA512: 26e3782e4cac2629bbeef40815edeaaddf59f91ec8f63017e5b8c223c4060c0a535b2640a7dbf34e4b25a20fa887a6bc109f7d57c5864a6d9896832db7f87b9f
  • Loading branch information
hebasto committed Sep 27, 2024
2 parents 43cdb75 + 02d3121 commit 7b056a0
Show file tree
Hide file tree
Showing 13 changed files with 510 additions and 0 deletions.
5 changes: 5 additions & 0 deletions src/Makefile.qt.include
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ QT_MOC_CPP = \
qml/models/moc_networktraffictower.cpp \
qml/models/moc_nodemodel.cpp \
qml/models/moc_options_model.cpp \
qml/models/moc_peerdetailsmodel.cpp \
qml/models/moc_peerlistsortproxy.cpp \
qml/models/moc_walletlistmodel.cpp \
qml/moc_appmode.cpp \
Expand Down Expand Up @@ -122,6 +123,7 @@ BITCOIN_QT_H = \
qml/models/networktraffictower.h \
qml/models/nodemodel.h \
qml/models/options_model.h \
qml/models/peerdetailsmodel.h \
qml/models/peerlistsortproxy.h \
qml/models/walletlistmodel.h \
qml/appmode.h \
Expand Down Expand Up @@ -312,6 +314,7 @@ BITCOIN_QML_BASE_CPP = \
qml/models/networktraffictower.cpp \
qml/models/nodemodel.cpp \
qml/models/options_model.cpp \
qml/models/peerdetailsmodel.cpp \
qml/models/peerlistsortproxy.cpp \
qml/models/walletlistmodel.cpp \
qml/imageprovider.cpp \
Expand Down Expand Up @@ -385,6 +388,7 @@ QML_RES_QML = \
qml/controls/Icon.qml \
qml/controls/InformationPage.qml \
qml/controls/IPAddressValueInput.qml \
qml/controls/KeyValueRow.qml \
qml/controls/NavButton.qml \
qml/controls/PageIndicator.qml \
qml/controls/NavigationBar.qml \
Expand All @@ -407,6 +411,7 @@ QML_RES_QML = \
qml/pages/node/NodeRunner.qml \
qml/pages/node/NodeSettings.qml \
qml/pages/node/Peers.qml \
qml/pages/node/PeerDetails.qml \
qml/pages/node/Shutdown.qml \
qml/pages/onboarding/OnboardingBlockclock.qml \
qml/pages/onboarding/OnboardingConnection.qml \
Expand Down
3 changes: 3 additions & 0 deletions src/qml/bitcoin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include <qml/models/networktraffictower.h>
#include <qml/models/nodemodel.h>
#include <qml/models/options_model.h>
#include <qml/models/peerdetailsmodel.h>
#include <qml/models/peerlistsortproxy.h>
#include <qml/models/walletlistmodel.h>
#include <qml/imageprovider.h>
Expand Down Expand Up @@ -315,6 +316,8 @@ int QmlGuiMain(int argc, char* argv[])
qmlRegisterSingletonInstance<AppMode>("org.bitcoincore.qt", 1, 0, "AppMode", &app_mode);
qmlRegisterType<BlockClockDial>("org.bitcoincore.qt", 1, 0, "BlockClockDial");
qmlRegisterType<LineGraph>("org.bitcoincore.qt", 1, 0, "LineGraph");
qmlRegisterUncreatableType<PeerDetailsModel>("org.bitcoincore.qt", 1, 0, "PeerDetailsModel", "");


engine.load(QUrl(QStringLiteral("qrc:///qml/pages/main.qml")));
if (engine.rootObjects().isEmpty()) {
Expand Down
3 changes: 3 additions & 0 deletions src/qml/bitcoin_qml.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
<file>controls/Icon.qml</file>
<file>controls/InformationPage.qml</file>
<file>controls/IPAddressValueInput.qml</file>
<file>controls/KeyValueRow.qml</file>
<file>controls/NavButton.qml</file>
<file>controls/PageIndicator.qml</file>
<file>controls/NavigationBar.qml</file>
Expand All @@ -51,6 +52,7 @@
<file>pages/node/NodeRunner.qml</file>
<file>pages/node/NodeSettings.qml</file>
<file>pages/node/Peers.qml</file>
<file>pages/node/PeerDetails.qml</file>
<file>pages/node/Shutdown.qml</file>
<file>pages/onboarding/OnboardingBlockclock.qml</file>
<file>pages/onboarding/OnboardingConnection.qml</file>
Expand Down Expand Up @@ -96,6 +98,7 @@
<file alias="gear-outline">res/icons/gear-outline.png</file>
<file alias="hidden">res/icons/hidden.png</file>
<file alias="info">res/icons/info.png</file>
<file alias="minus">res/icons/minus.png</file>
<file alias="network-dark">res/icons/network-dark.png</file>
<file alias="network-light">res/icons/network-light.png</file>
<file alias="plus">res/icons/plus.png</file>
Expand Down
32 changes: 32 additions & 0 deletions src/qml/controls/KeyValueRow.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2024 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import org.bitcoincore.qt 1.0

RowLayout {
id: root
property alias key: keyField.contentItem
property alias value: valueField.contentItem
width: parent.width

spacing: 10
Pane {
id: keyField
implicitWidth: 125
Layout.alignment: Qt.AlignLeft
background: null
padding: 0
}
Pane {
id: valueField
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
implicitHeight: Math.max(valueField.contentHeight, 21)
padding: 0
background: null
}
}
5 changes: 5 additions & 0 deletions src/qml/imageprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize
return QIcon(":/icons/info").pixmap(requested_size);
}

if (id == "minus") {
*size = requested_size;
return QIcon(":/icons/minus").pixmap(requested_size);
}

if (id == "network-dark") {
*size = requested_size;
return QIcon(":/icons/network-dark").pixmap(requested_size);
Expand Down
56 changes: 56 additions & 0 deletions src/qml/models/peerdetailsmodel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) 2024 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <qml/models/peerdetailsmodel.h>

PeerDetailsModel::PeerDetailsModel(CNodeCombinedStats* nodeStats, PeerTableModel* parent)
: m_combinedStats{nodeStats}
, m_model{parent}
, m_disconnected{false}
{
for (int row = 0; row < m_model->rowCount(); ++row) {
QModelIndex index = m_model->index(row, 0);
int nodeIdInRow = m_model->data(index, PeerTableModel::NetNodeId).toInt();
if (nodeIdInRow == m_combinedStats->nodeStats.nodeid) {
m_row = row;
break;
}
}
connect(parent, &PeerTableModel::rowsRemoved, this, &PeerDetailsModel::onModelRowsRemoved);
connect(parent, &PeerTableModel::dataChanged, this, &PeerDetailsModel::onModelDataChanged);
}

void PeerDetailsModel::onModelRowsRemoved(const QModelIndex& parent, int first, int last)
{
for (int row = first; row <= last; ++row) {
QModelIndex index = m_model->index(row, 0, parent);
int nodeIdInRow = m_model->data(index, PeerTableModel::NetNodeId).toInt();
if (nodeIdInRow == this->nodeId()) {
if (!m_disconnected) {
m_disconnected = true;
Q_EMIT disconnected();
}
break;
}
}
}

void PeerDetailsModel::onModelDataChanged(const QModelIndex& /* top_left */, const QModelIndex& /* bottom_right */)
{
if (m_model->data(m_model->index(m_row, 0), PeerTableModel::NetNodeId).isNull() ||
m_model->data(m_model->index(m_row, 0), PeerTableModel::NetNodeId).toInt() != nodeId()) {
if (!m_disconnected) {
m_disconnected = true;
Q_EMIT disconnected();
}
return;
}

m_combinedStats = m_model->data(m_model->index(m_row, 0), PeerTableModel::StatsRole).value<CNodeCombinedStats*>();

// Only update when all information is available
if (m_combinedStats && m_combinedStats->fNodeStateStatsAvailable) {
Q_EMIT dataChanged();
}
}
95 changes: 95 additions & 0 deletions src/qml/models/peerdetailsmodel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Copyright (c) 2024 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#ifndef BITCOIN_QML_MODELS_PEERDETAILSMODEL_H
#define BITCOIN_QML_MODELS_PEERDETAILSMODEL_H

#include <QObject>

#include <qt/guiutil.h>
#include <qt/peertablemodel.h>
#include <qt/rpcconsole.h>
#include <util/time.h>

class PeerDetailsModel : public QObject
{
Q_OBJECT
Q_PROPERTY(int nodeId READ nodeId NOTIFY dataChanged)
Q_PROPERTY(QString address READ address NOTIFY dataChanged)
Q_PROPERTY(QString addressLocal READ addressLocal NOTIFY dataChanged)
Q_PROPERTY(QString type READ type NOTIFY dataChanged)
Q_PROPERTY(QString version READ version NOTIFY dataChanged)
Q_PROPERTY(QString userAgent READ userAgent NOTIFY dataChanged)
Q_PROPERTY(QString services READ services NOTIFY dataChanged)
Q_PROPERTY(bool transactionRelay READ transactionRelay NOTIFY dataChanged)
Q_PROPERTY(bool addressRelay READ addressRelay NOTIFY dataChanged)
Q_PROPERTY(QString startingHeight READ startingHeight NOTIFY dataChanged)
Q_PROPERTY(QString syncedHeaders READ syncedHeaders NOTIFY dataChanged)
Q_PROPERTY(QString syncedBlocks READ syncedBlocks NOTIFY dataChanged)
Q_PROPERTY(QString direction READ direction NOTIFY dataChanged)
Q_PROPERTY(QString connectionDuration READ connectionDuration NOTIFY dataChanged)
Q_PROPERTY(QString lastSend READ lastSend NOTIFY dataChanged)
Q_PROPERTY(QString lastReceived READ lastReceived NOTIFY dataChanged)
Q_PROPERTY(QString bytesSent READ bytesSent NOTIFY dataChanged)
Q_PROPERTY(QString bytesReceived READ bytesReceived NOTIFY dataChanged)
Q_PROPERTY(QString pingTime READ pingTime NOTIFY dataChanged)
Q_PROPERTY(QString pingWait READ pingWait NOTIFY dataChanged)
Q_PROPERTY(QString pingMin READ pingMin NOTIFY dataChanged)
Q_PROPERTY(QString timeOffset READ timeOffset NOTIFY dataChanged)
Q_PROPERTY(QString mappedAS READ mappedAS NOTIFY dataChanged)
Q_PROPERTY(QString permission READ permission NOTIFY dataChanged)

public:
explicit PeerDetailsModel(CNodeCombinedStats* nodeStats, PeerTableModel* model);

int nodeId() const { return m_combinedStats->nodeStats.nodeid; }
QString address() const { return QString::fromStdString(m_combinedStats->nodeStats.m_addr_name); }
QString addressLocal() const { return QString::fromStdString(m_combinedStats->nodeStats.addrLocal); }
QString type() const { return GUIUtil::ConnectionTypeToQString(m_combinedStats->nodeStats.m_conn_type, /*prepend_direction=*/true); }
QString version() const { return QString::number(m_combinedStats->nodeStats.nVersion); }
QString userAgent() const { return QString::fromStdString(m_combinedStats->nodeStats.cleanSubVer); }
QString services() const { return GUIUtil::formatServicesStr(m_combinedStats->nodeStateStats.their_services); }
bool transactionRelay() const { return m_combinedStats->nodeStateStats.m_relay_txs; }
bool addressRelay() const { return m_combinedStats->nodeStateStats.m_addr_relay_enabled; }
QString startingHeight() const { return QString::number(m_combinedStats->nodeStateStats.m_starting_height); }
QString syncedHeaders() const { return QString::number(m_combinedStats->nodeStateStats.nSyncHeight); }
QString syncedBlocks() const { return QString::number(m_combinedStats->nodeStateStats.nCommonHeight); }
QString direction() const { return QString::fromStdString(m_combinedStats->nodeStats.fInbound ? "Inbound" : "Outbound"); }
QString connectionDuration() const { return GUIUtil::formatDurationStr(GetTime<std::chrono::seconds>() - m_combinedStats->nodeStats.m_connected); }
QString lastSend() const { return GUIUtil::formatDurationStr(GetTime<std::chrono::seconds>() - m_combinedStats->nodeStats.m_last_send); }
QString lastReceived() const { return GUIUtil::formatDurationStr(GetTime<std::chrono::seconds>() - m_combinedStats->nodeStats.m_last_recv); }
QString bytesSent() const { return GUIUtil::formatBytes(m_combinedStats->nodeStats.nSendBytes); }
QString bytesReceived() const { return GUIUtil::formatBytes(m_combinedStats->nodeStats.nRecvBytes); }
QString pingTime() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStats.m_last_ping_time); }
QString pingMin() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStats.m_min_ping_time); }
QString pingWait() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStateStats.m_ping_wait); }
QString timeOffset() const { return GUIUtil::formatTimeOffset(m_combinedStats->nodeStats.nTimeOffset); }
QString mappedAS() const { return m_combinedStats->nodeStats.m_mapped_as != 0 ? QString::number(m_combinedStats->nodeStats.m_mapped_as) : tr("N/A"); }
QString permission() const {
if (m_combinedStats->nodeStats.m_permission_flags == NetPermissionFlags::None) {
return tr("N/A");
}
QStringList permissions;
for (const auto& permission : NetPermissions::ToStrings(m_combinedStats->nodeStats.m_permission_flags)) {
permissions.append(QString::fromStdString(permission));
}
return permissions.join(" & ");
}

Q_SIGNALS:
void dataChanged();
void disconnected();

private Q_SLOTS:
void onModelRowsRemoved(const QModelIndex& parent, int first, int last);
void onModelDataChanged(const QModelIndex& top_left, const QModelIndex& bottom_right);

private:
int m_row;
CNodeCombinedStats* m_combinedStats;
PeerTableModel* m_model;
bool m_disconnected;
};

#endif // BITCOIN_QML_MODELS_PEERDETAILSMODEL_H
6 changes: 6 additions & 0 deletions src/qml/models/peerlistsortproxy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include <qml/models/peerlistsortproxy.h>
#include <qml/models/peerdetailsmodel.h>
#include <qt/peertablemodel.h>

PeerListSortProxy::PeerListSortProxy(QObject* parent)
Expand All @@ -23,6 +24,7 @@ QHash<int, QByteArray> PeerListSortProxy::roleNames() const
roles[PeerTableModel::Sent] = "sent";
roles[PeerTableModel::Received] = "received";
roles[PeerTableModel::Subversion] = "subversion";
roles[PeerTableModel::StatsRole] = "stats";
return roles;
}

Expand All @@ -40,6 +42,10 @@ int PeerListSortProxy::RoleNameToIndex(const QString & name) const
QVariant PeerListSortProxy::data(const QModelIndex& index, int role) const
{
if (role == PeerTableModel::StatsRole) {
auto stats = PeerTableSortProxy::data(index, role);
auto details = new PeerDetailsModel(stats.value<CNodeCombinedStats*>(), qobject_cast<PeerTableModel*>(sourceModel()));
return QVariant::fromValue(details);
} else if (role == PeerTableModel::NetNodeId) {
return PeerTableSortProxy::data(index, role);
}

Expand Down
11 changes: 11 additions & 0 deletions src/qml/pages/node/NodeSettings.qml
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,17 @@ Item {
nodeSettingsView.pop()
peerTableModel.stopAutoRefresh();
}
onPeerSelected: (peerDetails) => {
nodeSettingsView.push(peer_details, {"details": peerDetails})
}
}
}
Component {
id: peer_details
PeerDetails {
onBackClicked: {
nodeSettingsView.pop()
}
}
}
Component {
Expand Down
Loading

0 comments on commit 7b056a0

Please sign in to comment.