From 42d60930100eb56a7da935ee1f896cf8413b9bec Mon Sep 17 00:00:00 2001 From: erhant Date: Thu, 18 Jun 2026 20:19:13 +0300 Subject: [PATCH] feat: copy buttons and add config editor --- README.md | 3 +- src/lez_explorer_ui.rep | 17 ++- src/lez_explorer_ui_backend.cpp | 134 +++++++++++++++++-- src/lez_explorer_ui_backend.h | 18 ++- src/qml/Main.qml | 4 + src/qml/components/NavigationBar.qml | 6 + src/qml/icons/settings.svg | 4 + src/qml/pages/HomePage.qml | 133 ++++++++----------- src/qml/pages/SettingsPage.qml | 191 +++++++++++++++++++++++++++ tests/ui-tests.mjs | 2 +- 10 files changed, 415 insertions(+), 97 deletions(-) create mode 100644 src/qml/icons/settings.svg create mode 100644 src/qml/pages/SettingsPage.qml diff --git a/README.md b/README.md index ce66e39..baba6d2 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ A QML block explorer for the **Logos Execution Zone** (LEZ), packaged as a `logo - Search by block id, block hash, transaction hash, or account id - Browser-style back/forward navigation - "Load more" pagination for older blocks +- Copy buttons on hashes / ids; in-app **Settings** JSON editor to configure & (re)start the indexer (no file paths; persists across launches) - Styled with the shared `Logos.Theme` / `Logos.Controls` design system ## Architecture @@ -34,7 +35,7 @@ nix run . # preview in logos-standalone-app (spawns the ui-host) nix build .#lgx # bundle as a .lgx for logos-basecamp ``` -Running against live data needs the indexer reachable; enter its `indexer_config.json` path in the explorer's config field (leave blank if the indexer is already running). Load the explorer and indexer `.lgx` bundles together in logos-basecamp for the full experience. +Running against live data: open **Settings** (gear icon, top-right), edit the indexer config JSON (pre-filled with a working template) and press **Save & Start** — the explorer persists the config and (re)starts the indexer from it, so no file paths are involved, and it auto-restarts from the saved config on the next launch. Load the explorer and indexer `.lgx` bundles together in logos-basecamp for the full experience. ## Testing diff --git a/src/lez_explorer_ui.rep b/src/lez_explorer_ui.rep index 79bc24b..dead461 100644 --- a/src/lez_explorer_ui.rep +++ b/src/lez_explorer_ui.rep @@ -15,15 +15,20 @@ class LezExplorerUi PROP(QVariantList recentBlocks READONLY) // Tip block id (from getLastFinalizedBlockId). PROP(qlonglong chainHeight=0 READONLY) - // Absolute path of the indexer config currently in use ("" if none set). - PROP(QString indexerConfig READONLY) // "Connecting" / "Connected" / "Error" — drives the health indicator. PROP(QString connectionStatus READONLY) + // The indexer config JSON currently in effect ("" until one is saved). The + // Settings editor seeds from this (falling back to defaultConfig). + PROP(QString configText READONLY) + // Built-in starter config template (seeded by the backend) for the Settings + // editor's "Reset to default". + PROP(QString defaultConfig READONLY) - // Point the indexer at a config and (re)start ingestion, then refresh the - // feed. port is the indexer RPC port. Returns true on success. A blank - // configPath skips start_indexer (the indexer is assumed already running). - SLOT(bool applyIndexerConfig(QString configPath, QString port)) + // Persist `json` as the indexer config and (re)start ingestion from it, then + // refresh the feed. `port` is the indexer RPC port (blank keeps the current + // one). The backend writes the JSON to a file itself — the UI never deals + // with paths. Returns true on success. + SLOT(bool applyConfigJson(QString json, QString port)) // Reload the latest page into recentBlocks and re-baseline the poller. SLOT(void refreshBlocks()) // Append the next older page of blocks to recentBlocks. diff --git a/src/lez_explorer_ui_backend.cpp b/src/lez_explorer_ui_backend.cpp index 929e2ac..cb02799 100644 --- a/src/lez_explorer_ui_backend.cpp +++ b/src/lez_explorer_ui_backend.cpp @@ -4,11 +4,16 @@ #include #include +#include +#include +#include #include #include #include +#include #include #include +#include #include #include @@ -23,6 +28,31 @@ constexpr int kPollIntervalMs = 2000; // missing block one by one (matches the former LpIndexerService threshold). constexpr quint64 kGapRebuildThreshold = 8; +// Starter config shown in the Settings editor (and its "Reset to default"). +// A working local-dev indexer config; the user edits the fields (bedrock addr, +// channel id, initial accounts/commitments, signing key, ...) and saves. +const char* const kDefaultConfig = R"JSON({ + "home": ".", + "consensus_info_polling_interval": "1s", + "bedrock_config": { + "addr": "http://localhost:8080", + "backoff": { + "start_delay": "100ms", + "max_retries": 5 + } + }, + "channel_id": "0101010101010101010101010101010101010101010101010101010101010101", + "initial_accounts": [ + { "account_id": "CbgR6tj5kWx5oziiFptM7jMvrQeYY3Mzaao6ciuhSr2r", "balance": 10000 }, + { "account_id": "2RHZhw9h534Zr3eq2RGhQete2Hh667foECzXPmSkGni2", "balance": 20000 } + ], + "initial_commitments": [], + "signing_key": [ + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37, 37 + ] +})JSON"; + // 64/128-bit values arrive as decimal strings (to dodge JSON double precision // loss), but be lenient and also accept a JSON number. quint64 jsonU64(const QJsonValue& value) @@ -186,7 +216,10 @@ QVariantMap txJsonToMap(const QString& json) } // namespace -LezExplorerUiBackend::LezExplorerUiBackend() = default; +LezExplorerUiBackend::LezExplorerUiBackend() +{ + setDefaultConfig(QString::fromUtf8(kDefaultConfig)); +} LezExplorerUiBackend::~LezExplorerUiBackend() = default; @@ -194,6 +227,13 @@ void LezExplorerUiBackend::onContextReady() { setConnectionStatus(QStringLiteral("Connecting")); + // Reload + auto-start the previously-saved config, if any (set-once UX). + const QString saved = readConfigFile(); + if (!saved.isEmpty()) { + setConfigText(saved); + startIndexerFromFile(); + } + // The indexer pushes no events — poll the tip for live head updates. m_pollTimer = new QTimer(this); m_pollTimer->setInterval(kPollIntervalMs); @@ -204,29 +244,39 @@ void LezExplorerUiBackend::onContextReady() pollTip(); } -bool LezExplorerUiBackend::applyIndexerConfig(QString configPath, QString port) +bool LezExplorerUiBackend::applyConfigJson(QString json, QString port) { - const QString trimmedConfig = configPath.trimmed(); const QString trimmedPort = port.trimmed(); if (!trimmedPort.isEmpty()) { m_port = trimmedPort; } - setIndexerConfig(trimmedConfig); - bool started = true; - if (!trimmedConfig.isEmpty()) { - const qlonglong code = modules().lez_indexer_module.start_indexer(trimmedConfig, m_port); - started = (code == 0); - if (!started) { - emit error(QStringLiteral("start_indexer failed (code %1) for %2").arg(code).arg(trimmedConfig)); - } + const QString trimmed = json.trimmed(); + if (trimmed.isEmpty()) { + emit error(QStringLiteral("Config is empty.")); + return false; } + // Reject invalid JSON up front so we never start the indexer from garbage. + QJsonParseError parseError; + QJsonDocument::fromJson(trimmed.toUtf8(), &parseError); + if (parseError.error != QJsonParseError::NoError) { + emit error(QStringLiteral("Invalid JSON: %1").arg(parseError.errorString())); + return false; + } + + if (!writeConfigFile(json)) { + emit error(QStringLiteral("Could not write config to %1").arg(configFilePath())); + return false; + } + setConfigText(json); + + const bool ok = startIndexerFromFile(); // Re-baseline the poller; a (re)started indexer may ingest afresh. m_lastPolledTip = 0; m_polledOnce = false; refreshBlocks(); - return started; + return ok; } void LezExplorerUiBackend::refreshBlocks() @@ -436,3 +486,63 @@ QVariantList LezExplorerUiBackend::parseBlockArrayJson(const QString& json) cons } return blocks; } + +namespace { +// The writable path where the edited config JSON is persisted and the indexer +// is started from (so the UI never deals with paths). Same logic, shared by the +// const and non-const accessors. +QString resolveConfigFilePath() +{ + QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (dir.isEmpty()) { + dir = QDir::tempPath(); + } + return QDir(dir).filePath(QStringLiteral("indexer_config.json")); +} +} // namespace + +QString LezExplorerUiBackend::configFilePath() +{ + if (m_configFilePath.isEmpty()) { + m_configFilePath = resolveConfigFilePath(); + } + return m_configFilePath; +} + +bool LezExplorerUiBackend::writeConfigFile(const QString& json) +{ + const QString path = configFilePath(); + const QDir dir = QFileInfo(path).dir(); + if (!dir.exists() && !dir.mkpath(QStringLiteral("."))) { + return false; + } + QFile file(path); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { + return false; + } + const QByteArray bytes = json.toUtf8(); + const bool ok = file.write(bytes) == bytes.size(); + file.close(); + return ok; +} + +QString LezExplorerUiBackend::readConfigFile() const +{ + QFile file(m_configFilePath.isEmpty() ? resolveConfigFilePath() : m_configFilePath); + if (!file.exists() || !file.open(QIODevice::ReadOnly | QIODevice::Text)) { + return {}; + } + const QString content = QString::fromUtf8(file.readAll()); + file.close(); + return content; +} + +bool LezExplorerUiBackend::startIndexerFromFile() +{ + const qlonglong code = modules().lez_indexer_module.start_indexer(configFilePath(), m_port); + if (code != 0) { + emit error(QStringLiteral("start_indexer failed (code %1)").arg(code)); + return false; + } + return true; +} diff --git a/src/lez_explorer_ui_backend.h b/src/lez_explorer_ui_backend.h index 8bf49ec..d056294 100644 --- a/src/lez_explorer_ui_backend.h +++ b/src/lez_explorer_ui_backend.h @@ -23,6 +23,11 @@ class QTimer; * that JSON into QVariantMap / QVariantList (schema preserved from the former * `LpIndexerService`) for the QML view, and runs a 2 s poll on the chain tip to * keep `recentBlocks` / `chainHeight` live (the indexer emits no events). + * + * Config is edited in-app (Settings page) as JSON, not as a path: the indexer's + * `start_indexer` takes a file path, so this class persists the edited JSON to a + * writable file and starts the indexer from there. The last-saved config is + * reloaded and auto-started on the next launch. */ class LezExplorerUiBackend : public LezExplorerUiSimpleSource, public LogosUiPluginContext { public: @@ -30,11 +35,11 @@ public: ~LezExplorerUiBackend() override; // Fires when ui-host wires the plugin's LogosAPI: modules() is live, so we - // start the poll here. + // load any saved config, start it, and start the poll here. void onContextReady() override; // .rep SLOTs. - bool applyIndexerConfig(QString configPath, QString port) override; + bool applyConfigJson(QString json, QString port) override; void refreshBlocks() override; void loadMoreBlocks() override; QVariantMap getBlockById(QString id) override; @@ -52,7 +57,16 @@ private: // Parse a getBlocks(...) JSON array payload into a list of block maps. QVariantList parseBlockArrayJson(const QString& json) const; + // Writable path where the edited config JSON is persisted, and where the + // indexer is started from. Computed once (lazily). + QString configFilePath(); + bool writeConfigFile(const QString& json); + QString readConfigFile() const; + // start_indexer from configFilePath(); returns true on success. + bool startIndexerFromFile(); + QString m_port = QStringLiteral("8779"); + QString m_configFilePath; QVariantList m_recentBlocks; quint64 m_newestLoadedId = 0; // highest block id currently in m_recentBlocks quint64 m_oldestLoadedId = 0; // lowest block id currently in m_recentBlocks diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 11c5800..7001b52 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -46,6 +46,7 @@ Rectangle { function navigateBlock(id) { root.navigateTo({ type: "block", param: id }); } function navigateTx(hash) { root.navigateTo({ type: "tx", param: hash }); } function navigateAccount(id) { root.navigateTo({ type: "account", param: id }); } + function navigateSettings() { root.navigateTo({ type: "settings" }); } function showSearchResults(results, query) { root.navigateTo({ type: "search", param: results, query: query }); } @@ -72,6 +73,8 @@ Rectangle { url = "pages/SearchResultsPage.qml"; props.results = d.param; props.query = d.query || ""; + } else if (d.type === "settings") { + url = "pages/SettingsPage.qml"; } pageLoader.setSource(Qt.resolvedUrl(url), props); navBar.canGoBack = root.histIndex > 0; @@ -101,6 +104,7 @@ Rectangle { onBackClicked: root.goBack() onForwardClicked: root.goForward() onHomeClicked: root.goHome() + onSettingsClicked: root.navigateSettings() } SearchBar { diff --git a/src/qml/components/NavigationBar.qml b/src/qml/components/NavigationBar.qml index 5f64c11..d587560 100644 --- a/src/qml/components/NavigationBar.qml +++ b/src/qml/components/NavigationBar.qml @@ -12,6 +12,7 @@ RowLayout { signal backClicked() signal forwardClicked() signal homeClicked() + signal settingsClicked() spacing: Theme.spacing.small @@ -40,4 +41,9 @@ RowLayout { } Item { Layout.fillWidth: true } + + IconButton { + source: Qt.resolvedUrl("../icons/settings.svg") + onClicked: root.settingsClicked() + } } diff --git a/src/qml/icons/settings.svg b/src/qml/icons/settings.svg new file mode 100644 index 0000000..12516d8 --- /dev/null +++ b/src/qml/icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/qml/pages/HomePage.qml b/src/qml/pages/HomePage.qml index fa39ccb..eeec57e 100644 --- a/src/qml/pages/HomePage.qml +++ b/src/qml/pages/HomePage.qml @@ -11,70 +11,16 @@ Item { // Injected by Main: the controller (navigation + backend access). property var explorer readonly property var backend: explorer ? explorer.backend : null + readonly property bool connected: backend && backend.connectionStatus === "Connected" ColumnLayout { anchors.fill: parent spacing: Theme.spacing.medium - // Indexer config + health. + // Health bar. Card { Layout.fillWidth: true - RowLayout { - Layout.fillWidth: true - spacing: Theme.spacing.medium - - LogosText { - text: "Indexer config" - color: Theme.palette.textSecondary - font.pixelSize: Theme.typography.secondaryText - Layout.alignment: Qt.AlignVCenter - } - - Rectangle { - Layout.fillWidth: true - Layout.preferredHeight: 38 - radius: Theme.spacing.radiusMedium - color: Theme.palette.backgroundSecondary - border.width: 1 - border.color: configField.activeFocus ? Theme.palette.borderInteractive - : Theme.palette.borderSecondary - - TextField { - id: configField - anchors.fill: parent - anchors.leftMargin: Theme.spacing.medium - anchors.rightMargin: Theme.spacing.medium - verticalAlignment: TextInput.AlignVCenter - text: page.backend ? page.backend.indexerConfig : "" - placeholderText: "absolute path to indexer_config.json (blank = already running)" - color: Theme.palette.text - placeholderTextColor: Theme.palette.textMuted - font.pixelSize: Theme.typography.secondaryText - background: Item {} - onAccepted: page.applyConfig() - } - } - - Rectangle { - Layout.preferredHeight: 38 - Layout.preferredWidth: 84 - radius: Theme.spacing.radiusMedium - color: applyHover.hovered ? Qt.lighter(Theme.palette.primary, 1.1) : Theme.palette.primary - - HoverHandler { id: applyHover } - TapHandler { onTapped: page.applyConfig() } - - LogosText { - anchors.centerIn: parent - text: "Apply" - color: Theme.palette.background - font.pixelSize: Theme.typography.secondaryText - font.weight: Theme.typography.weightBold - } - } - } - RowLayout { Layout.fillWidth: true spacing: Theme.spacing.small @@ -84,9 +30,7 @@ Item { height: 9 radius: 4.5 Layout.alignment: Qt.AlignVCenter - color: page.backend && page.backend.connectionStatus === "Connected" - ? Theme.palette.success - : Theme.palette.warning + color: page.connected ? Theme.palette.success : Theme.palette.warning } LogosText { text: { @@ -97,6 +41,28 @@ Item { } color: Theme.palette.textMuted font.pixelSize: Theme.typography.secondaryText + Layout.alignment: Qt.AlignVCenter + } + + Item { Layout.fillWidth: true } + + Rectangle { + Layout.preferredHeight: 34 + Layout.preferredWidth: 100 + radius: Theme.spacing.radiusMedium + color: settingsHover.hovered ? Theme.palette.backgroundSecondary : Theme.palette.backgroundElevated + border.width: 1 + border.color: Theme.palette.borderSecondary + + HoverHandler { id: settingsHover } + TapHandler { onTapped: if (page.explorer) page.explorer.navigateSettings() } + + LogosText { + anchors.centerIn: parent + text: "Settings" + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + } } } } @@ -147,26 +113,43 @@ Item { } // Empty state. - LogosText { + ColumnLayout { anchors.centerIn: parent visible: blockList.count === 0 width: parent.width * 0.8 - horizontalAlignment: Text.AlignHCenter - wrapMode: Text.WordWrap - text: page.backend && page.backend.connectionStatus === "Connected" - ? "No blocks indexed yet." - : "Waiting for the indexer… set the config path above if it isn't running." - color: Theme.palette.textMuted - font.pixelSize: Theme.typography.secondaryText + spacing: Theme.spacing.medium + + LogosText { + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + text: page.connected + ? "No blocks indexed yet." + : "No indexer running yet. Open Settings to configure and start it." + color: Theme.palette.textMuted + font.pixelSize: Theme.typography.secondaryText + } + + Rectangle { + visible: !page.connected + Layout.alignment: Qt.AlignHCenter + implicitHeight: 38 + implicitWidth: 160 + radius: Theme.spacing.radiusMedium + color: openHover.hovered ? Qt.lighter(Theme.palette.primary, 1.1) : Theme.palette.primary + + HoverHandler { id: openHover } + TapHandler { onTapped: if (page.explorer) page.explorer.navigateSettings() } + + LogosText { + anchors.centerIn: parent + text: "Open Settings" + color: Theme.palette.background + font.pixelSize: Theme.typography.secondaryText + font.weight: Theme.typography.weightBold + } + } } } } - - function applyConfig() { - if (!page.backend) - return; - // Empty port → backend keeps its default (8779). - logos.watch(page.backend.applyIndexerConfig(configField.text, ""), - function (ok) {}, function (err) {}); - } } diff --git a/src/qml/pages/SettingsPage.qml b/src/qml/pages/SettingsPage.qml new file mode 100644 index 0000000..2c54412 --- /dev/null +++ b/src/qml/pages/SettingsPage.qml @@ -0,0 +1,191 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Logos.Controls +import Logos.Theme +import "../components" + +Item { + id: page + + property var explorer + readonly property var backend: explorer ? explorer.backend : null + property bool statusIsError: false + + Component.onCompleted: { + // Seed the editor with the saved config, falling back to the template. + if (page.backend) { + var current = page.backend.configText; + editor.text = (current && current.length > 0) ? current : page.backend.defaultConfig; + portField.text = "8779"; + } + } + + // Surface backend-side failures (write/start) in the status line. + Connections { + target: page.backend + ignoreUnknownSignals: true + function onError(message) { + page.setStatus(message, true); + } + } + + ColumnLayout { + anchors.fill: parent + spacing: Theme.spacing.medium + + SectionHeader { title: "Indexer Settings" } + + LogosText { + Layout.fillWidth: true + wrapMode: Text.WordWrap + text: "Edit the indexer configuration below and press Save. The config is " + + "persisted and the indexer is (re)started from it — no file paths needed. " + + "It is reloaded automatically the next time you open the explorer." + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + } + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.medium + + LogosText { + text: "RPC port" + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + Layout.alignment: Qt.AlignVCenter + } + Rectangle { + Layout.preferredWidth: 120 + Layout.preferredHeight: 36 + radius: Theme.spacing.radiusMedium + color: Theme.palette.backgroundSecondary + border.width: 1 + border.color: portField.activeFocus ? Theme.palette.borderInteractive + : Theme.palette.borderSecondary + TextField { + id: portField + anchors.fill: parent + anchors.leftMargin: Theme.spacing.medium + anchors.rightMargin: Theme.spacing.medium + verticalAlignment: TextInput.AlignVCenter + text: "8779" + inputMethodHints: Qt.ImhDigitsOnly + color: Theme.palette.text + font.pixelSize: Theme.typography.secondaryText + background: Item {} + } + } + Item { Layout.fillWidth: true } + } + + // JSON editor. The TextArea is anchored to fill its container directly + // (NOT wrapped in a ScrollView — there it doesn't inherit the viewport + // size and collapses to an invisible implicit size). It scrolls its + // viewport to follow the cursor; that's enough for a config-sized doc. + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: Theme.spacing.radiusLarge + color: Theme.palette.backgroundElevated + border.width: 1 + border.color: editor.activeFocus ? Theme.palette.borderInteractive + : Theme.palette.borderSecondary + clip: true + + TextArea { + id: editor + anchors.fill: parent + anchors.margins: Theme.spacing.medium + font.family: "Menlo, Monaco, Courier New, monospace" + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.text + wrapMode: TextArea.Wrap + background: Item {} + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Theme.spacing.medium + + LogosText { + id: statusLabel + Layout.fillWidth: true + wrapMode: Text.WordWrap + color: page.statusIsError ? Theme.palette.error : Theme.palette.success + font.pixelSize: Theme.typography.secondaryText + } + + // Reset to the built-in template. + Rectangle { + Layout.preferredHeight: 38 + Layout.preferredWidth: 150 + radius: Theme.spacing.radiusMedium + color: resetHover.hovered ? Theme.palette.backgroundSecondary : Theme.palette.backgroundElevated + border.width: 1 + border.color: Theme.palette.borderSecondary + + HoverHandler { id: resetHover } + TapHandler { onTapped: if (page.backend) { editor.text = page.backend.defaultConfig; page.setStatus("", false); } } + + LogosText { + anchors.centerIn: parent + text: "Reset to default" + color: Theme.palette.textSecondary + font.pixelSize: Theme.typography.secondaryText + } + } + + // Save + (re)start. + Rectangle { + Layout.preferredHeight: 38 + Layout.preferredWidth: 150 + radius: Theme.spacing.radiusMedium + color: saveHover.hovered ? Qt.lighter(Theme.palette.primary, 1.1) : Theme.palette.primary + + HoverHandler { id: saveHover } + TapHandler { onTapped: page.save() } + + LogosText { + anchors.centerIn: parent + text: "Save & Start" + color: Theme.palette.background + font.pixelSize: Theme.typography.secondaryText + font.weight: Theme.typography.weightBold + } + } + } + } + + function setStatus(message, isError) { + page.statusIsError = isError; + statusLabel.text = message; + } + + function save() { + if (!page.backend) + return; + // Validate locally first for an immediate, precise error. + try { + JSON.parse(editor.text); + } catch (e) { + page.setStatus("Invalid JSON: " + e, true); + return; + } + page.setStatus("Saving…", false); + logos.watch(page.backend.applyConfigJson(editor.text, portField.text), + function (ok) { + if (ok) { + page.setStatus("Saved. Indexer starting…", false); + page.explorer.goHome(); + } else { + page.setStatus("Save failed — check the config and try again.", true); + } + }, + function (err) { + page.setStatus("Error: " + err, true); + }); + } +} diff --git a/tests/ui-tests.mjs b/tests/ui-tests.mjs index 4147571..e281c0f 100644 --- a/tests/ui-tests.mjs +++ b/tests/ui-tests.mjs @@ -20,7 +20,7 @@ test("lez_explorer_ui: loads the view", async (app) => { test("lez_explorer_ui: renders the home page", async (app) => { await app.waitFor( - async () => { await app.expectTexts(["Recent Blocks", "Indexer config"]); }, + async () => { await app.expectTexts(["Recent Blocks", "Settings"]); }, { timeout: 15000, interval: 500, description: "the home page to render" } ); });