mirror of
https://github.com/logos-blockchain/logos-blockchain-ui.git
synced 2026-07-08 00:39:28 +00:00
Stability fixes
This commit is contained in:
parent
901a7fa7d4
commit
0de0168f63
@ -183,19 +183,22 @@ Rectangle {
|
||||
? root.backend.status === BlockchainBackend.Running
|
||||
: false
|
||||
|
||||
// Channel Deposit requires a running node. If the node stops while
|
||||
// it's selected, fall back to Accounts so the user isn't stranded
|
||||
// on a disabled nav item.
|
||||
// Wallet operations require a running node. If the node stops while
|
||||
// the Operations tab is open, fall back to the Node tab so the user
|
||||
// isn't stranded on a disabled tab.
|
||||
onNodeRunningChanged: {
|
||||
if (!nodeRunning && operationIndex === 3)
|
||||
operationIndex = 0
|
||||
if (!nodeRunning)
|
||||
operationTabBar.currentIndex = 0
|
||||
}
|
||||
|
||||
LogosTabBar {
|
||||
id: operationTabBar
|
||||
Layout.fillWidth: true
|
||||
LogosTabButton { text: qsTr("Node") }
|
||||
LogosTabButton { text: qsTr("Operations") }
|
||||
LogosTabButton {
|
||||
text: qsTr("Operations")
|
||||
enabled: opPage.nodeRunning
|
||||
}
|
||||
}
|
||||
|
||||
StackLayout {
|
||||
@ -266,11 +269,7 @@ Rectangle {
|
||||
NavItem { label: qsTr("Accounts"); index: 0 }
|
||||
NavItem { label: qsTr("Transfer"); index: 1 }
|
||||
NavItem { label: qsTr("Leader Rewards"); index: 2 }
|
||||
NavItem {
|
||||
label: qsTr("Channel Deposit")
|
||||
index: 3
|
||||
itemEnabled: opPage.nodeRunning
|
||||
}
|
||||
NavItem { label: qsTr("Channel Deposit"); index: 3 }
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
@ -406,7 +405,6 @@ Rectangle {
|
||||
component NavItem: Rectangle {
|
||||
property string label
|
||||
property int index
|
||||
property bool itemEnabled: true
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
@ -414,7 +412,6 @@ Rectangle {
|
||||
color: opPage.operationIndex === index
|
||||
? Theme.palette.backgroundTertiary
|
||||
: (navMouse.containsMouse ? Theme.palette.backgroundSecondary : "transparent")
|
||||
opacity: itemEnabled ? 1.0 : 0.4
|
||||
|
||||
LogosText {
|
||||
anchors.left: parent.left
|
||||
@ -434,7 +431,6 @@ Rectangle {
|
||||
MouseArea {
|
||||
id: navMouse
|
||||
anchors.fill: parent
|
||||
enabled: itemEnabled
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: opPage.operationIndex = index
|
||||
|
||||
@ -19,6 +19,12 @@ Rectangle {
|
||||
// The transactions role is a QStringList; surface it for the Repeater.
|
||||
readonly property var transactionsList: model.transactions || []
|
||||
|
||||
// Roles read as `undefined` during the brief QtRO replica sync at node
|
||||
// startup. Treat the fallback as active ONLY when the model says so
|
||||
// explicitly (parsed === false); undefined means "still loading", not
|
||||
// "unparsed" — otherwise freshly-arrived blocks flash as Unparsed.
|
||||
readonly property bool isUnparsed: model.parsed === false
|
||||
|
||||
width: ListView.view ? ListView.view.width : implicitWidth
|
||||
implicitHeight: col.implicitHeight + 2 * Theme.spacing.medium
|
||||
|
||||
@ -53,7 +59,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
LogosText {
|
||||
visible: model.parsed
|
||||
visible: !del.isUnparsed
|
||||
text: qsTr("slot %1").arg(model.slot || qsTr("?"))
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
@ -61,7 +67,7 @@ Rectangle {
|
||||
|
||||
// Version badge
|
||||
Rectangle {
|
||||
visible: model.parsed && (model.version || "").length > 0
|
||||
visible: !del.isUnparsed && (model.version || "").length > 0
|
||||
radius: Theme.spacing.radiusSmall
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
@ -78,7 +84,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
LogosText {
|
||||
visible: !model.parsed
|
||||
visible: del.isUnparsed
|
||||
text: qsTr("Unparsed block")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.warning
|
||||
@ -87,7 +93,7 @@ Rectangle {
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
LogosText {
|
||||
visible: model.parsed
|
||||
visible: !del.isUnparsed
|
||||
text: qsTr("%1 tx").arg(model.txCount || 0)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
@ -109,14 +115,23 @@ Rectangle {
|
||||
}
|
||||
|
||||
// Parsed: structured header
|
||||
HashRow { label: qsTr("Parent block"); value: model.parentBlock || ""; visible: model.parsed }
|
||||
HashRow { label: qsTr("Block root"); value: model.blockRoot || ""; visible: model.parsed }
|
||||
HashRow { label: qsTr("Signature"); value: model.signature || ""; visible: model.parsed }
|
||||
HashRow {
|
||||
label: qsTr("Parent block"); value: model.parentBlock || ""; visible: !del.isUnparsed
|
||||
onCopyRequested: (t) => del.copyToClipboard(t)
|
||||
}
|
||||
HashRow {
|
||||
label: qsTr("Block root"); value: model.blockRoot || ""; visible: !del.isUnparsed
|
||||
onCopyRequested: (t) => del.copyToClipboard(t)
|
||||
}
|
||||
HashRow {
|
||||
label: qsTr("Signature"); value: model.signature || ""; visible: !del.isUnparsed
|
||||
onCopyRequested: (t) => del.copyToClipboard(t)
|
||||
}
|
||||
|
||||
// Proof of leadership (collapsible sub-group)
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: model.parsed
|
||||
visible: !del.isUnparsed
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: (del.proofExpanded ? "▾ " : "▸ ") + qsTr("Proof of leadership")
|
||||
@ -129,34 +144,35 @@ Rectangle {
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
visible: model.parsed && del.proofExpanded
|
||||
visible: !del.isUnparsed && del.proofExpanded
|
||||
spacing: Theme.spacing.small
|
||||
HashRow { label: qsTr("Leader key"); value: model.leaderKey || "" }
|
||||
HashRow { label: qsTr("Entropy"); value: model.entropy || "" }
|
||||
HashRow { label: qsTr("Proof"); value: model.proof || "" }
|
||||
HashRow { label: qsTr("Voucher cm"); value: model.voucherCm || "" }
|
||||
HashRow { label: qsTr("Leader key"); value: model.leaderKey || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
|
||||
HashRow { label: qsTr("Entropy"); value: model.entropy || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
|
||||
HashRow { label: qsTr("Proof"); value: model.proof || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
|
||||
HashRow { label: qsTr("Voucher cm"); value: model.voucherCm || ""; onCopyRequested: (t) => del.copyToClipboard(t) }
|
||||
}
|
||||
|
||||
// Transactions
|
||||
LogosText {
|
||||
visible: model.parsed
|
||||
visible: !del.isUnparsed
|
||||
text: qsTr("Transactions (%1)").arg(model.txCount || 0)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: model.parsed
|
||||
spacing: Theme.spacing.tiny
|
||||
visible: !del.isUnparsed
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
Repeater {
|
||||
model: del.expanded ? del.transactionsList : []
|
||||
delegate: TxItem {
|
||||
delegate: TransactionDelegate {
|
||||
required property int index
|
||||
required property string modelData
|
||||
Layout.fillWidth: true
|
||||
idx: index
|
||||
json: modelData
|
||||
onCopyToClipboard: (t) => del.copyToClipboard(t)
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +187,7 @@ Rectangle {
|
||||
// Unparsed: raw fallback
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
visible: !model.parsed
|
||||
visible: del.isUnparsed
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: qsTr("Raw payload")
|
||||
@ -183,80 +199,9 @@ Rectangle {
|
||||
}
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
visible: !model.parsed
|
||||
json: model.rawJson || ""
|
||||
visible: del.isUnparsed
|
||||
json: model.rawJson || qsTr("(no payload)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Inline helpers ----
|
||||
|
||||
component HashRow: RowLayout {
|
||||
property string label
|
||||
property string value
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: label
|
||||
Layout.preferredWidth: 110
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: value && value.length > 0 ? value : "—"
|
||||
elide: Text.ElideMiddle
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
}
|
||||
LogosCopyButton {
|
||||
visible: value && value.length > 0
|
||||
onCopyText: del.copyToClipboard(value)
|
||||
}
|
||||
}
|
||||
|
||||
component JsonBlock: Rectangle {
|
||||
property string json
|
||||
implicitHeight: jsonText.implicitHeight + 2 * Theme.spacing.small
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusSmall
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
LogosText {
|
||||
id: jsonText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.small
|
||||
text: parent.json
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
}
|
||||
|
||||
component TxItem: ColumnLayout {
|
||||
id: txRoot
|
||||
property int idx
|
||||
property string json
|
||||
property bool open: false
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: (txRoot.open ? "▾ " : "▸ ") + qsTr("Transaction %1").arg(txRoot.idx + 1)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
TapHandler { onTapped: txRoot.open = !txRoot.open }
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosCopyButton { onCopyText: del.copyToClipboard(txRoot.json) }
|
||||
}
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
visible: txRoot.open
|
||||
json: txRoot.json
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
src/qml/controls/HashRow.qml
Normal file
41
src/qml/controls/HashRow.qml
Normal file
@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// A labelled value row with an elided monospace value and a copy button.
|
||||
// Used for hashes/keys and any short scalar field.
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
property string label: ""
|
||||
property string value: ""
|
||||
property int labelWidth: 110
|
||||
property bool copyable: true
|
||||
|
||||
signal copyRequested(string text)
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
LogosText {
|
||||
visible: root.label.length > 0
|
||||
text: root.label
|
||||
Layout.preferredWidth: root.labelWidth
|
||||
Layout.alignment: Qt.AlignTop
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: root.value && root.value.length > 0 ? root.value : "—"
|
||||
elide: Text.ElideMiddle
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
}
|
||||
LogosCopyButton {
|
||||
visible: root.copyable && root.value && root.value.length > 0
|
||||
onCopyText: root.copyRequested(root.value)
|
||||
}
|
||||
}
|
||||
32
src/qml/controls/JsonBlock.qml
Normal file
32
src/qml/controls/JsonBlock.qml
Normal file
@ -0,0 +1,32 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// A boxed, monospace, wrapping JSON/text block. Used for prettified payloads
|
||||
// and raw fallbacks.
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string json: ""
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: jsonText.implicitHeight + 2 * Theme.spacing.small
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusSmall
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
LogosText {
|
||||
id: jsonText
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.small
|
||||
text: root.json
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "monospace"
|
||||
wrapMode: Text.WrapAnywhere
|
||||
}
|
||||
}
|
||||
179
src/qml/controls/TransactionDelegate.qml
Normal file
179
src/qml/controls/TransactionDelegate.qml
Normal file
@ -0,0 +1,179 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
// Structured view of a single block transaction (a SignedMantleTx JSON string).
|
||||
//
|
||||
// Shape (see nomos-node core/src/mantle):
|
||||
// { "mantle_tx": { "ops": [ {opcode, payload}, ... ] },
|
||||
// "ops_proofs": [ <proof>, ... ] } // paired with ops by index
|
||||
//
|
||||
// Each op is labelled by its opcode name; the payload (and paired proof) are
|
||||
// shown as prettified JSON. Unparsable transactions fall back to raw JSON.
|
||||
ColumnLayout {
|
||||
id: txRoot
|
||||
|
||||
property string json: ""
|
||||
property int idx: 0
|
||||
property bool open: false
|
||||
|
||||
signal copyToClipboard(string text)
|
||||
|
||||
readonly property var parsed: safeParse(json)
|
||||
readonly property bool parseFailed: parsed === null
|
||||
readonly property var ops: (parsed && parsed.mantle_tx && parsed.mantle_tx.ops)
|
||||
? parsed.mantle_tx.ops : []
|
||||
readonly property var proofs: (parsed && parsed.ops_proofs) ? parsed.ops_proofs : []
|
||||
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
function safeParse(s) {
|
||||
try { return JSON.parse(s) } catch (e) { return null }
|
||||
}
|
||||
|
||||
function opName(code) {
|
||||
switch (code) {
|
||||
case 0: return qsTr("Transfer")
|
||||
case 16: return qsTr("Channel Config")
|
||||
case 17: return qsTr("Channel Inscribe")
|
||||
case 18: return qsTr("Channel Deposit")
|
||||
case 19: return qsTr("Channel Withdraw")
|
||||
case 32: return qsTr("SDP Declare")
|
||||
case 33: return qsTr("SDP Withdraw")
|
||||
case 34: return qsTr("SDP Active")
|
||||
case 48: return qsTr("Leader Claim")
|
||||
default: return qsTr("Op 0x%1").arg(Number(code).toString(16))
|
||||
}
|
||||
}
|
||||
|
||||
function opSummary() {
|
||||
if (parseFailed) return qsTr("unparsed")
|
||||
if (!ops.length) return qsTr("no ops")
|
||||
var names = []
|
||||
for (var i = 0; i < ops.length; i++) names.push(opName(ops[i].opcode))
|
||||
return names.join(", ")
|
||||
}
|
||||
|
||||
function pretty(v) {
|
||||
try { return JSON.stringify(v, null, 2) } catch (e) { return String(v) }
|
||||
}
|
||||
|
||||
function proofVariant(p) {
|
||||
if (!p || typeof p !== "object") return ""
|
||||
var ks = Object.keys(p)
|
||||
return ks.length ? ks[0] : ""
|
||||
}
|
||||
|
||||
// ---- Header (toggle) ----
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: (txRoot.open ? "▾ " : "▸ ") + qsTr("Transaction %1").arg(txRoot.idx + 1)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
TapHandler { onTapped: txRoot.open = !txRoot.open }
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: txRoot.opSummary()
|
||||
elide: Text.ElideRight
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
TapHandler { onTapped: txRoot.open = !txRoot.open }
|
||||
}
|
||||
LogosCopyButton { onCopyText: txRoot.copyToClipboard(txRoot.json) }
|
||||
}
|
||||
|
||||
// ---- Body ----
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
visible: txRoot.open
|
||||
spacing: Theme.spacing.small
|
||||
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
visible: txRoot.parseFailed
|
||||
json: txRoot.json
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: txRoot.open && !txRoot.parseFailed ? txRoot.ops : []
|
||||
delegate: OpView {
|
||||
required property int index
|
||||
required property var modelData
|
||||
Layout.fillWidth: true
|
||||
op: modelData
|
||||
proof: txRoot.proofs.length > index ? txRoot.proofs[index] : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Inline: one operation (opcode name + payload/proof JSON) ----
|
||||
component OpView: ColumnLayout {
|
||||
id: opView
|
||||
property var op
|
||||
property var proof: null
|
||||
spacing: Theme.spacing.tiny
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.small
|
||||
Rectangle {
|
||||
radius: Theme.spacing.radiusSmall
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
implicitWidth: opcodeText.implicitWidth + 2 * Theme.spacing.small
|
||||
implicitHeight: opcodeText.implicitHeight + Theme.spacing.tiny
|
||||
LogosText {
|
||||
id: opcodeText
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("op %1").arg(opView.op && opView.op.opcode !== undefined ? opView.op.opcode : "?")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
}
|
||||
LogosText {
|
||||
Layout.fillWidth: true
|
||||
text: txRoot.opName(opView.op ? opView.op.opcode : -1)
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
}
|
||||
LogosCopyButton {
|
||||
onCopyText: txRoot.copyToClipboard(txRoot.pretty(opView.op ? opView.op.payload : null))
|
||||
}
|
||||
}
|
||||
|
||||
// payload as JSON
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
json: txRoot.pretty(opView.op ? opView.op.payload : null)
|
||||
}
|
||||
|
||||
// paired proof
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
visible: opView.proof !== null && opView.proof !== undefined
|
||||
spacing: Theme.spacing.small
|
||||
LogosText {
|
||||
text: qsTr("Proof · %1").arg(txRoot.proofVariant(opView.proof) || qsTr("unknown"))
|
||||
color: Theme.palette.textSecondary
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
LogosCopyButton { onCopyText: txRoot.copyToClipboard(txRoot.pretty(opView.proof)) }
|
||||
}
|
||||
JsonBlock {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Theme.spacing.medium
|
||||
visible: opView.proof !== null && opView.proof !== undefined
|
||||
json: (opView.proof !== null && opView.proof !== undefined) ? txRoot.pretty(opView.proof) : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user