From 02d3121218e1dc04582a85149cc3eabe795aef03 Mon Sep 17 00:00:00 2001 From: johnny9 <985648+johnny9@users.noreply.github.com> Date: Sat, 20 Jan 2024 08:42:58 -0500 Subject: [PATCH] qml: Add PeerDetails page fixup --- src/Makefile.qt.include | 4 + src/qml/bitcoin.cpp | 3 + src/qml/bitcoin_qml.qrc | 1 + src/qml/models/peerdetailsmodel.cpp | 56 ++++++ src/qml/models/peerdetailsmodel.h | 95 +++++++++ src/qml/models/peerlistsortproxy.cpp | 6 + src/qml/pages/node/NodeSettings.qml | 11 ++ src/qml/pages/node/PeerDetails.qml | 286 +++++++++++++++++++++++++++ src/qml/pages/node/Peers.qml | 5 + 9 files changed, 467 insertions(+) create mode 100644 src/qml/models/peerdetailsmodel.cpp create mode 100644 src/qml/models/peerdetailsmodel.h create mode 100644 src/qml/pages/node/PeerDetails.qml diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index e1d6a44818..a61043608d 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -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 \ @@ -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 \ @@ -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 \ @@ -408,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 \ diff --git a/src/qml/bitcoin.cpp b/src/qml/bitcoin.cpp index 367512cbe1..0e5d0f9ce7 100644 --- a/src/qml/bitcoin.cpp +++ b/src/qml/bitcoin.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -315,6 +316,8 @@ int QmlGuiMain(int argc, char* argv[]) qmlRegisterSingletonInstance("org.bitcoincore.qt", 1, 0, "AppMode", &app_mode); qmlRegisterType("org.bitcoincore.qt", 1, 0, "BlockClockDial"); qmlRegisterType("org.bitcoincore.qt", 1, 0, "LineGraph"); + qmlRegisterUncreatableType("org.bitcoincore.qt", 1, 0, "PeerDetailsModel", ""); + engine.load(QUrl(QStringLiteral("qrc:///qml/pages/main.qml"))); if (engine.rootObjects().isEmpty()) { diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index b5052d87e4..ec48e0c74d 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -52,6 +52,7 @@ pages/node/NodeRunner.qml pages/node/NodeSettings.qml pages/node/Peers.qml + pages/node/PeerDetails.qml pages/node/Shutdown.qml pages/onboarding/OnboardingBlockclock.qml pages/onboarding/OnboardingConnection.qml diff --git a/src/qml/models/peerdetailsmodel.cpp b/src/qml/models/peerdetailsmodel.cpp new file mode 100644 index 0000000000..cf764cd711 --- /dev/null +++ b/src/qml/models/peerdetailsmodel.cpp @@ -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 + +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(); + + // Only update when all information is available + if (m_combinedStats && m_combinedStats->fNodeStateStatsAvailable) { + Q_EMIT dataChanged(); + } +} diff --git a/src/qml/models/peerdetailsmodel.h b/src/qml/models/peerdetailsmodel.h new file mode 100644 index 0000000000..9f6b17ad83 --- /dev/null +++ b/src/qml/models/peerdetailsmodel.h @@ -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 + +#include +#include +#include +#include + +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() - m_combinedStats->nodeStats.m_connected); } + QString lastSend() const { return GUIUtil::formatDurationStr(GetTime() - m_combinedStats->nodeStats.m_last_send); } + QString lastReceived() const { return GUIUtil::formatDurationStr(GetTime() - 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 diff --git a/src/qml/models/peerlistsortproxy.cpp b/src/qml/models/peerlistsortproxy.cpp index b566672458..8e1ad9b363 100644 --- a/src/qml/models/peerlistsortproxy.cpp +++ b/src/qml/models/peerlistsortproxy.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include PeerListSortProxy::PeerListSortProxy(QObject* parent) @@ -23,6 +24,7 @@ QHash PeerListSortProxy::roleNames() const roles[PeerTableModel::Sent] = "sent"; roles[PeerTableModel::Received] = "received"; roles[PeerTableModel::Subversion] = "subversion"; + roles[PeerTableModel::StatsRole] = "stats"; return roles; } @@ -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(), qobject_cast(sourceModel())); + return QVariant::fromValue(details); + } else if (role == PeerTableModel::NetNodeId) { return PeerTableSortProxy::data(index, role); } diff --git a/src/qml/pages/node/NodeSettings.qml b/src/qml/pages/node/NodeSettings.qml index a2ca4c667b..19b207320d 100644 --- a/src/qml/pages/node/NodeSettings.qml +++ b/src/qml/pages/node/NodeSettings.qml @@ -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 { diff --git a/src/qml/pages/node/PeerDetails.qml b/src/qml/pages/node/PeerDetails.qml new file mode 100644 index 0000000000..c68dc36486 --- /dev/null +++ b/src/qml/pages/node/PeerDetails.qml @@ -0,0 +1,286 @@ +// 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 +import "../../controls" +import "../../components" + +Page { + id: root + signal backClicked() + + property PeerDetailsModel details + + Connections { + target: details + function onDisconnected() { + root.backClicked() + } + } + + background: null + header: NavigationBar2 { + leftItem: NavButton { + iconSource: "image://images/caret-left" + text: qsTr("Back") + onClicked: root.backClicked() + } + centerItem: Header { + headerBold: true + headerSize: 18 + header: qsTr("Peer " + details.nodeId) + } + } + + ScrollView { + id: scrollView + width: parent.width + height: parent.height + clip: true + contentWidth: width + + Column { + width: Math.min(parent.width - 40, 450) + anchors.horizontalCenter: parent.horizontalCenter + spacing: 10 + topPadding: 30 + bottomPadding: 30 + + CoreText { + text: qsTr("Information"); + bold: true; + font.pixelSize: 18; + horizontalAlignment: Qt.AlignLeft; + color: Theme.color.neutral9; + } + + Column { + width: parent.width + bottomPadding: 5 + + KeyValueRow { key: KeyText { text: qsTr("Address"); } value: ValText { text: details.address; color: Theme.color.neutral9; }} + KeyValueRow { key: KeyText { text: qsTr("VIA"); } value: ValText { text: details.addressLocal; color: Theme.color.neutral9; }} + KeyValueRow { key: KeyText { text: qsTr("Type"); } value: ValText { text: details.type; color: Theme.color.neutral9; }} + KeyValueRow { + id: permissionsRow + property string permissionsValue: details.permission + property bool isPermissioned: permissionsValue != "N/A" + key: KeyText { + text: qsTr("Permissions"); + active: permissionsRow.isPermissioned + } + value: Loader { + sourceComponent: permissionsRow.isPermissioned ? permissioned : notPermissioned + } + Component { + id: permissioned + ValText { text: permissionsRow.permissionsValue; } + } + Component { + id: notPermissioned + Row { + IconButton { + iconLocation: "image://images/minus"; + icon.color: Theme.color.neutral6 + } + } + } + } + KeyValueRow { key: KeyText { text: qsTr("Version"); } value: ValText { text: details.version; }} + KeyValueRow { key: KeyText { text: qsTr("User agent"); } value: ValText { text: details.userAgent; }} + KeyValueRow { key: KeyText { text: qsTr("Services"); } value: ValText { text: details.services; }} + KeyValueRow { + id: transactionRelayRow + property bool isTransactionRelay: details.transactionRelay + key: KeyText { text: qsTr("Transaction relay"); } + value: Row { + IconButton { + iconLocation: transactionRelayRow.isTransactionRelay ? "image://images/check" : "image://images/cross" + anchors.verticalCenter: parent.verticalCenter + } + } + } + KeyValueRow { + id: addressRelayRow + property bool isAddressRelay: details.addressRelay + key: KeyText { text: qsTr("Address relay"); } + value: Row { + IconButton { + iconLocation: addressRelayRow.isAddressRelay ? "image://images/check" : "image://images/cross" + anchors.verticalCenter: parent.verticalCenter + } + } + } + KeyValueRow { + id: asRow + property string mappedASValue: details.mappedAS + property bool isMappedAS: mappedASValue != "N/A" + key: KeyText { + text: qsTr("Mapped AS"); + active: asRow.isMappedAS + } + + value: Loader { + sourceComponent: asRow.isMappedAS ? mappedAs : notMappedAs + } + + Component { + id: mappedAs + ValText { text: asRow.mappedASValue; } + } + + Component { + id: notMappedAs + Row { + IconButton { + iconLocation: "image://images/minus"; + icon.color: Theme.color.neutral6 + anchors.verticalCenter: parent.verticalCenter + } + } + } + } + } + + CoreText { + text: qsTr("Block data"); + bold: true; + font.pixelSize: 18; + horizontalAlignment: Qt.AlignLeft; + color: Theme.color.neutral9; + } + + Column { + width: parent.width + bottomPadding: 5 + KeyValueRow { key: KeyText { text: qsTr("Starting block"); } value: ValText { text: details.startingHeight; }} + KeyValueRow { key: KeyText { text: qsTr("Synced headers"); } value: ValText { text: details.syncedHeaders; }} + KeyValueRow { key: KeyText { text: qsTr("Synced blocks"); } value: ValText { text: details.syncedBlocks; }} + } + + CoreText { + text: qsTr("Network traffic"); + bold: true; + font.pixelSize: 18; + horizontalAlignment: Qt.AlignLeft; + color: Theme.color.neutral9; + } + Column { + width: parent.width + bottomPadding: 5 + KeyValueRow { + key: KeyText { text: qsTr("Direction"); } + value: Row { + IconButton { + iconLocation: details.direction === "Inbound" ? "image://images/arrow-down" : "image://images/arrow-up" + icon.height: 9 + anchors.verticalCenter: parent.verticalCenter + } + ValText { + text: details.direction + anchors.verticalCenter: parent.verticalCenter + } + } + } + KeyValueRow { key: KeyText { text: qsTr("Connection time"); } value: NetStatValue { text: details.connectionDuration; }} + KeyValueRow { key: KeyText { text: qsTr("Last send"); } value: NetStatValue { text: details.lastSend + qsTr(" ago"); }} + KeyValueRow { key: KeyText { text: qsTr("Last receive"); } value: NetStatValue { text: details.lastReceived + qsTr(" ago"); }} + KeyValueRow { key: KeyText { text: qsTr("Sent"); } value: NetStatValue { text: details.bytesSent + qsTr(" total"); }} + KeyValueRow { key: KeyText { text: qsTr("Received"); } value: NetStatValue { text: details.bytesReceived + qsTr(" total"); }} + KeyValueRow { key: KeyText { text: qsTr("Ping time"); } value: NetStatValue { text: details.pingTime; }} + KeyValueRow { + id: pingWaitRow + property string pingWaitValue: details.pingWait + property bool isPingWait: pingWaitValue != "N/A" + key: KeyText { + text: qsTr("Ping wait"); + active: pingWaitRow.isPingWait + } + + value: Loader { + sourceComponent: pingWaitRow.isPingWait ? pingWait : notPingWait + } + + Component { + id: pingWait + ValText { text: pingWaitRow.pingWaitValue; } + } + + Component { + id: notPingWait + Row { + Button { + padding: 0 + display: AbstractButton.IconOnly + height: 21 + width: 21 + icon.source: "image://images/minus" + icon.color: Theme.color.neutral6 + icon.height: 21 + icon.width: 21 + background: null + } + } + } + } + KeyValueRow { key: KeyText { text: qsTr("Min ping"); } value: NetStatValue { text: details.pingMin; }} + KeyValueRow { key: KeyText {text: qsTr("Time offset"); } value: NetStatValue { text: details.timeOffset; }} + } + } + } + + component KeyText: CoreText { + property bool active: true + color: active ? Theme.color.neutral9 : Theme.color.neutral6 + horizontalAlignment: Qt.AlignLeft + verticalAlignment: Text.AlignVCenter + } + + component ValText: CoreText { + property bool active: true + color: active ? Theme.color.neutral8 : Theme.color.neutral6 + horizontalAlignment: Qt.AlignLeft + verticalAlignment: Text.AlignVCenter + } + + component IconButton: Button { + id: iconButton + property alias iconLocation: iconButton.icon.source + padding: 0 + display: AbstractButton.IconOnly + height: 21 + width: 21 + icon.color: Theme.color.neutral9 + icon.height: 21 + icon.width: 21 + background: null + } + + component NetStatIndicator: Button { + width: 21 + height: 21 + background: Rectangle { + width: 8 + height: 8 + radius: 4 + anchors.centerIn: parent + color: Theme.color.green + } + } + + component NetStatValue: Row { + property alias text: valText.text + spacing: 0 + NetStatIndicator {} + ValText { + id: valText + anchors.verticalCenter: parent.verticalCenter + } + } +} + + diff --git a/src/qml/pages/node/Peers.qml b/src/qml/pages/node/Peers.qml index a8b0173aa9..173029ec7d 100644 --- a/src/qml/pages/node/Peers.qml +++ b/src/qml/pages/node/Peers.qml @@ -12,6 +12,7 @@ import "../../components" Page { signal backClicked + signal peerSelected(PeerDetailsModel peerDetails) id: root background: null @@ -161,6 +162,7 @@ Page { required property string direction; required property string connectionType; required property string network; + required property PeerDetailsModel stats; readonly property color stateColor: { if (delegate.down) { return Theme.color.orange @@ -232,6 +234,9 @@ Page { width: parent.width } } + onClicked: { + root.peerSelected(stats) + } contentItem: ColumnLayout { RowLayout { Layout.fillWidth: true