Skip to content

Commit 7b056a0

Browse files
committed
Merge #388: Add PeerDetails page
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
2 parents 43cdb75 + 02d3121 commit 7b056a0

13 files changed

+510
-0
lines changed

src/Makefile.qt.include

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ QT_MOC_CPP = \
4141
qml/models/moc_networktraffictower.cpp \
4242
qml/models/moc_nodemodel.cpp \
4343
qml/models/moc_options_model.cpp \
44+
qml/models/moc_peerdetailsmodel.cpp \
4445
qml/models/moc_peerlistsortproxy.cpp \
4546
qml/models/moc_walletlistmodel.cpp \
4647
qml/moc_appmode.cpp \
@@ -122,6 +123,7 @@ BITCOIN_QT_H = \
122123
qml/models/networktraffictower.h \
123124
qml/models/nodemodel.h \
124125
qml/models/options_model.h \
126+
qml/models/peerdetailsmodel.h \
125127
qml/models/peerlistsortproxy.h \
126128
qml/models/walletlistmodel.h \
127129
qml/appmode.h \
@@ -312,6 +314,7 @@ BITCOIN_QML_BASE_CPP = \
312314
qml/models/networktraffictower.cpp \
313315
qml/models/nodemodel.cpp \
314316
qml/models/options_model.cpp \
317+
qml/models/peerdetailsmodel.cpp \
315318
qml/models/peerlistsortproxy.cpp \
316319
qml/models/walletlistmodel.cpp \
317320
qml/imageprovider.cpp \
@@ -385,6 +388,7 @@ QML_RES_QML = \
385388
qml/controls/Icon.qml \
386389
qml/controls/InformationPage.qml \
387390
qml/controls/IPAddressValueInput.qml \
391+
qml/controls/KeyValueRow.qml \
388392
qml/controls/NavButton.qml \
389393
qml/controls/PageIndicator.qml \
390394
qml/controls/NavigationBar.qml \
@@ -407,6 +411,7 @@ QML_RES_QML = \
407411
qml/pages/node/NodeRunner.qml \
408412
qml/pages/node/NodeSettings.qml \
409413
qml/pages/node/Peers.qml \
414+
qml/pages/node/PeerDetails.qml \
410415
qml/pages/node/Shutdown.qml \
411416
qml/pages/onboarding/OnboardingBlockclock.qml \
412417
qml/pages/onboarding/OnboardingConnection.qml \

src/qml/bitcoin.cpp

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
#include <qml/models/networktraffictower.h>
2626
#include <qml/models/nodemodel.h>
2727
#include <qml/models/options_model.h>
28+
#include <qml/models/peerdetailsmodel.h>
2829
#include <qml/models/peerlistsortproxy.h>
2930
#include <qml/models/walletlistmodel.h>
3031
#include <qml/imageprovider.h>
@@ -315,6 +316,8 @@ int QmlGuiMain(int argc, char* argv[])
315316
qmlRegisterSingletonInstance<AppMode>("org.bitcoincore.qt", 1, 0, "AppMode", &app_mode);
316317
qmlRegisterType<BlockClockDial>("org.bitcoincore.qt", 1, 0, "BlockClockDial");
317318
qmlRegisterType<LineGraph>("org.bitcoincore.qt", 1, 0, "LineGraph");
319+
qmlRegisterUncreatableType<PeerDetailsModel>("org.bitcoincore.qt", 1, 0, "PeerDetailsModel", "");
320+
318321

319322
engine.load(QUrl(QStringLiteral("qrc:///qml/pages/main.qml")));
320323
if (engine.rootObjects().isEmpty()) {

src/qml/bitcoin_qml.qrc

+3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
<file>controls/Icon.qml</file>
3030
<file>controls/InformationPage.qml</file>
3131
<file>controls/IPAddressValueInput.qml</file>
32+
<file>controls/KeyValueRow.qml</file>
3233
<file>controls/NavButton.qml</file>
3334
<file>controls/PageIndicator.qml</file>
3435
<file>controls/NavigationBar.qml</file>
@@ -51,6 +52,7 @@
5152
<file>pages/node/NodeRunner.qml</file>
5253
<file>pages/node/NodeSettings.qml</file>
5354
<file>pages/node/Peers.qml</file>
55+
<file>pages/node/PeerDetails.qml</file>
5456
<file>pages/node/Shutdown.qml</file>
5557
<file>pages/onboarding/OnboardingBlockclock.qml</file>
5658
<file>pages/onboarding/OnboardingConnection.qml</file>
@@ -96,6 +98,7 @@
9698
<file alias="gear-outline">res/icons/gear-outline.png</file>
9799
<file alias="hidden">res/icons/hidden.png</file>
98100
<file alias="info">res/icons/info.png</file>
101+
<file alias="minus">res/icons/minus.png</file>
99102
<file alias="network-dark">res/icons/network-dark.png</file>
100103
<file alias="network-light">res/icons/network-light.png</file>
101104
<file alias="plus">res/icons/plus.png</file>

src/qml/controls/KeyValueRow.qml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright (c) 2024 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
import QtQuick 2.15
6+
import QtQuick.Controls 2.15
7+
import QtQuick.Layouts 1.15
8+
import org.bitcoincore.qt 1.0
9+
10+
RowLayout {
11+
id: root
12+
property alias key: keyField.contentItem
13+
property alias value: valueField.contentItem
14+
width: parent.width
15+
16+
spacing: 10
17+
Pane {
18+
id: keyField
19+
implicitWidth: 125
20+
Layout.alignment: Qt.AlignLeft
21+
background: null
22+
padding: 0
23+
}
24+
Pane {
25+
id: valueField
26+
Layout.fillWidth: true
27+
Layout.alignment: Qt.AlignLeft
28+
implicitHeight: Math.max(valueField.contentHeight, 21)
29+
padding: 0
30+
background: null
31+
}
32+
}

src/qml/imageprovider.cpp

+5
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize
107107
return QIcon(":/icons/info").pixmap(requested_size);
108108
}
109109

110+
if (id == "minus") {
111+
*size = requested_size;
112+
return QIcon(":/icons/minus").pixmap(requested_size);
113+
}
114+
110115
if (id == "network-dark") {
111116
*size = requested_size;
112117
return QIcon(":/icons/network-dark").pixmap(requested_size);

src/qml/models/peerdetailsmodel.cpp

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// Copyright (c) 2024 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#include <qml/models/peerdetailsmodel.h>
6+
7+
PeerDetailsModel::PeerDetailsModel(CNodeCombinedStats* nodeStats, PeerTableModel* parent)
8+
: m_combinedStats{nodeStats}
9+
, m_model{parent}
10+
, m_disconnected{false}
11+
{
12+
for (int row = 0; row < m_model->rowCount(); ++row) {
13+
QModelIndex index = m_model->index(row, 0);
14+
int nodeIdInRow = m_model->data(index, PeerTableModel::NetNodeId).toInt();
15+
if (nodeIdInRow == m_combinedStats->nodeStats.nodeid) {
16+
m_row = row;
17+
break;
18+
}
19+
}
20+
connect(parent, &PeerTableModel::rowsRemoved, this, &PeerDetailsModel::onModelRowsRemoved);
21+
connect(parent, &PeerTableModel::dataChanged, this, &PeerDetailsModel::onModelDataChanged);
22+
}
23+
24+
void PeerDetailsModel::onModelRowsRemoved(const QModelIndex& parent, int first, int last)
25+
{
26+
for (int row = first; row <= last; ++row) {
27+
QModelIndex index = m_model->index(row, 0, parent);
28+
int nodeIdInRow = m_model->data(index, PeerTableModel::NetNodeId).toInt();
29+
if (nodeIdInRow == this->nodeId()) {
30+
if (!m_disconnected) {
31+
m_disconnected = true;
32+
Q_EMIT disconnected();
33+
}
34+
break;
35+
}
36+
}
37+
}
38+
39+
void PeerDetailsModel::onModelDataChanged(const QModelIndex& /* top_left */, const QModelIndex& /* bottom_right */)
40+
{
41+
if (m_model->data(m_model->index(m_row, 0), PeerTableModel::NetNodeId).isNull() ||
42+
m_model->data(m_model->index(m_row, 0), PeerTableModel::NetNodeId).toInt() != nodeId()) {
43+
if (!m_disconnected) {
44+
m_disconnected = true;
45+
Q_EMIT disconnected();
46+
}
47+
return;
48+
}
49+
50+
m_combinedStats = m_model->data(m_model->index(m_row, 0), PeerTableModel::StatsRole).value<CNodeCombinedStats*>();
51+
52+
// Only update when all information is available
53+
if (m_combinedStats && m_combinedStats->fNodeStateStatsAvailable) {
54+
Q_EMIT dataChanged();
55+
}
56+
}

src/qml/models/peerdetailsmodel.h

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
// Copyright (c) 2024 The Bitcoin Core developers
2+
// Distributed under the MIT software license, see the accompanying
3+
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
4+
5+
#ifndef BITCOIN_QML_MODELS_PEERDETAILSMODEL_H
6+
#define BITCOIN_QML_MODELS_PEERDETAILSMODEL_H
7+
8+
#include <QObject>
9+
10+
#include <qt/guiutil.h>
11+
#include <qt/peertablemodel.h>
12+
#include <qt/rpcconsole.h>
13+
#include <util/time.h>
14+
15+
class PeerDetailsModel : public QObject
16+
{
17+
Q_OBJECT
18+
Q_PROPERTY(int nodeId READ nodeId NOTIFY dataChanged)
19+
Q_PROPERTY(QString address READ address NOTIFY dataChanged)
20+
Q_PROPERTY(QString addressLocal READ addressLocal NOTIFY dataChanged)
21+
Q_PROPERTY(QString type READ type NOTIFY dataChanged)
22+
Q_PROPERTY(QString version READ version NOTIFY dataChanged)
23+
Q_PROPERTY(QString userAgent READ userAgent NOTIFY dataChanged)
24+
Q_PROPERTY(QString services READ services NOTIFY dataChanged)
25+
Q_PROPERTY(bool transactionRelay READ transactionRelay NOTIFY dataChanged)
26+
Q_PROPERTY(bool addressRelay READ addressRelay NOTIFY dataChanged)
27+
Q_PROPERTY(QString startingHeight READ startingHeight NOTIFY dataChanged)
28+
Q_PROPERTY(QString syncedHeaders READ syncedHeaders NOTIFY dataChanged)
29+
Q_PROPERTY(QString syncedBlocks READ syncedBlocks NOTIFY dataChanged)
30+
Q_PROPERTY(QString direction READ direction NOTIFY dataChanged)
31+
Q_PROPERTY(QString connectionDuration READ connectionDuration NOTIFY dataChanged)
32+
Q_PROPERTY(QString lastSend READ lastSend NOTIFY dataChanged)
33+
Q_PROPERTY(QString lastReceived READ lastReceived NOTIFY dataChanged)
34+
Q_PROPERTY(QString bytesSent READ bytesSent NOTIFY dataChanged)
35+
Q_PROPERTY(QString bytesReceived READ bytesReceived NOTIFY dataChanged)
36+
Q_PROPERTY(QString pingTime READ pingTime NOTIFY dataChanged)
37+
Q_PROPERTY(QString pingWait READ pingWait NOTIFY dataChanged)
38+
Q_PROPERTY(QString pingMin READ pingMin NOTIFY dataChanged)
39+
Q_PROPERTY(QString timeOffset READ timeOffset NOTIFY dataChanged)
40+
Q_PROPERTY(QString mappedAS READ mappedAS NOTIFY dataChanged)
41+
Q_PROPERTY(QString permission READ permission NOTIFY dataChanged)
42+
43+
public:
44+
explicit PeerDetailsModel(CNodeCombinedStats* nodeStats, PeerTableModel* model);
45+
46+
int nodeId() const { return m_combinedStats->nodeStats.nodeid; }
47+
QString address() const { return QString::fromStdString(m_combinedStats->nodeStats.m_addr_name); }
48+
QString addressLocal() const { return QString::fromStdString(m_combinedStats->nodeStats.addrLocal); }
49+
QString type() const { return GUIUtil::ConnectionTypeToQString(m_combinedStats->nodeStats.m_conn_type, /*prepend_direction=*/true); }
50+
QString version() const { return QString::number(m_combinedStats->nodeStats.nVersion); }
51+
QString userAgent() const { return QString::fromStdString(m_combinedStats->nodeStats.cleanSubVer); }
52+
QString services() const { return GUIUtil::formatServicesStr(m_combinedStats->nodeStateStats.their_services); }
53+
bool transactionRelay() const { return m_combinedStats->nodeStateStats.m_relay_txs; }
54+
bool addressRelay() const { return m_combinedStats->nodeStateStats.m_addr_relay_enabled; }
55+
QString startingHeight() const { return QString::number(m_combinedStats->nodeStateStats.m_starting_height); }
56+
QString syncedHeaders() const { return QString::number(m_combinedStats->nodeStateStats.nSyncHeight); }
57+
QString syncedBlocks() const { return QString::number(m_combinedStats->nodeStateStats.nCommonHeight); }
58+
QString direction() const { return QString::fromStdString(m_combinedStats->nodeStats.fInbound ? "Inbound" : "Outbound"); }
59+
QString connectionDuration() const { return GUIUtil::formatDurationStr(GetTime<std::chrono::seconds>() - m_combinedStats->nodeStats.m_connected); }
60+
QString lastSend() const { return GUIUtil::formatDurationStr(GetTime<std::chrono::seconds>() - m_combinedStats->nodeStats.m_last_send); }
61+
QString lastReceived() const { return GUIUtil::formatDurationStr(GetTime<std::chrono::seconds>() - m_combinedStats->nodeStats.m_last_recv); }
62+
QString bytesSent() const { return GUIUtil::formatBytes(m_combinedStats->nodeStats.nSendBytes); }
63+
QString bytesReceived() const { return GUIUtil::formatBytes(m_combinedStats->nodeStats.nRecvBytes); }
64+
QString pingTime() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStats.m_last_ping_time); }
65+
QString pingMin() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStats.m_min_ping_time); }
66+
QString pingWait() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStateStats.m_ping_wait); }
67+
QString timeOffset() const { return GUIUtil::formatTimeOffset(m_combinedStats->nodeStats.nTimeOffset); }
68+
QString mappedAS() const { return m_combinedStats->nodeStats.m_mapped_as != 0 ? QString::number(m_combinedStats->nodeStats.m_mapped_as) : tr("N/A"); }
69+
QString permission() const {
70+
if (m_combinedStats->nodeStats.m_permission_flags == NetPermissionFlags::None) {
71+
return tr("N/A");
72+
}
73+
QStringList permissions;
74+
for (const auto& permission : NetPermissions::ToStrings(m_combinedStats->nodeStats.m_permission_flags)) {
75+
permissions.append(QString::fromStdString(permission));
76+
}
77+
return permissions.join(" & ");
78+
}
79+
80+
Q_SIGNALS:
81+
void dataChanged();
82+
void disconnected();
83+
84+
private Q_SLOTS:
85+
void onModelRowsRemoved(const QModelIndex& parent, int first, int last);
86+
void onModelDataChanged(const QModelIndex& top_left, const QModelIndex& bottom_right);
87+
88+
private:
89+
int m_row;
90+
CNodeCombinedStats* m_combinedStats;
91+
PeerTableModel* m_model;
92+
bool m_disconnected;
93+
};
94+
95+
#endif // BITCOIN_QML_MODELS_PEERDETAILSMODEL_H

src/qml/models/peerlistsortproxy.cpp

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
44

55
#include <qml/models/peerlistsortproxy.h>
6+
#include <qml/models/peerdetailsmodel.h>
67
#include <qt/peertablemodel.h>
78

89
PeerListSortProxy::PeerListSortProxy(QObject* parent)
@@ -23,6 +24,7 @@ QHash<int, QByteArray> PeerListSortProxy::roleNames() const
2324
roles[PeerTableModel::Sent] = "sent";
2425
roles[PeerTableModel::Received] = "received";
2526
roles[PeerTableModel::Subversion] = "subversion";
27+
roles[PeerTableModel::StatsRole] = "stats";
2628
return roles;
2729
}
2830

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

src/qml/pages/node/NodeSettings.qml

+11
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,17 @@ Item {
193193
nodeSettingsView.pop()
194194
peerTableModel.stopAutoRefresh();
195195
}
196+
onPeerSelected: (peerDetails) => {
197+
nodeSettingsView.push(peer_details, {"details": peerDetails})
198+
}
199+
}
200+
}
201+
Component {
202+
id: peer_details
203+
PeerDetails {
204+
onBackClicked: {
205+
nodeSettingsView.pop()
206+
}
196207
}
197208
}
198209
Component {

0 commit comments

Comments
 (0)