mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-25 02:33:24 -03:00
Merge #162: Add network to peers window and peer details
e262a19b0b
gui: display network in peer details (Jon Atack)9136953073
gui: rename peer tab column headers, initialize in .h (Hennadii Stepanov)05c08c696a
gui: add network column in peers tab/window (Jon Atack)e0e55060bf
gui: fix broken doxygen formatting in src/qt/guiutil.h (Jon Atack)0d5613f9de
gui: create GUIUtil::NetworkToQString() utility function (Jon Atack)af9103cc79
net, rpc: change CNodeStats::m_network from string to Network (Jon Atack) Pull request description: and rename peers window column headers from NodeId and Node/Service to Peer Id and Address. ![Screenshot from 2020-12-27 14-45-31](https://user-images.githubusercontent.com/2415484/103172228-efec8600-4849-11eb-8cee-04a3d2ab1273.png) ACKs for top commit: laanwj: ACKe262a19b0b
Tree-SHA512: 709c2a805c109c2dd033aca7b6b6dc94ebe2ce7a0168c71249e1e661c9c57d1f1c781a5b9ccf3b776bedeb83ae2fb5c505637337c45b1eb9a418cb1693a89761
This commit is contained in:
commit
d875bcc8f9
9 changed files with 127 additions and 75 deletions
|
@ -566,7 +566,7 @@ void CNode::copyStats(CNodeStats &stats, const std::vector<bool> &m_asmap)
|
|||
X(nServices);
|
||||
X(addr);
|
||||
X(addrBind);
|
||||
stats.m_network = GetNetworkName(ConnectedThroughNetwork());
|
||||
stats.m_network = ConnectedThroughNetwork();
|
||||
stats.m_mapped_as = addr.GetMappedAS(m_asmap);
|
||||
if (m_tx_relay != nullptr) {
|
||||
LOCK(m_tx_relay->cs_filter);
|
||||
|
|
|
@ -720,8 +720,8 @@ public:
|
|||
CAddress addr;
|
||||
// Bind address of our side of the connection
|
||||
CAddress addrBind;
|
||||
// Name of the network the peer connected through
|
||||
std::string m_network;
|
||||
// Network the peer connected through
|
||||
Network m_network;
|
||||
uint32_t m_mapped_as;
|
||||
std::string m_conn_type_string;
|
||||
};
|
||||
|
|
|
@ -1100,14 +1100,17 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_21">
|
||||
<widget class="QLabel" name="peerNetworkLabel">
|
||||
<property name="toolTip">
|
||||
<string>The network protocol this peer is connected through: IPv4, IPv6, Onion, I2P, or CJDNS.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Version</string>
|
||||
<string>Network</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="peerVersion">
|
||||
<widget class="QLabel" name="peerNetwork">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1123,14 +1126,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_28">
|
||||
<widget class="QLabel" name="label_21">
|
||||
<property name="text">
|
||||
<string>User Agent</string>
|
||||
<string>Version</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QLabel" name="peerSubversion">
|
||||
<widget class="QLabel" name="peerVersion">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1146,14 +1149,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<widget class="QLabel" name="label_28">
|
||||
<property name="text">
|
||||
<string>Services</string>
|
||||
<string>User Agent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QLabel" name="peerServices">
|
||||
<widget class="QLabel" name="peerSubversion">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1169,14 +1172,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_29">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string>Starting Block</string>
|
||||
<string>Services</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QLabel" name="peerHeight">
|
||||
<widget class="QLabel" name="peerServices">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1192,14 +1195,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="label_27">
|
||||
<widget class="QLabel" name="label_29">
|
||||
<property name="text">
|
||||
<string>Synced Headers</string>
|
||||
<string>Starting Block</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QLabel" name="peerSyncHeight">
|
||||
<widget class="QLabel" name="peerHeight">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1215,14 +1218,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_25">
|
||||
<widget class="QLabel" name="label_27">
|
||||
<property name="text">
|
||||
<string>Synced Blocks</string>
|
||||
<string>Synced Headers</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QLabel" name="peerCommonHeight">
|
||||
<widget class="QLabel" name="peerSyncHeight">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1238,14 +1241,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<widget class="QLabel" name="label_22">
|
||||
<widget class="QLabel" name="label_25">
|
||||
<property name="text">
|
||||
<string>Connection Time</string>
|
||||
<string>Synced Blocks</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QLabel" name="peerConnTime">
|
||||
<widget class="QLabel" name="peerCommonHeight">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1261,14 +1264,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="9" column="0">
|
||||
<widget class="QLabel" name="label_15">
|
||||
<widget class="QLabel" name="label_22">
|
||||
<property name="text">
|
||||
<string>Last Send</string>
|
||||
<string>Connection Time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="9" column="1">
|
||||
<widget class="QLabel" name="peerLastSend">
|
||||
<widget class="QLabel" name="peerConnTime">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1284,14 +1287,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="10" column="0">
|
||||
<widget class="QLabel" name="label_19">
|
||||
<widget class="QLabel" name="label_15">
|
||||
<property name="text">
|
||||
<string>Last Receive</string>
|
||||
<string>Last Send</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="10" column="1">
|
||||
<widget class="QLabel" name="peerLastRecv">
|
||||
<widget class="QLabel" name="peerLastSend">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1307,14 +1310,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="11" column="0">
|
||||
<widget class="QLabel" name="label_18">
|
||||
<widget class="QLabel" name="label_19">
|
||||
<property name="text">
|
||||
<string>Sent</string>
|
||||
<string>Last Receive</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="1">
|
||||
<widget class="QLabel" name="peerBytesSent">
|
||||
<widget class="QLabel" name="peerLastRecv">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1330,14 +1333,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="12" column="0">
|
||||
<widget class="QLabel" name="label_20">
|
||||
<widget class="QLabel" name="label_18">
|
||||
<property name="text">
|
||||
<string>Received</string>
|
||||
<string>Sent</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="1">
|
||||
<widget class="QLabel" name="peerBytesRecv">
|
||||
<widget class="QLabel" name="peerBytesSent">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1353,14 +1356,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="13" column="0">
|
||||
<widget class="QLabel" name="label_26">
|
||||
<widget class="QLabel" name="label_20">
|
||||
<property name="text">
|
||||
<string>Ping Time</string>
|
||||
<string>Received</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="13" column="1">
|
||||
<widget class="QLabel" name="peerPingTime">
|
||||
<widget class="QLabel" name="peerBytesRecv">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1376,17 +1379,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="14" column="0">
|
||||
<widget class="QLabel" name="peerPingWaitLabel">
|
||||
<property name="toolTip">
|
||||
<string>The duration of a currently outstanding ping.</string>
|
||||
</property>
|
||||
<widget class="QLabel" name="label_26">
|
||||
<property name="text">
|
||||
<string>Ping Wait</string>
|
||||
<string>Ping Time</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="14" column="1">
|
||||
<widget class="QLabel" name="peerPingWait">
|
||||
<widget class="QLabel" name="peerPingTime">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1402,14 +1402,17 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="15" column="0">
|
||||
<widget class="QLabel" name="peerMinPingLabel">
|
||||
<widget class="QLabel" name="peerPingWaitLabel">
|
||||
<property name="toolTip">
|
||||
<string>The duration of a currently outstanding ping.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Min Ping</string>
|
||||
<string>Ping Wait</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="15" column="1">
|
||||
<widget class="QLabel" name="peerMinPing">
|
||||
<widget class="QLabel" name="peerPingWait">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1425,14 +1428,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="16" column="0">
|
||||
<widget class="QLabel" name="label_timeoffset">
|
||||
<widget class="QLabel" name="peerMinPingLabel">
|
||||
<property name="text">
|
||||
<string>Time Offset</string>
|
||||
<string>Min Ping</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="16" column="1">
|
||||
<widget class="QLabel" name="timeoffset">
|
||||
<widget class="QLabel" name="peerMinPing">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1448,17 +1451,14 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="17" column="0">
|
||||
<widget class="QLabel" name="peerMappedASLabel">
|
||||
<property name="toolTip">
|
||||
<string>The mapped Autonomous System used for diversifying peer selection.</string>
|
||||
</property>
|
||||
<widget class="QLabel" name="label_timeoffset">
|
||||
<property name="text">
|
||||
<string>Mapped AS</string>
|
||||
<string>Time Offset</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="17" column="1">
|
||||
<widget class="QLabel" name="peerMappedAS">
|
||||
<widget class="QLabel" name="timeoffset">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
|
@ -1474,6 +1474,32 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="18" column="0">
|
||||
<widget class="QLabel" name="peerMappedASLabel">
|
||||
<property name="toolTip">
|
||||
<string>The mapped Autonomous System used for diversifying peer selection.</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Mapped AS</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="18" column="1">
|
||||
<widget class="QLabel" name="peerMappedAS">
|
||||
<property name="cursor">
|
||||
<cursorShape>IBeamCursor</cursorShape>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>N/A</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::LinksAccessibleByMouse|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="19" column="0">
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
|
|
|
@ -749,6 +749,21 @@ QString boostPathToQString(const fs::path &path)
|
|||
return QString::fromStdString(path.string());
|
||||
}
|
||||
|
||||
QString NetworkToQString(Network net)
|
||||
{
|
||||
switch (net) {
|
||||
case NET_UNROUTABLE: return QObject::tr("Unroutable");
|
||||
case NET_IPV4: return "IPv4";
|
||||
case NET_IPV6: return "IPv6";
|
||||
case NET_ONION: return "Onion";
|
||||
case NET_I2P: return "I2P";
|
||||
case NET_CJDNS: return "CJDNS";
|
||||
case NET_INTERNAL: return QObject::tr("Internal");
|
||||
case NET_MAX: assert(false);
|
||||
} // no default case, so the compiler can warn about missing cases
|
||||
assert(false);
|
||||
}
|
||||
|
||||
QString formatDurationStr(int secs)
|
||||
{
|
||||
QStringList strList;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
#include <amount.h>
|
||||
#include <fs.h>
|
||||
#include <netaddress.h>
|
||||
|
||||
#include <QEvent>
|
||||
#include <QHeaderView>
|
||||
|
@ -218,22 +219,25 @@ namespace GUIUtil
|
|||
bool GetStartOnSystemStartup();
|
||||
bool SetStartOnSystemStartup(bool fAutoStart);
|
||||
|
||||
/* Convert QString to OS specific boost path through UTF-8 */
|
||||
/** Convert QString to OS specific boost path through UTF-8 */
|
||||
fs::path qstringToBoostPath(const QString &path);
|
||||
|
||||
/* Convert OS specific boost path to QString through UTF-8 */
|
||||
/** Convert OS specific boost path to QString through UTF-8 */
|
||||
QString boostPathToQString(const fs::path &path);
|
||||
|
||||
/* Convert seconds into a QString with days, hours, mins, secs */
|
||||
/** Convert enum Network to QString */
|
||||
QString NetworkToQString(Network net);
|
||||
|
||||
/** Convert seconds into a QString with days, hours, mins, secs */
|
||||
QString formatDurationStr(int secs);
|
||||
|
||||
/* Format CNodeStats.nServices bitmask into a user-readable string */
|
||||
/** Format CNodeStats.nServices bitmask into a user-readable string */
|
||||
QString formatServicesStr(quint64 mask);
|
||||
|
||||
/* Format a CNodeStats.m_ping_usec into a user-readable string or display N/A, if 0*/
|
||||
/** Format a CNodeStats.m_ping_usec into a user-readable string or display N/A, if 0 */
|
||||
QString formatPingTime(int64_t ping_usec);
|
||||
|
||||
/* Format a CNodeCombinedStats.nTimeOffset into a user-readable string. */
|
||||
/** Format a CNodeCombinedStats.nTimeOffset into a user-readable string */
|
||||
QString formatTimeOffset(int64_t nTimeOffset);
|
||||
|
||||
QString formatNiceTimeOffset(qint64 secs);
|
||||
|
|
|
@ -29,14 +29,16 @@ bool NodeLessThan::operator()(const CNodeCombinedStats &left, const CNodeCombine
|
|||
return pLeft->nodeid < pRight->nodeid;
|
||||
case PeerTableModel::Address:
|
||||
return pLeft->addrName.compare(pRight->addrName) < 0;
|
||||
case PeerTableModel::Subversion:
|
||||
return pLeft->cleanSubVer.compare(pRight->cleanSubVer) < 0;
|
||||
case PeerTableModel::Network:
|
||||
return pLeft->m_network < pRight->m_network;
|
||||
case PeerTableModel::Ping:
|
||||
return pLeft->m_min_ping_usec < pRight->m_min_ping_usec;
|
||||
case PeerTableModel::Sent:
|
||||
return pLeft->nSendBytes < pRight->nSendBytes;
|
||||
case PeerTableModel::Received:
|
||||
return pLeft->nRecvBytes < pRight->nRecvBytes;
|
||||
case PeerTableModel::Subversion:
|
||||
return pLeft->cleanSubVer.compare(pRight->cleanSubVer) < 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -104,7 +106,6 @@ PeerTableModel::PeerTableModel(interfaces::Node& node, QObject* parent) :
|
|||
m_node(node),
|
||||
timer(nullptr)
|
||||
{
|
||||
columns << tr("NodeId") << tr("Node/Service") << tr("Ping") << tr("Sent") << tr("Received") << tr("User Agent");
|
||||
priv.reset(new PeerTablePriv());
|
||||
|
||||
// set up timer for auto refresh
|
||||
|
@ -158,17 +159,21 @@ QVariant PeerTableModel::data(const QModelIndex &index, int role) const
|
|||
case Address:
|
||||
// prepend to peer address down-arrow symbol for inbound connection and up-arrow for outbound connection
|
||||
return QString(rec->nodeStats.fInbound ? "↓ " : "↑ ") + QString::fromStdString(rec->nodeStats.addrName);
|
||||
case Subversion:
|
||||
return QString::fromStdString(rec->nodeStats.cleanSubVer);
|
||||
case Network:
|
||||
return GUIUtil::NetworkToQString(rec->nodeStats.m_network);
|
||||
case Ping:
|
||||
return GUIUtil::formatPingTime(rec->nodeStats.m_min_ping_usec);
|
||||
case Sent:
|
||||
return GUIUtil::formatBytes(rec->nodeStats.nSendBytes);
|
||||
case Received:
|
||||
return GUIUtil::formatBytes(rec->nodeStats.nRecvBytes);
|
||||
case Subversion:
|
||||
return QString::fromStdString(rec->nodeStats.cleanSubVer);
|
||||
}
|
||||
} else if (role == Qt::TextAlignmentRole) {
|
||||
switch (index.column()) {
|
||||
case Network:
|
||||
return QVariant(Qt::AlignCenter);
|
||||
case Ping:
|
||||
case Sent:
|
||||
case Received:
|
||||
|
|
|
@ -60,10 +60,11 @@ public:
|
|||
enum ColumnIndex {
|
||||
NetNodeId = 0,
|
||||
Address = 1,
|
||||
Ping = 2,
|
||||
Sent = 3,
|
||||
Received = 4,
|
||||
Subversion = 5
|
||||
Network = 2,
|
||||
Ping = 3,
|
||||
Sent = 4,
|
||||
Received = 5,
|
||||
Subversion = 6
|
||||
};
|
||||
|
||||
/** @name Methods overridden from QAbstractTableModel
|
||||
|
@ -82,7 +83,7 @@ public Q_SLOTS:
|
|||
|
||||
private:
|
||||
interfaces::Node& m_node;
|
||||
QStringList columns;
|
||||
const QStringList columns{tr("Peer Id"), tr("Address"), tr("Network"), tr("Ping"), tr("Sent"), tr("Received"), tr("User Agent")};
|
||||
std::unique_ptr<PeerTablePriv> priv;
|
||||
QTimer *timer;
|
||||
};
|
||||
|
|
|
@ -1092,7 +1092,7 @@ void RPCConsole::updateDetailWidget()
|
|||
const CNodeCombinedStats *stats = clientModel->getPeerTableModel()->getNodeStats(selected_rows.first().row());
|
||||
// update the detail ui with latest node information
|
||||
QString peerAddrDetails(QString::fromStdString(stats->nodeStats.addrName) + " ");
|
||||
peerAddrDetails += tr("(node id: %1)").arg(QString::number(stats->nodeStats.nodeid));
|
||||
peerAddrDetails += tr("(peer id: %1)").arg(QString::number(stats->nodeStats.nodeid));
|
||||
if (!stats->nodeStats.addrLocal.empty())
|
||||
peerAddrDetails += "<br />" + tr("via %1").arg(QString::fromStdString(stats->nodeStats.addrLocal));
|
||||
ui->peerHeading->setText(peerAddrDetails);
|
||||
|
@ -1109,6 +1109,7 @@ void RPCConsole::updateDetailWidget()
|
|||
ui->peerVersion->setText(QString::number(stats->nodeStats.nVersion));
|
||||
ui->peerSubversion->setText(QString::fromStdString(stats->nodeStats.cleanSubVer));
|
||||
ui->peerDirection->setText(stats->nodeStats.fInbound ? tr("Inbound") : tr("Outbound"));
|
||||
ui->peerNetwork->setText(GUIUtil::NetworkToQString(stats->nodeStats.m_network));
|
||||
if (stats->nodeStats.m_permissionFlags == PF_NONE) {
|
||||
ui->peerPermissions->setText(tr("N/A"));
|
||||
} else {
|
||||
|
|
|
@ -187,7 +187,7 @@ static RPCHelpMan getpeerinfo()
|
|||
if (!(stats.addrLocal.empty())) {
|
||||
obj.pushKV("addrlocal", stats.addrLocal);
|
||||
}
|
||||
obj.pushKV("network", stats.m_network);
|
||||
obj.pushKV("network", GetNetworkName(stats.m_network));
|
||||
if (stats.m_mapped_as != 0) {
|
||||
obj.pushKV("mapped_as", uint64_t(stats.m_mapped_as));
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue