feat: add support to generate configs

This commit is contained in:
Khushboo Mehta 2026-02-24 17:05:46 +01:00
parent 0baedaae98
commit 65e6576474
11 changed files with 662 additions and 111 deletions

14
flake.lock generated
View File

@ -23,16 +23,16 @@
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1771872481,
"narHash": "sha256-IrUERmJHFcRZB4jzOJ4FWhQtbnC6dujWMz4mbo/1ZpA=",
"lastModified": 1772023618,
"narHash": "sha256-rDjDhC9CxwPK2b0Pwc7UXh+xCy5EkK+T5LJ0I7j2OHM=",
"owner": "logos-blockchain",
"repo": "logos-blockchain",
"rev": "7bcd351d5be0b7f28ec4e730ebe0441744243c4c",
"rev": "fed5efe6bd52c0d62d490c35f5c2a4368b94342f",
"type": "github"
},
"original": {
"owner": "logos-blockchain",
"ref": "0.1.7",
"ref": "fed5efe6bd52c0d62d490c35f5c2a4368b94342f",
"repo": "logos-blockchain",
"type": "github"
}
@ -68,11 +68,11 @@
]
},
"locked": {
"lastModified": 1771930601,
"narHash": "sha256-FTMRqWVjdpDLcKYdFNA7kxr4Pmp1q73vKhEoTcoDSlI=",
"lastModified": 1772023717,
"narHash": "sha256-aFzRBR3CVosBOqQeYCB1tz7Vp/rkDYLtAvqgQjTdE4c=",
"owner": "logos-blockchain",
"repo": "logos-blockchain-module",
"rev": "888a66849ffc9babeb33ac070145b29861f0a23d",
"rev": "1320bd760ffe9c3cc2d8c11f76d8296b01faeef9",
"type": "github"
},
"original": {

View File

@ -3,7 +3,10 @@
#include <QClipboard>
#include <QDebug>
#include <QDateTime>
#include <QDir>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSettings>
#include <QTimer>
#include <QUrl>
@ -98,6 +101,14 @@ void BlockchainBackend::setDeploymentConfig(const QString& path)
}
}
void BlockchainBackend::setUseGeneratedConfig(bool useGenerated)
{
if (m_useGeneratedConfig != useGenerated) {
m_useGeneratedConfig = useGenerated;
emit useGeneratedConfigChanged();
}
}
void BlockchainBackend::clearLogs()
{
m_logModel->clear();
@ -206,3 +217,80 @@ void BlockchainBackend::onNewBlock(const QVariantList& data)
}
m_logModel->append(line);
}
static QString toLocalPath(const QString& pathInput)
{
if (pathInput.trimmed().isEmpty())
return pathInput;
return QUrl::fromUserInput(pathInput).toLocalFile();
}
int BlockchainBackend::generateConfig(const QString& outputPath,
const QStringList& initialPeers,
int netPort,
int blendPort,
const QString& httpAddr,
const QString& externalAddress,
bool noPublicIpCheck,
int deploymentMode,
const QString& deploymentConfigPath,
const QString& statePath)
{
if (!m_blockchainClient) {
return -1;
}
QVariantMap normalized;
// Output path: default if empty, then normalize
QString out = outputPath.trimmed();
if (out.isEmpty()) {
out = generatedUserConfigPath();
} else {
out = toLocalPath(out);
}
normalized.insert(QStringLiteral("output"), out);
if (!initialPeers.isEmpty()) {
QVariantList peersList;
for (const QString& p : initialPeers) {
if (!p.trimmed().isEmpty())
peersList.append(p.trimmed());
}
if (!peersList.isEmpty())
normalized.insert(QStringLiteral("initial_peers"), peersList);
}
if (netPort > 0)
normalized.insert(QStringLiteral("net_port"), netPort);
if (blendPort > 0)
normalized.insert(QStringLiteral("blend_port"), blendPort);
if (!httpAddr.trimmed().isEmpty())
normalized.insert(QStringLiteral("http_addr"), httpAddr.trimmed());
if (!externalAddress.trimmed().isEmpty())
normalized.insert(QStringLiteral("external_address"), externalAddress.trimmed());
if (noPublicIpCheck)
normalized.insert(QStringLiteral("no_public_ip_check"), true);
if (deploymentMode == 0) {
QVariantMap deployment;
deployment.insert(QStringLiteral("well_known_deployment"), QStringLiteral("devnet"));
normalized.insert(QStringLiteral("deployment"), deployment);
} else if (deploymentMode == 1 && !deploymentConfigPath.trimmed().isEmpty()) {
QVariantMap deployment;
deployment.insert(QStringLiteral("config_path"), toLocalPath(deploymentConfigPath.trimmed()));
normalized.insert(QStringLiteral("deployment"), deployment);
}
if (!statePath.trimmed().isEmpty())
normalized.insert(QStringLiteral("state_path"), toLocalPath(statePath.trimmed()));
const QJsonDocument doc = QJsonDocument::fromVariant(normalized);
const QByteArray jsonBytes = doc.toJson(QJsonDocument::Compact);
const QString jsonToSend = QString::fromUtf8(jsonBytes);
QVariant result = m_blockchainClient->invokeRemoteMethod(
BLOCKCHAIN_MODULE_NAME, "generate_user_config_from_str", jsonToSend);
return result.isValid() ? result.toInt() : -1;
}
QString BlockchainBackend::generatedUserConfigPath() const
{
return QDir::currentPath() + QStringLiteral("/user_config.yaml");
}

View File

@ -3,6 +3,7 @@
#include <QObject>
#include <QString>
#include <QStringList>
#include <QVariant>
#include "logos_api.h"
#include "logos_api_client.h"
#include "LogModel.h"
@ -29,8 +30,10 @@ public:
Q_PROPERTY(BlockchainStatus status READ status NOTIFY statusChanged)
Q_PROPERTY(QString userConfig READ userConfig WRITE setUserConfig NOTIFY userConfigChanged)
Q_PROPERTY(QString deploymentConfig READ deploymentConfig WRITE setDeploymentConfig NOTIFY deploymentConfigChanged)
Q_PROPERTY(bool useGeneratedConfig READ useGeneratedConfig WRITE setUseGeneratedConfig NOTIFY useGeneratedConfigChanged)
Q_PROPERTY(LogModel* logModel READ logModel CONSTANT)
Q_PROPERTY(QStringList knownAddresses READ knownAddresses NOTIFY knownAddressesChanged)
Q_PROPERTY(QString generatedUserConfigPath READ generatedUserConfigPath CONSTANT)
explicit BlockchainBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
~BlockchainBackend();
@ -38,11 +41,13 @@ public:
BlockchainStatus status() const { return m_status; }
QString userConfig() const { return m_userConfig; }
QString deploymentConfig() const { return m_deploymentConfig; }
bool useGeneratedConfig() const { return m_useGeneratedConfig; }
LogModel* logModel() const { return m_logModel; }
QStringList knownAddresses() const { return m_knownAddresses; }
void setUserConfig(const QString& path);
void setDeploymentConfig(const QString& path);
void setUseGeneratedConfig(bool useGenerated);
Q_INVOKABLE void clearLogs();
Q_INVOKABLE void copyToClipboard(const QString& text);
Q_INVOKABLE QString getBalance(const QString& addressHex);
@ -53,6 +58,17 @@ public:
Q_INVOKABLE void startBlockchain();
Q_INVOKABLE void stopBlockchain();
Q_INVOKABLE void refreshKnownAddresses();
Q_INVOKABLE int generateConfig(const QString& outputPath,
const QStringList& initialPeers,
int netPort,
int blendPort,
const QString& httpAddr,
const QString& externalAddress,
bool noPublicIpCheck,
int deploymentMode,
const QString& deploymentConfigPath,
const QString& statePath);
Q_INVOKABLE QString generatedUserConfigPath() const;
public slots:
void onNewBlock(const QVariantList& data);
@ -61,6 +77,7 @@ signals:
void statusChanged();
void userConfigChanged();
void deploymentConfigChanged();
void useGeneratedConfigChanged();
void knownAddressesChanged();
private:
@ -69,6 +86,7 @@ private:
BlockchainStatus m_status;
QString m_userConfig;
QString m_deploymentConfig;
bool m_useGeneratedConfig = false;
LogModel* m_logModel;
QStringList m_knownAddresses;

View File

@ -5,6 +5,9 @@
<file>qml/views/StatusConfigView.qml</file>
<file>qml/views/LogsView.qml</file>
<file>qml/views/WalletView.qml</file>
<file>qml/views/GenerateConfigView.qml</file>
<file>qml/views/ConfigChoiceView.qml</file>
<file>qml/views/SetConfigPathView.qml</file>
<file>icons/blockchain.png</file>
</qresource>
</RCC>

View File

@ -1,8 +1,6 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import BlockchainBackend
import Logos.Theme
@ -46,75 +44,96 @@ Rectangle {
default: return Theme.palette.textSecondary;
}
}
property int currentPage: 0 // 0 = config choice (page 1), 1 = node + wallet + logs (page 2)
}
color: Theme.palette.background
SplitView {
StackLayout {
anchors.fill: parent
anchors.margins: Theme.spacing.large
orientation: Qt.Vertical
currentIndex: _d.currentPage
// Top: Status/Config + Wallet side-by-side
RowLayout {
SplitView.fillWidth: true
SplitView.minimumHeight: 200
StatusConfigView {
Layout.preferredWidth: parent.width / 2
statusText: _d.getStatusString(backend.status)
statusColor: _d.getStatusColor(backend.status)
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()
onChangeUserConfigRequested: userConfigFileDialog.open()
onChangeDeploymentConfigRequested: deploymentConfigFileDialog.open()
}
WalletView {
id: walletView
Layout.preferredWidth: parent.width / 2
knownAddresses: backend.knownAddresses
onGetBalanceRequested: function(addressHex) {
walletView.setBalanceResult(backend.getBalance(addressHex))
// Page 1: Config choice (Option 1: Generate own config, Option 2: Set path to configs)
ScrollView {
id: configChoiceScrollView
clip: true
ConfigChoiceView {
id: configChoiceView
width: configChoiceScrollView.availableWidth
userConfigPath: backend.userConfig
deploymentConfigPath: backend.deploymentConfig
generatedUserConfigPath: backend.generatedUserConfigPath
onUserConfigPathSelected: function(path) { backend.userConfig = path }
onDeploymentConfigPathSelected: function(path) { backend.deploymentConfig = path }
onSetPathToConfigsRequested: function() {
backend.useGeneratedConfig = false
_d.currentPage = 1
}
onTransferRequested: function(fromKeyHex, toKeyHex, amount) {
walletView.setTransferResult(backend.transferFunds(fromKeyHex, toKeyHex, amount))
onGenerateRequested: function(outputPath, initialPeers, netPort, blendPort, httpAddr, externalAddress, noPublicIpCheck, deploymentMode, deploymentConfigPath, statePath) {
configChoiceView.generateResultSuccess = false
configChoiceView.generateResultMessage = ""
var code = backend.generateConfig(outputPath, initialPeers, netPort, blendPort, httpAddr, externalAddress, noPublicIpCheck, deploymentMode, deploymentConfigPath, statePath)
configChoiceView.generateResultSuccess = (code === 0)
configChoiceView.generateResultMessage = code === 0 ? qsTr("Config generated successfully.") : qsTr("Generate failed (code: %1).").arg(code)
if (code === 0) {
backend.userConfig = (outputPath !== "") ? outputPath : backend.generatedUserConfigPath
backend.deploymentConfig = (deploymentMode === 1 && deploymentConfigPath !== "") ? deploymentConfigPath : ""
backend.useGeneratedConfig = true
_d.currentPage = 1
}
}
}
}
// Bottom: Logs
LogsView {
SplitView.fillWidth: true
SplitView.minimumHeight: 150
// Page 2: Start node, balances, transfer, logs
SplitView {
orientation: Qt.Vertical
logModel: backend.logModel
onClearRequested: backend.clearLogs()
onCopyToClipboard: (text) => backend.copyToClipboard(text)
RowLayout {
SplitView.fillWidth: true
SplitView.minimumHeight: 200
StatusConfigView {
Layout.preferredWidth: parent.width / 2
statusText: _d.getStatusString(backend.status)
statusColor: _d.getStatusColor(backend.status)
userConfig: backend.userConfig
deploymentConfig: backend.deploymentConfig
useGeneratedConfig: backend.useGeneratedConfig
canStart: !!backend.userConfig
&& backend.status !== BlockchainBackend.Starting
&& backend.status !== BlockchainBackend.Stopping
isRunning: backend.status === BlockchainBackend.Running
onStartRequested: backend.startBlockchain()
onStopRequested: backend.stopBlockchain()
onChangeConfigRequested: _d.currentPage = 0
}
WalletView {
id: walletView
Layout.preferredWidth: parent.width / 2
knownAddresses: backend.knownAddresses
onGetBalanceRequested: function(addressHex) {
walletView.setBalanceResult(backend.getBalance(addressHex))
}
onTransferRequested: function(fromKeyHex, toKeyHex, amount) {
walletView.setTransferResult(backend.transferFunds(fromKeyHex, toKeyHex, amount))
}
}
}
LogsView {
SplitView.fillWidth: true
SplitView.minimumHeight: 150
logModel: backend.logModel
onClearRequested: backend.clearLogs()
onCopyToClipboard: (text) => backend.copyToClipboard(text)
}
}
}
FileDialog {
id: userConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)"]
currentFolder: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
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

@ -0,0 +1,113 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Logos.Theme
import Logos.Controls
ColumnLayout {
id: root
property string userConfigPath: ""
property string deploymentConfigPath: ""
property string generatedUserConfigPath: ""
property bool generateResultSuccess: false
property string generateResultMessage: ""
signal generateRequested(string outputPath, var initialPeers, int netPort, int blendPort, string httpAddr, string externalAddress, bool noPublicIpCheck, int deploymentMode, string deploymentConfigPath, string statePath)
signal setPathToConfigsRequested()
signal userConfigPathSelected(string path)
signal deploymentConfigPathSelected(string path)
QtObject {
id: d
property int selectedOption: 0
}
spacing: Theme.spacing.large
LogosText {
Layout.alignment: Qt.AlignLeft
font.bold: true
font.pixelSize: Theme.typography.primaryText
text: qsTr("Choose how to set up your node config")
}
LogosText {
Layout.alignment: Qt.AlignLeft
Layout.topMargin: -Theme.spacing.small
text: qsTr("Generate a new config, or set paths to your existing config files.")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.large
LogosButton {
text: qsTr("Generate config")
Layout.preferredHeight: 50
Layout.fillWidth: true
onClicked: d.selectedOption = 1
}
LogosButton {
text: qsTr("Set path to config")
Layout.preferredHeight: 50
Layout.fillWidth: true
onClicked: d.selectedOption = 2
}
}
Loader {
id: contentLoader
Layout.fillWidth: true
Layout.fillHeight: true
visible: d.selectedOption === 1 || d.selectedOption === 2
active: d.selectedOption === 1 || d.selectedOption === 2
sourceComponent: d.selectedOption === 1 ? generateConfigComponent : (d.selectedOption === 2 ? setConfigPathComponent : null)
}
Item {
Layout.fillHeight: true
visible: d.selectedOption !== 1 && d.selectedOption !== 2
}
Component {
id: generateConfigComponent
ColumnLayout {
spacing: Theme.spacing.medium
GenerateConfigView {
generatedUserConfigPath: root.generatedUserConfigPath
resultSuccess: root.generateResultSuccess
resultMessage: root.generateResultMessage
Layout.fillWidth: true
onGenerateRequested: root.generateRequested(
outputPath,
initialPeers,
netPort,
blendPort,
httpAddr,
externalAddress,
noPublicIpCheck,
deploymentMode,
deploymentConfigPath,
statePath)
}
}
}
Component {
id: setConfigPathComponent
SetConfigPathView {
userConfigPath: root.userConfigPath
deploymentConfigPath: root.deploymentConfigPath
onUserConfigPathSelected: function(path) { root.userConfigPathSelected(path) }
onDeploymentConfigPathSelected: function(path) { root.deploymentConfigPathSelected(path) }
onSetPathToConfigsRequested: root.setPathToConfigsRequested()
}
}
}

View File

@ -0,0 +1,215 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import Logos.Theme
import Logos.Controls
ColumnLayout {
id: root
property string generatedUserConfigPath: ""
property bool resultSuccess: false
property string resultMessage: ""
signal generateRequested(string outputPath, var initialPeers, int netPort, int blendPort, string httpAddr, string externalAddress, bool noPublicIpCheck, int deploymentMode, string deploymentConfigPath, string statePath)
Component.onCompleted: {
if (outputField.text === "" && root.generatedUserConfigPath !== "")
outputField.text = root.generatedUserConfigPath
}
QtObject {
id: d
function doGenerate() {
var peers = initialPeersArea.text.split("\n").map(function(s) { return s.trim() }).filter(function(s) { return s.length > 0 })
root.generateRequested(
outputField.text.trim(),
peers,
netPortSpin.value,
blendPortSpin.value,
httpAddrField.text.trim(),
externalAddrField.text.trim(),
noPublicIpCheckBox.checked,
devnetRadio.checked ? 0 : 1,
customDeploymentField.text.trim(),
statePathField.text.trim())
}
}
spacing: Theme.spacing.medium
LogosText {
Layout.alignment: Qt.AlignLeft
font.bold: true
text: qsTr("Generate user config")
}
LogosText {
Layout.alignment: Qt.AlignLeft
Layout.topMargin: -Theme.spacing.small
text: qsTr("All fields are optional. Values are passed as args to generate config.")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
// Output path (defaults to generated path; user can change via text or folder browse)
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosTextField {
id: outputField
Layout.fillWidth: true
placeholderText: root.generatedUserConfigPath || qsTr("Output config path (e.g. node_config.yaml)")
}
LogosButton {
text: qsTr("Browse…")
onClicked: outputFolderDialog.open()
}
}
// Initial peers (multi-line)
LogosText {
Layout.alignment: Qt.AlignLeft
text: qsTr("Initial peers (one per line)")
font.pixelSize: Theme.typography.secondaryText
}
ScrollView {
Layout.fillWidth: true
Layout.preferredHeight: 60
clip: true
TextArea {
id: initialPeersArea
placeholderText: qsTr("Peer addresses, one per line")
font.pixelSize: Theme.typography.secondaryText
}
}
// Net port / Blend port
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.large
LogosText {
text: qsTr("Net port")
font.pixelSize: Theme.typography.secondaryText
}
SpinBox {
id: netPortSpin
from: 0
to: 65535
value: 0
Layout.preferredWidth: 100
editable: true
}
Item { Layout.fillWidth: true }
LogosText {
text: qsTr("Blend port")
font.pixelSize: Theme.typography.secondaryText
}
SpinBox {
id: blendPortSpin
from: 0
to: 65535
value: 0
Layout.preferredWidth: 100
editable: true
}
}
LogosTextField {
id: httpAddrField
Layout.fillWidth: true
placeholderText: qsTr("HTTP address (e.g. 0.0.0.0:8080)")
}
LogosTextField {
id: externalAddrField
Layout.fillWidth: true
placeholderText: qsTr("External address (e.g. public IP:port)")
}
CheckBox {
id: noPublicIpCheckBox
text: qsTr("No public IP check")
}
// Deployment
LogosText {
Layout.alignment: Qt.AlignLeft
text: qsTr("Deployment")
font.pixelSize: Theme.typography.secondaryText
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.medium
RadioButton {
id: devnetRadio
checked: true
text: qsTr("Devnet")
}
RadioButton {
id: customRadio
text: qsTr("Custom config")
}
LogosTextField {
id: customDeploymentField
visible: customRadio.checked
Layout.fillWidth: true
placeholderText: qsTr("Path to deployment config")
}
LogosButton {
visible: customRadio.checked
text: qsTr("Browse")
onClicked: deploymentConfigFileDialog.open()
}
}
LogosTextField {
id: statePathField
Layout.fillWidth: true
placeholderText: qsTr("State path")
}
LogosButton {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: 50
text: qsTr("Generate config")
onClicked: d.doGenerate()
}
LogosText {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
text: root.resultMessage
color: root.resultSuccess ? Theme.palette.success : Theme.palette.error
font.pixelSize: Theme.typography.secondaryText
wrapMode: Text.WordWrap
visible: root.resultMessage !== ""
}
FileDialog {
id: deploymentConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)", "All files (*)"]
currentFolder: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
onAccepted: customDeploymentField.text = selectedFile
}
FolderDialog {
id: outputFolderDialog
modality: Qt.NonModal
title: qsTr("Choose folder for config file")
onAccepted: {
var urlStr = selectedFolder.toString()
if (urlStr.indexOf("file://") === 0)
urlStr = urlStr.substring(7)
if (urlStr.length > 0)
outputField.text = urlStr + "/user_config.yaml"
}
}
}

View File

@ -0,0 +1,91 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import Logos.Theme
import Logos.Controls
ColumnLayout {
id: root
property string userConfigPath: ""
property string deploymentConfigPath: ""
signal userConfigPathSelected(string path)
signal deploymentConfigPathSelected(string path)
signal setPathToConfigsRequested()
spacing: Theme.spacing.medium
LogosText {
Layout.alignment: Qt.AlignLeft
text: qsTr("Select your config files, then continue to the node.")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
text: qsTr("User Config: ")
font.bold: true
}
LogosText {
Layout.fillWidth: true
text: root.userConfigPath || qsTr("No file selected")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
LogosButton {
text: qsTr("Browse")
onClicked: userConfigFileDialog.open()
}
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
text: qsTr("Deployment Config: ")
font.bold: true
}
LogosText {
Layout.fillWidth: true
text: root.deploymentConfigPath || qsTr("No file selected")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
LogosButton {
text: qsTr("Browse")
onClicked: deploymentConfigFileDialog.open()
}
}
LogosButton {
Layout.alignment: Qt.AlignHCenter
Layout.fillWidth: true
Layout.preferredHeight: 50
text: qsTr("Continue")
enabled: !!root.userConfigPath
onClicked: root.setPathToConfigsRequested()
}
FileDialog {
id: userConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)"]
onAccepted: root.userConfigPathSelected(selectedFile)
}
FileDialog {
id: deploymentConfigFileDialog
modality: Qt.NonModal
nameFilters: ["YAML files (*.yaml)"]
onAccepted: root.deploymentConfigPathSelected(selectedFile)
}
}

View File

@ -13,13 +13,13 @@ ColumnLayout {
required property color statusColor
required property string userConfig
required property string deploymentConfig
required property bool useGeneratedConfig
required property bool canStart
required property bool isRunning
signal startRequested()
signal stopRequested()
signal changeUserConfigRequested()
signal changeDeploymentConfigRequested()
signal changeConfigRequested()
spacing: Theme.spacing.large
@ -89,55 +89,56 @@ ColumnLayout {
anchors.margins: Theme.spacing.large
spacing: Theme.spacing.medium
// User Config Card
ConfigSelectionPanel {
id: userConfigContent
title: qsTr("User Config: ")
configPath: root.userConfig || qsTr("No file selected")
onChangeRequested: root.changeUserConfigRequested()
LogosText {
text: qsTr("Config")
font.bold: true
}
// Deployment Config Card
ConfigSelectionPanel {
id: deploymentConfigContent
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacing.small
LogosText {
text: qsTr("User Config: ")
font.bold: true
}
LogosText {
Layout.fillWidth: true
text: (root.userConfig || qsTr("No file selected")) +
(root.useGeneratedConfig ? " " + qsTr("(Generated)") : "")
font.pixelSize: Theme.typography.secondaryText
color: Theme.palette.textSecondary
wrapMode: Text.WordWrap
}
}
title: qsTr("Deployment Config: ")
configPath: root.deploymentConfig || qsTr("No file selected")
onChangeRequested: root.changeDeploymentConfigRequested()
RowLayout {
Layout.fillWidth: true
Layout.topMargin: -Theme.spacing.small
spacing: Theme.spacing.small
LogosText {
text: qsTr("Deployment Config: ")
font.bold: true
}
LogosText {
Layout.fillWidth: true
text: (root.useGeneratedConfig && root.deploymentConfig ? root.deploymentConfig :
root.useGeneratedConfig ? qsTr("Devnet (default)") :
(root.deploymentConfig || qsTr("No file selected")))
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: root.changeConfigRequested()
}
}
}
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

@ -183,7 +183,7 @@ ColumnLayout {
visible: comboControl.count > 0
}
contentItem: Item {
implicitWidth: comboControl.availableWidth
implicitWidth: 200
implicitHeight: 30
TextField {

View File

@ -1,4 +1,7 @@
module views
StatusConfigView 1.0 StatusConfigView.qml
LogsView 1.0 LogsView.qml
WalletView 1.0 WalletView.qml
WalletView 1.0 WalletView.qml
GenerateConfigView 1.0 GenerateConfigView.qml
ConfigChoiceView 1.0 ConfigChoiceView.qml
SetConfigPathView 1.0 SetConfigPathView.qml