Skip to content

Commit b7161c5

Browse files
committed
qml: Add PeerDetails page
1 parent 67150ef commit b7161c5

File tree

9 files changed

+283
-0
lines changed

9 files changed

+283
-0
lines changed

src/Makefile.qt.include

Lines changed: 4 additions & 0 deletions
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/moc_appmode.cpp \
4647
qt/moc_addressbookpage.cpp \
@@ -120,6 +121,7 @@ BITCOIN_QT_H = \
120121
qml/models/networktraffictower.h \
121122
qml/models/nodemodel.h \
122123
qml/models/options_model.h \
124+
qml/models/peerdetailsmodel.h \
123125
qml/models/peerlistsortproxy.h \
124126
qml/appmode.h \
125127
qml/bitcoin.h \
@@ -307,6 +309,7 @@ BITCOIN_QML_BASE_CPP = \
307309
qml/models/networktraffictower.cpp \
308310
qml/models/nodemodel.cpp \
309311
qml/models/options_model.cpp \
312+
qml/models/peerdetailsmodel.cpp \
310313
qml/models/peerlistsortproxy.cpp \
311314
qml/imageprovider.cpp \
312315
qml/util.cpp
@@ -384,6 +387,7 @@ QML_RES_QML = \
384387
qml/pages/node/NodeRunner.qml \
385388
qml/pages/node/NodeSettings.qml \
386389
qml/pages/node/Peers.qml \
390+
qml/pages/node/PeerDetails.qml \
387391
qml/pages/node/Shutdown.qml \
388392
qml/pages/onboarding/OnboardingBlockclock.qml \
389393
qml/pages/onboarding/OnboardingConnection.qml \

src/qml/bitcoin.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <qml/models/networktraffictower.h>
2525
#include <qml/models/nodemodel.h>
2626
#include <qml/models/options_model.h>
27+
#include <qml/models/peerdetailsmodel.h>
2728
#include <qml/models/peerlistsortproxy.h>
2829
#include <qml/imageprovider.h>
2930
#include <qml/util.h>
@@ -292,6 +293,8 @@ int QmlGuiMain(int argc, char* argv[])
292293
qmlRegisterSingletonInstance<AppMode>("org.bitcoincore.qt", 1, 0, "AppMode", &app_mode);
293294
qmlRegisterType<BlockClockDial>("org.bitcoincore.qt", 1, 0, "BlockClockDial");
294295
qmlRegisterType<LineGraph>("org.bitcoincore.qt", 1, 0, "LineGraph");
296+
qmlRegisterUncreatableType<PeerDetailsModel>("org.bitcoincore.qt", 1, 0, "PeerDetailsModel", "");
297+
295298

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

src/qml/bitcoin_qml.qrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<file>pages/node/NodeRunner.qml</file>
4646
<file>pages/node/NodeSettings.qml</file>
4747
<file>pages/node/Peers.qml</file>
48+
<file>pages/node/PeerDetails.qml</file>
4849
<file>pages/node/Shutdown.qml</file>
4950
<file>pages/onboarding/OnboardingBlockclock.qml</file>
5051
<file>pages/onboarding/OnboardingConnection.qml</file>

src/qml/models/peerdetailsmodel.cpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
{
11+
for (int row = 0; row < m_model->rowCount(); ++row) {
12+
QModelIndex index = m_model->index(row, 0);
13+
int nodeIdInRow = m_model->data(index, PeerTableModel::NetNodeId).toInt();
14+
if (nodeIdInRow == m_combinedStats->nodeStats.nodeid) {
15+
m_row = row;
16+
break;
17+
}
18+
}
19+
connect(parent, &PeerTableModel::rowsRemoved, this, &PeerDetailsModel::onModelRowsRemoved);
20+
connect(parent, &PeerTableModel::dataChanged, this, &PeerDetailsModel::onModelDataChanged);
21+
}
22+
23+
void PeerDetailsModel::onModelRowsRemoved(const QModelIndex& parent, int first, int last)
24+
{
25+
for (int row = first; row <= last; ++row) {
26+
QModelIndex index = m_model->index(row, 0, parent);
27+
int nodeIdInRow = m_model->data(index, PeerTableModel::NetNodeId).toInt();
28+
if (nodeIdInRow == this->nodeId()) {
29+
Q_EMIT disconnected();
30+
break;
31+
}
32+
}
33+
}
34+
35+
void PeerDetailsModel::onModelDataChanged(const QModelIndex& /* top_left */, const QModelIndex& /* bottom_right */)
36+
{
37+
if (m_model->data(m_model->index(m_row, 0), PeerTableModel::NetNodeId).isNull() ||
38+
m_model->data(m_model->index(m_row, 0), PeerTableModel::NetNodeId).toInt() != nodeId()) {
39+
// This peer has been removed from the model
40+
Q_EMIT disconnected();
41+
return;
42+
}
43+
44+
m_combinedStats = m_model->data(m_model->index(m_row, 0), PeerTableModel::StatsRole).value<CNodeCombinedStats*>();
45+
46+
// Only update when all information is available
47+
if (m_combinedStats && m_combinedStats->fNodeStateStatsAvailable) {
48+
Q_EMIT dataChanged();
49+
}
50+
}

src/qml/models/peerdetailsmodel.h

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
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 lastSend READ lastSend NOTIFY dataChanged)
32+
Q_PROPERTY(QString lastReceived READ lastReceived NOTIFY dataChanged)
33+
Q_PROPERTY(QString bytesSent READ bytesSent NOTIFY dataChanged)
34+
Q_PROPERTY(QString bytesReceived READ bytesReceived NOTIFY dataChanged)
35+
Q_PROPERTY(QString pingTime READ pingTime NOTIFY dataChanged)
36+
Q_PROPERTY(QString pingWait READ pingWait NOTIFY dataChanged)
37+
Q_PROPERTY(QString pingMin READ pingMin NOTIFY dataChanged)
38+
Q_PROPERTY(QString timeOffset READ timeOffset NOTIFY dataChanged)
39+
Q_PROPERTY(QString mappedAS READ mappedAS NOTIFY dataChanged)
40+
Q_PROPERTY(QString permission READ permission NOTIFY dataChanged)
41+
42+
public:
43+
explicit PeerDetailsModel(CNodeCombinedStats* nodeStats, PeerTableModel* model);
44+
45+
int nodeId() const { return m_combinedStats->nodeStats.nodeid; }
46+
QString address() const { return QString::fromStdString(m_combinedStats->nodeStats.m_addr_name); }
47+
QString addressLocal() const { return QString::fromStdString(m_combinedStats->nodeStats.addrLocal); }
48+
QString type() const { return GUIUtil::ConnectionTypeToQString(m_combinedStats->nodeStats.m_conn_type, /*prepend_direction=*/true); }
49+
QString version() const { return QString::number(m_combinedStats->nodeStats.nVersion); }
50+
QString userAgent() const { return QString::fromStdString(m_combinedStats->nodeStats.cleanSubVer); }
51+
QString services() const { return GUIUtil::formatServicesStr(m_combinedStats->nodeStateStats.their_services); }
52+
bool transactionRelay() const { return m_combinedStats->nodeStateStats.m_relay_txs; }
53+
bool addressRelay() const { return m_combinedStats->nodeStateStats.m_addr_relay_enabled; }
54+
QString startingHeight() const { return QString::number(m_combinedStats->nodeStats.m_starting_height); }
55+
QString syncedHeaders() const { return QString::number(m_combinedStats->nodeStateStats.nSyncHeight); }
56+
QString syncedBlocks() const { return QString::number(m_combinedStats->nodeStateStats.nCommonHeight); }
57+
QString direction() const { return QString::fromStdString(m_combinedStats->nodeStats.fInbound ? "Inbound" : "Outbound"); }
58+
QString lastSend() const { return GUIUtil::formatDurationStr(GetTime<std::chrono::seconds>() - m_combinedStats->nodeStats.m_last_send); }
59+
QString lastReceived() const { return GUIUtil::formatDurationStr(GetTime<std::chrono::seconds>() - m_combinedStats->nodeStats.m_last_recv); }
60+
QString bytesSent() const { return GUIUtil::formatBytes(m_combinedStats->nodeStats.nSendBytes); }
61+
QString bytesReceived() const { return GUIUtil::formatBytes(m_combinedStats->nodeStats.nRecvBytes); }
62+
QString pingTime() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStats.m_last_ping_time); }
63+
QString pingMin() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStats.m_min_ping_time); }
64+
QString pingWait() const { return GUIUtil::formatPingTime(m_combinedStats->nodeStateStats.m_ping_wait); }
65+
QString timeOffset() const { return GUIUtil::formatTimeOffset(m_combinedStats->nodeStats.nTimeOffset); }
66+
QString mappedAS() const { return m_combinedStats->nodeStats.m_mapped_as != 0 ? QString::number(m_combinedStats->nodeStats.m_mapped_as) : tr("N/A"); }
67+
QString permission() const {
68+
if (m_combinedStats->nodeStats.m_permission_flags == NetPermissionFlags::None) {
69+
return tr("N/A");
70+
}
71+
QStringList permissions;
72+
for (const auto& permission : NetPermissions::ToStrings(m_combinedStats->nodeStats.m_permission_flags)) {
73+
permissions.append(QString::fromStdString(permission));
74+
}
75+
return permissions.join(" & ");
76+
}
77+
78+
Q_SIGNALS:
79+
void dataChanged();
80+
void disconnected();
81+
82+
private Q_SLOTS:
83+
void onModelRowsRemoved(const QModelIndex& parent, int first, int last);
84+
void onModelDataChanged(const QModelIndex& top_left, const QModelIndex& bottom_right);
85+
86+
private:
87+
int m_row;
88+
CNodeCombinedStats* m_combinedStats;
89+
PeerTableModel* m_model;
90+
};
91+
92+
#endif // BITCOIN_QML_MODELS_PEERDETAILSMODEL_H

src/qml/models/peerlistsortproxy.cpp

Lines changed: 6 additions & 0 deletions
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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,17 @@ Item {
190190
nodeSettingsView.pop()
191191
peerTableModel.stopAutoRefresh();
192192
}
193+
onPeerSelected: (peerDetails) => {
194+
nodeSettingsView.push(peer_details, {"details": peerDetails})
195+
}
196+
}
197+
}
198+
Component {
199+
id: peer_details
200+
PeerDetails {
201+
onBackClicked: {
202+
nodeSettingsView.pop()
203+
}
193204
}
194205
}
195206
Component {

src/qml/pages/node/PeerDetails.qml

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
import "../../controls"
10+
import "../../components"
11+
12+
Page {
13+
signal backClicked()
14+
15+
property PeerDetailsModel details
16+
17+
Connections {
18+
target: details
19+
function onDisconnected() {
20+
root.backClicked()
21+
}
22+
}
23+
24+
id: root
25+
background: null
26+
27+
header: NavigationBar2 {
28+
leftItem: NavButton {
29+
iconSource: "image://images/caret-left"
30+
text: qsTr("Back")
31+
onClicked: root.backClicked()
32+
}
33+
centerItem: Header {
34+
headerBold: true
35+
headerSize: 18
36+
header: qsTr("Peer " + details.nodeId)
37+
}
38+
}
39+
40+
component PeerKeyValueRow: Row {
41+
width: parent.width
42+
property string key: ""
43+
property string value: ""
44+
CoreText {
45+
color: Theme.color.neutral9;
46+
text: key;
47+
width: 125;
48+
horizontalAlignment: Qt.AlignLeft;
49+
}
50+
CoreText {
51+
color: Theme.color.neutral9;
52+
maximumLineCount: 1;
53+
elide: Text.ElideRight;
54+
wrapMode: Text.NoWrap;
55+
text: value
56+
width: parent.width - 125;
57+
horizontalAlignment: Qt.AlignLeft;
58+
}
59+
}
60+
61+
ScrollView {
62+
id: scrollView
63+
width: parent.width
64+
height: parent.height
65+
clip: true
66+
contentWidth: width
67+
68+
Column {
69+
width: Math.min(parent.width - 40, 450)
70+
anchors.horizontalCenter: parent.horizontalCenter
71+
spacing: 20
72+
73+
CoreText { text: "Information"; bold: true; font.pixelSize: 18; horizontalAlignment: Qt.AlignLeft; }
74+
Column {
75+
width: parent.width
76+
PeerKeyValueRow { key: qsTr("IP"); value: details.address }
77+
PeerKeyValueRow { key: qsTr("VIA"); value: details.addressLocal }
78+
PeerKeyValueRow { key: qsTr("Type"); value: details.type }
79+
PeerKeyValueRow { key: qsTr("Permission"); value: details.permission }
80+
PeerKeyValueRow { key: qsTr("Version"); value: details.version }
81+
PeerKeyValueRow { key: qsTr("User agent"); value: details.userAgent }
82+
PeerKeyValueRow { key: qsTr("Services"); value: details.services }
83+
PeerKeyValueRow { key: qsTr("Transport relay"); value: details.transactionRelay }
84+
PeerKeyValueRow { key: qsTr("Address relay"); value: details.addressRelay }
85+
PeerKeyValueRow { key: qsTr("Mapped AS"); value: details.mappedAS }
86+
}
87+
88+
CoreText { text: "Block data"; bold: true; font.pixelSize: 18; horizontalAlignment: Qt.AlignLeft; }
89+
Column {
90+
width: parent.width
91+
PeerKeyValueRow { key: qsTr("Starting block"); value: details.startingHeight }
92+
PeerKeyValueRow { key: qsTr("Synced headers"); value: details.syncedHeaders }
93+
PeerKeyValueRow { key: qsTr("Synced blocks"); value: details.syncedBlocks }
94+
}
95+
96+
CoreText { text: "Network traffic"; bold: true; font.pixelSize: 18; horizontalAlignment: Qt.AlignLeft; }
97+
Column {
98+
width: parent.width
99+
PeerKeyValueRow { key: qsTr("Direction"); value: details.direction }
100+
PeerKeyValueRow { key: qsTr("Last send"); value: details.lastSend }
101+
PeerKeyValueRow { key: qsTr("Last receive"); value: details.lastReceived }
102+
PeerKeyValueRow { key: qsTr("Sent"); value: details.bytesSent }
103+
PeerKeyValueRow { key: qsTr("Received"); value: details.bytesReceived }
104+
PeerKeyValueRow { key: qsTr("Ping time"); value: details.pingTime }
105+
PeerKeyValueRow { key: qsTr("Ping wait"); value: details.pingWait }
106+
PeerKeyValueRow { key: qsTr("Min ping"); value: details.pingMin }
107+
PeerKeyValueRow { key: qsTr("Time offset"); value: details.timeOffset }
108+
}
109+
}
110+
}
111+
}

src/qml/pages/node/Peers.qml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import "../../components"
1212

1313
Page {
1414
signal backClicked
15+
signal peerSelected(PeerDetailsModel peerDetails)
1516

1617
id: root
1718
background: null
@@ -161,6 +162,7 @@ Page {
161162
required property string direction;
162163
required property string connectionType;
163164
required property string network;
165+
required property PeerDetailsModel stats;
164166
readonly property color stateColor: {
165167
if (delegate.down) {
166168
return Theme.color.orange
@@ -232,6 +234,9 @@ Page {
232234
width: parent.width
233235
}
234236
}
237+
onClicked: {
238+
root.peerSelected(stats)
239+
}
235240
contentItem: ColumnLayout {
236241
RowLayout {
237242
Layout.fillWidth: true

0 commit comments

Comments
 (0)