feat: add deplayment config path in ui + some visual improvements

This commit is contained in:
Khushboo Mehta 2026-02-24 11:28:28 +01:00
parent c8e4afb4d9
commit d94d1c3f3e
8 changed files with 235 additions and 145 deletions

7
flake.lock generated
View File

@ -801,16 +801,17 @@
]
},
"locked": {
"lastModified": 1771871578,
"narHash": "sha256-6Mu3cmdhd8e7i+n8OWcaIBye+i12gwlwt1fhd9QCbCI=",
"lastModified": 1770837874,
"narHash": "sha256-wr75lv1q4U1FS5+l/6ypwzJFJe06l2RyUvx1npoRS88=",
"owner": "logos-co",
"repo": "logos-liblogos",
"rev": "19d29d4ef99292d9285b3a561cb7ea8029be3b74",
"rev": "e3741c01fd3abf6b7bd9ff2fa8edf89c41fc0cea",
"type": "github"
},
"original": {
"owner": "logos-co",
"repo": "logos-liblogos",
"rev": "e3741c01fd3abf6b7bd9ff2fa8edf89c41fc0cea",
"type": "github"
}
},

View File

@ -5,7 +5,7 @@
# Follow the same nixpkgs as logos-liblogos to ensure compatibility
nixpkgs.follows = "logos-liblogos/nixpkgs";
logos-cpp-sdk.url = "github:logos-co/logos-cpp-sdk";
logos-liblogos.url = "github:logos-co/logos-liblogos";
logos-liblogos.url = "github:logos-co/logos-liblogos?rev=e3741c01fd3abf6b7bd9ff2fa8edf89c41fc0cea";
logos-blockchain-module.url = "github:logos-blockchain/logos-blockchain-module";
logos-capability-module.url = "github:logos-co/logos-capability-module";
logos-design-system.url = "github:logos-co/logos-design-system";

View File

@ -1,34 +1,43 @@
#include "BlockchainBackend.h"
#include <QByteArray>
#include <QClipboard>
#include <QDebug>
#include <QDateTime>
#include <QGuiApplication>
#include <QSettings>
#include <QTimer>
#include <QUrl>
#include <QVariant>
namespace {
const char SETTINGS_ORG[] = "Logos";
const char SETTINGS_APP[] = "BlockchainUI";
const char CONFIG_PATH_KEY[] = "configPath";
const char USER_CONFIG_KEY[] = "userConfigPath";
const char DEPLOYMENT_CONFIG_KEY[] = "deploymentConfigPath";
const QString BLOCKCHAIN_MODULE_NAME = QStringLiteral("liblogos_blockchain_module");
}
BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent)
: QObject(parent),
m_status(NotStarted),
m_configPath(""),
m_userConfig(""),
m_deploymentConfig(""),
m_logModel(new LogModel(this)),
m_logosAPI(nullptr),
m_blockchainClient(nullptr)
{
QSettings s(SETTINGS_ORG, SETTINGS_APP);
const QString envConfigPath = QString::fromUtf8(qgetenv("LB_CONFIG_PATH"));
const QString savedConfigPath = s.value(CONFIG_PATH_KEY).toString();
const QString savedUserConfig = s.value(USER_CONFIG_KEY).toString();
const QString savedDeploymentConfig = s.value(DEPLOYMENT_CONFIG_KEY).toString();
if (!envConfigPath.isEmpty()) {
m_configPath = envConfigPath;
} else if (!savedConfigPath.isEmpty()) {
m_configPath = savedConfigPath;
m_userConfig = envConfigPath;
} else if (!savedUserConfig.isEmpty()) {
m_userConfig = savedUserConfig;
}
if (!savedDeploymentConfig.isEmpty()) {
m_deploymentConfig = savedDeploymentConfig;
}
if (!logosAPI) {
@ -67,14 +76,25 @@ void BlockchainBackend::setStatus(BlockchainStatus newStatus)
}
}
void BlockchainBackend::setConfigPath(const QString& path)
void BlockchainBackend::setUserConfig(const QString& path)
{
const QString localPath = QUrl::fromUserInput(path).toLocalFile();
if (m_configPath != localPath) {
m_configPath = localPath;
if (m_userConfig != localPath) {
m_userConfig = localPath;
QSettings s(SETTINGS_ORG, SETTINGS_APP);
s.setValue(CONFIG_PATH_KEY, m_configPath);
emit configPathChanged();
s.setValue(USER_CONFIG_KEY, m_userConfig);
emit userConfigChanged();
}
}
void BlockchainBackend::setDeploymentConfig(const QString& path)
{
const QString localPath = QUrl::fromUserInput(path).toLocalFile();
if (m_deploymentConfig != localPath) {
m_deploymentConfig = localPath;
QSettings s(SETTINGS_ORG, SETTINGS_APP);
s.setValue(DEPLOYMENT_CONFIG_KEY, m_deploymentConfig);
emit deploymentConfigChanged();
}
}
@ -83,6 +103,12 @@ void BlockchainBackend::clearLogs()
m_logModel->clear();
}
void BlockchainBackend::copyToClipboard(const QString& text)
{
if (QGuiApplication::clipboard())
QGuiApplication::clipboard()->setText(text);
}
QString BlockchainBackend::getBalance(const QString& addressHex)
{
if (!m_blockchainClient) {
@ -118,7 +144,7 @@ void BlockchainBackend::startBlockchain()
setStatus(Starting);
QVariant result = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "start", m_configPath, QString());
BLOCKCHAIN_MODULE_NAME, "start", m_userConfig, m_deploymentConfig);
int resultCode = result.isValid() ? result.toInt() : -1;
if (resultCode == 0 || resultCode == 1) {
@ -174,11 +200,7 @@ void BlockchainBackend::onNewBlock(const QVariantList& data)
QString line;
if (!data.isEmpty()) {
QString blockInfo = data.first().toString();
QString shortInfo = blockInfo.left(80);
if (blockInfo.length() > 80) {
shortInfo += "...";
}
line = QString("[%1] 📦 New block: %2").arg(timestamp, shortInfo);
line = QString("[%1] 📦 New block: %2").arg(timestamp, blockInfo);
} else {
line = QString("[%1] 📦 New block (no data)").arg(timestamp);
}

View File

@ -27,7 +27,8 @@ public:
Q_ENUM(BlockchainStatus)
Q_PROPERTY(BlockchainStatus status READ status NOTIFY statusChanged)
Q_PROPERTY(QString configPath READ configPath WRITE setConfigPath NOTIFY configPathChanged)
Q_PROPERTY(QString userConfig READ userConfig WRITE setUserConfig NOTIFY userConfigChanged)
Q_PROPERTY(QString deploymentConfig READ deploymentConfig WRITE setDeploymentConfig NOTIFY deploymentConfigChanged)
Q_PROPERTY(LogModel* logModel READ logModel CONSTANT)
Q_PROPERTY(QStringList knownAddresses READ knownAddresses NOTIFY knownAddressesChanged)
@ -35,12 +36,15 @@ public:
~BlockchainBackend();
BlockchainStatus status() const { return m_status; }
QString configPath() const { return m_configPath; }
QString userConfig() const { return m_userConfig; }
QString deploymentConfig() const { return m_deploymentConfig; }
LogModel* logModel() const { return m_logModel; }
QStringList knownAddresses() const { return m_knownAddresses; }
void setConfigPath(const QString& path);
void setUserConfig(const QString& path);
void setDeploymentConfig(const QString& path);
Q_INVOKABLE void clearLogs();
Q_INVOKABLE void copyToClipboard(const QString& text);
Q_INVOKABLE QString getBalance(const QString& addressHex);
Q_INVOKABLE QString transferFunds(
const QString& fromKeyHex,
@ -55,14 +59,16 @@ public slots:
signals:
void statusChanged();
void configPathChanged();
void userConfigChanged();
void deploymentConfigChanged();
void knownAddressesChanged();
private:
void setStatus(BlockchainStatus newStatus);
BlockchainStatus m_status;
QString m_configPath;
QString m_userConfig;
QString m_deploymentConfig;
LogModel* m_logModel;
QStringList m_knownAddresses;

View File

@ -64,15 +64,17 @@ Rectangle {
Layout.preferredWidth: parent.width / 2
statusText: _d.getStatusString(backend.status)
statusColor: _d.getStatusColor(backend.status)
configPath: backend.configPath
canStart: !!backend.configPath
userConfig: backend.userConfig
deploymentConfig: backend.deploymentConfig
canStart: !!backend.userConfig
&& backend.status !== BlockchainBackend.Starting
&& backend.status !== BlockchainBackend.Stopping
isRunning: backend.status === BlockchainBackend.Running
onStartRequested: backend.startBlockchain()
onStopRequested: backend.stopBlockchain()
onChangeConfigRequested: fileDialog.open()
onChangeUserConfigRequested: userConfigFileDialog.open()
onChangeDeploymentConfigRequested: deploymentConfigFileDialog.open()
}
WalletView {
@ -96,16 +98,23 @@ Rectangle {
logModel: backend.logModel
onClearRequested: backend.clearLogs()
onCopyToClipboard: (text) => backend.copyToClipboard(text)
}
}
FileDialog {
id: fileDialog
id: userConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)"]
currentFolder: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
onAccepted: {
backend.configPath = selectedFile
}
onAccepted: backend.userConfig = selectedFile
}
FileDialog {
id: deploymentConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)"]
currentFolder: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
onAccepted: backend.deploymentConfig = selectedFile
}
}

View File

@ -12,6 +12,7 @@ Control {
required property var logModel // LogModel (QAbstractListModel with "text" role)
signal clearRequested()
signal copyToClipboard(string text)
background: Rectangle {
color: Theme.palette.background
@ -60,11 +61,18 @@ Control {
model: root.logModel
spacing: 2
delegate: LogosText {
delegate: ItemDelegate{
width: ListView.view.width
text: model.text
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.Wrap
contentItem: LogosText {
text: model.text
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.Wrap
}
background: Rectangle {
color: hovered ? Theme.palette.background: "transparent"
radius: 2
}
onClicked: root.copyToClipboard(model.text)
}
LogosText {

View File

@ -11,13 +11,15 @@ ColumnLayout {
// --- Public API ---
required property string statusText
required property color statusColor
required property string configPath
required property string userConfig
required property string deploymentConfig
required property bool canStart
required property bool isRunning
signal startRequested()
signal stopRequested()
signal changeConfigRequested()
signal changeUserConfigRequested()
signal changeDeploymentConfigRequested()
spacing: Theme.spacing.large
@ -71,14 +73,15 @@ ColumnLayout {
Rectangle {
Layout.preferredWidth: parent.width * 0.9
Layout.preferredHeight: implicitHeight
implicitHeight: configContent.implicitHeight + 2 * Theme.spacing.large
implicitHeight: contentLayout.implicitHeight + 2 * Theme.spacing.large
color: Theme.palette.backgroundTertiary
radius: Theme.spacing.radiusLarge
border.color: Theme.palette.border
border.width: 1
ColumnLayout {
id: configContent
id: contentLayout
anchors.left: parent.left
anchors.right: parent.right
@ -86,29 +89,55 @@ ColumnLayout {
anchors.margins: Theme.spacing.large
spacing: Theme.spacing.medium
LogosText {
text: qsTr("Current Config: ")
font.bold: true
// User Config Card
ConfigSelectionPanel {
id: userConfigContent
title: qsTr("User Config: ")
configPath: root.userConfig || qsTr("No file selected")
onChangeRequested: root.changeUserConfigRequested()
}
LogosText {
Layout.fillWidth: true
Layout.topMargin: -Theme.spacing.medium
text: root.configPath || qsTr("No file selected")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
// Deployment Config Card
ConfigSelectionPanel {
id: deploymentConfigContent
LogosButton {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: parent.width
Layout.preferredHeight: 50
text: qsTr("Change")
onClicked: root.changeConfigRequested()
title: qsTr("Deployment Config: ")
configPath: root.deploymentConfig || qsTr("No file selected")
onChangeRequested: root.changeDeploymentConfigRequested()
}
}
}
Item { Layout.fillHeight: true }
component ConfigSelectionPanel: ColumnLayout {
property string title
property string configPath
signal changeRequested()
spacing: Theme.spacing.medium
LogosText {
text: title
font.bold: true
}
LogosText {
Layout.fillWidth: true
Layout.topMargin: -Theme.spacing.small
text: configPath
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
LogosButton {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: 50
text: qsTr("Change")
onClicked: changeRequested()
}
}
}

View File

@ -50,80 +50,9 @@ ColumnLayout {
}
// Dropdown of known addresses, or type a custom address
ComboBox {
StyledAddressComboBox {
id: balanceAddressCombo
Layout.fillWidth: true
padding: Theme.spacing.large
editable: true
model: knownAddresses
font.pixelSize: Theme.typography.secondaryText
background: Rectangle {
color: Theme.palette.backgroundTertiary
radius: Theme.spacing.radiusLarge
border.color: Theme.palette.border
border.width: 1
}
indicator: LogosText {
id: indicatorText
text: "▼"
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
x: balanceAddressCombo.width - width - Theme.spacing.small
y: (balanceAddressCombo.height - height) / 2
visible: balanceAddressCombo.count > 0
}
contentItem: LogosText {
width: parent.width - indicatorText.width - Theme.spacing.large
font.pixelSize: Theme.typography.secondaryText
font.bold: true
text: balanceAddressCombo.displayText
elide: Text.ElideRight
}
delegate: ItemDelegate {
id: delegate
width: balanceAddressCombo.width
contentItem: LogosText {
width: parent.width
height: contentHeight + Theme.spacing.large
font.pixelSize: Theme.typography.secondaryText
font.bold: true
text: modelData
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: delegate.highlighted ?
Theme.palette.backgroundTertiary:
Theme.palette.backgroundSecondary
}
highlighted: balanceAddressCombo.highlightedIndex === index
}
popup: Popup {
y: balanceAddressCombo.height - 1
width: balanceAddressCombo.width
height: contentItem.implicitHeight + 100
padding: 1
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: balanceAddressCombo.popup.visible ? balanceAddressCombo.delegateModel : null
ScrollIndicator.vertical: ScrollIndicator { }
highlightFollowsCurrentItem: false
}
background: Rectangle {
color: Theme.palette.backgroundSecondary
border.color: Theme.palette.border
border.width: 1
radius: Theme.spacing.radiusLarge
}
}
}
RowLayout {
@ -169,7 +98,7 @@ ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacing.large
spacing: Theme.spacing.large
spacing: Theme.spacing.small
LogosText {
text: qsTr("Transfer funds")
@ -177,18 +106,22 @@ ColumnLayout {
font.bold: true
}
CustomTextFeild {
id: transferFromField
placeholderText: qsTr("From key (64 hex chars)")
StyledAddressComboBox {
id: transferFromCombo
model: knownAddresses
}
CustomTextFeild {
LogosTextField {
id: transferToField
Layout.fillWidth: true
Layout.preferredHeight: 30
placeholderText: qsTr("To key (64 hex chars)")
}
CustomTextFeild {
LogosTextField {
id: transferAmountField
Layout.fillWidth: true
Layout.preferredHeight: 30
placeholderText: qsTr("Amount")
}
@ -201,7 +134,7 @@ ColumnLayout {
id: transferButton
text: qsTr("Transfer")
Layout.alignment: Qt.AlignRight
onClicked: root.transferRequested(transferFromField.text, transferToField.text, transferAmountField.text)
onClicked: root.transferRequested(transferFromCombo.currentText.trim(), transferToField.text.trim(), transferAmountField.text)
}
LogosButton {
@ -226,18 +159,100 @@ ColumnLayout {
Layout.preferredHeight: Theme.spacing.small
}
component CustomTextFeild: TextField {
id: textField
component StyledAddressComboBox: ComboBox {
id: comboControl
Layout.fillWidth: true
placeholderTextColor: Theme.palette.textMuted
padding: Theme.spacing.large
editable: true
font.pixelSize: Theme.typography.secondaryText
background: Rectangle {
radius: Theme.spacing.radiusSmall
color: Theme.palette.backgroundSecondary
border.color: textField.activeFocus ?
Theme.palette.overlayOrange :
Theme.palette.backgroundElevated
color: Theme.palette.backgroundTertiary
radius: Theme.spacing.radiusLarge
border.color: Theme.palette.border
border.width: 1
}
indicator: LogosText {
id: comboIndicator
text: "▼"
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
x: comboControl.width - width - Theme.spacing.small
y: (comboControl.height - height) / 2
visible: comboControl.count > 0
}
contentItem: Item {
implicitWidth: comboControl.availableWidth
implicitHeight: 30
TextField {
id: comboTextField
anchors.fill: parent
leftPadding: 0
rightPadding: comboControl.count > 0 ? comboIndicator.width + Theme.spacing.small : Theme.spacing.small
topPadding: 0
bottomPadding: 0
verticalAlignment: Text.AlignVCenter
font.pixelSize: Theme.typography.secondaryText
font.bold: true
text: comboControl.editText
onTextChanged: if (text !== comboControl.editText) comboControl.editText = text
selectByMouse: true
color: Theme.palette.text
background: Item { }
}
MouseArea {
anchors.fill: parent
visible: comboControl.count > 0
z: 1
onPressed: {
comboControl.popup.visible ? comboControl.popup.close() : comboControl.popup.open()
}
}
}
delegate: ItemDelegate {
id: comboDelegate
width: comboControl.width
contentItem: LogosText {
width: parent.width
height: contentHeight + Theme.spacing.large
font.pixelSize: Theme.typography.secondaryText
font.bold: true
text: modelData
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: comboDelegate.highlighted ?
Theme.palette.backgroundTertiary :
Theme.palette.backgroundSecondary
}
highlighted: comboControl.highlightedIndex === index
}
popup: Popup {
y: comboControl.height - 1
width: comboControl.width
height: contentItem.implicitHeight
padding: 1
onOpened: if (comboControl.count === 0) close()
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: comboControl.popup.visible ? comboControl.delegateModel : null
ScrollIndicator.vertical: ScrollIndicator { }
highlightFollowsCurrentItem: false
}
background: Rectangle {
color: Theme.palette.backgroundSecondary
border.color: Theme.palette.border
border.width: 1
radius: Theme.spacing.radiusLarge
}
}
}
}