mirror of
https://github.com/logos-blockchain/lez-explorer-ui.git
synced 2026-07-30 03:13:25 +00:00
Merge pull request #6 from logos-blockchain/erhant/migr-to-module-builder-update-LEZ
feat!: adopt port-less indexer module; gate search on connection
This commit is contained in:
commit
719174f9e8
2
.gitignore
vendored
2
.gitignore
vendored
@ -5,4 +5,4 @@ build
|
||||
lez-explorer-ui-*-lgx-*
|
||||
|
||||
# ignore rocksdb (may appear here during indexer tests)
|
||||
rocksdb
|
||||
rocksdb*
|
||||
10795
flake.lock
generated
10795
flake.lock
generated
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@
|
||||
# NOTE: git+https (not github:) because the branch name contains a slash —
|
||||
# the github: fetcher mis-routes slashed refs through the commits API (404).
|
||||
# TODO: repoint to the merge target once this branch lands / goes public.
|
||||
lez_indexer_module.url = "git+https://github.com/logos-blockchain/lez-indexer-module?ref=erhant/migr-to-logos-module-builder";
|
||||
lez_indexer_module.url = "git+https://github.com/logos-blockchain/lez-indexer-module?ref=erhant/migr-to-logos-module-builder-update-LEZ";
|
||||
};
|
||||
|
||||
outputs =
|
||||
|
||||
@ -13,10 +13,15 @@ class LezExplorerUi
|
||||
// Recent-blocks feed shown on the home page (newest first). Backend-owned:
|
||||
// the 2 s poll prepends new heads and "Load more" appends older pages.
|
||||
PROP(QVariantList recentBlocks READONLY)
|
||||
// Tip block id (from getLastFinalizedBlockId).
|
||||
// Tip block id (from the indexer status' indexedBlockId).
|
||||
PROP(qlonglong chainHeight=0 READONLY)
|
||||
// "Connecting" / "Connected" / "Error" — drives the health indicator.
|
||||
PROP(QString connectionStatus READONLY)
|
||||
// Ingestion status for the first-launch/sync banner, refreshed by the tip
|
||||
// poll. Map of { state: "starting"|"syncing"|"caught_up"|"error",
|
||||
// blockId: qulonglong, error: QString }. Lets the view show the current
|
||||
// phase (or the error) so "catching up" reads differently from "failed".
|
||||
PROP(QVariantMap syncStatus 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)
|
||||
@ -25,10 +30,9 @@ class LezExplorerUi
|
||||
PROP(QString defaultConfig READONLY)
|
||||
|
||||
// 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))
|
||||
// refresh the feed. The backend writes the JSON to a file itself — the UI
|
||||
// never deals with paths. Returns true on success.
|
||||
SLOT(bool applyConfigJson(QString json))
|
||||
// Reload the latest page into recentBlocks and re-baseline the poller.
|
||||
SLOT(void refreshBlocks())
|
||||
// Append the next older page of blocks to recentBlocks.
|
||||
|
||||
@ -32,7 +32,6 @@ constexpr quint64 kGapRebuildThreshold = 8;
|
||||
// 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",
|
||||
@ -244,13 +243,8 @@ void LezExplorerUiBackend::onContextReady()
|
||||
pollTip();
|
||||
}
|
||||
|
||||
bool LezExplorerUiBackend::applyConfigJson(QString json, QString port)
|
||||
bool LezExplorerUiBackend::applyConfigJson(QString json)
|
||||
{
|
||||
const QString trimmedPort = port.trimmed();
|
||||
if (!trimmedPort.isEmpty()) {
|
||||
m_port = trimmedPort;
|
||||
}
|
||||
|
||||
const QString trimmed = json.trimmed();
|
||||
if (trimmed.isEmpty()) {
|
||||
emit error(QStringLiteral("Config is empty."));
|
||||
@ -411,17 +405,54 @@ QVariantMap LezExplorerUiBackend::search(QString query)
|
||||
return results;
|
||||
}
|
||||
|
||||
quint64 LezExplorerUiBackend::applySyncStatus(const QString& json)
|
||||
{
|
||||
// Empty JSON means the indexer isn't running (not configured / not started);
|
||||
// a real status always carries one of the core's states.
|
||||
QString state = QStringLiteral("stopped");
|
||||
quint64 blockId = 0;
|
||||
QString errorMsg;
|
||||
|
||||
if (!json.isEmpty()) {
|
||||
QJsonParseError parseError;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json.toUtf8(), &parseError);
|
||||
if (parseError.error == QJsonParseError::NoError && doc.isObject()) {
|
||||
const QJsonObject obj = doc.object();
|
||||
state = obj.value(QStringLiteral("state")).toString(QStringLiteral("starting"));
|
||||
blockId = static_cast<quint64>(obj.value(QStringLiteral("indexedBlockId")).toDouble(0));
|
||||
errorMsg = obj.value(QStringLiteral("lastError")).toString();
|
||||
}
|
||||
}
|
||||
|
||||
QVariantMap status;
|
||||
status.insert(QStringLiteral("state"), state);
|
||||
status.insert(QStringLiteral("blockId"), static_cast<qulonglong>(blockId));
|
||||
status.insert(QStringLiteral("error"), errorMsg);
|
||||
setSyncStatus(status);
|
||||
|
||||
// Keep the legacy health indicator in step with the richer status.
|
||||
if (state == QLatin1String("error")) {
|
||||
setConnectionStatus(QStringLiteral("Error"));
|
||||
} else if (state == QLatin1String("syncing") || state == QLatin1String("caught_up")) {
|
||||
setConnectionStatus(QStringLiteral("Connected"));
|
||||
} else {
|
||||
setConnectionStatus(QStringLiteral("Connecting"));
|
||||
}
|
||||
|
||||
return blockId;
|
||||
}
|
||||
|
||||
void LezExplorerUiBackend::pollTip()
|
||||
{
|
||||
const QString tipStr = modules().lez_indexer_module.getLastFinalizedBlockId();
|
||||
bool ok = false;
|
||||
const quint64 tip = tipStr.toULongLong(&ok);
|
||||
if (!ok || tip == 0) {
|
||||
setConnectionStatus(QStringLiteral("Connecting"));
|
||||
// One status call drives both the sync banner and the feed: indexedBlockId
|
||||
// is the L2 tip we page from.
|
||||
const quint64 tip = applySyncStatus(modules().lez_indexer_module.getStatus());
|
||||
if (tip == 0) {
|
||||
// No finalized block yet (starting / syncing from genesis, or stopped);
|
||||
// syncStatus already conveys which, so just wait for the next poll.
|
||||
return;
|
||||
}
|
||||
|
||||
setConnectionStatus(QStringLiteral("Connected"));
|
||||
setChainHeight(static_cast<qlonglong>(tip));
|
||||
|
||||
// First successful poll: fill the feed and baseline without double-loading.
|
||||
@ -493,7 +524,7 @@ namespace {
|
||||
// const and non-const accessors.
|
||||
QString resolveConfigFilePath()
|
||||
{
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation);
|
||||
if (dir.isEmpty()) {
|
||||
dir = QDir::tempPath();
|
||||
}
|
||||
@ -539,7 +570,7 @@ QString LezExplorerUiBackend::readConfigFile() const
|
||||
|
||||
bool LezExplorerUiBackend::startIndexerFromFile()
|
||||
{
|
||||
const qlonglong code = modules().lez_indexer_module.start_indexer(configFilePath(), m_port);
|
||||
const qlonglong code = modules().lez_indexer_module.start_indexer(configFilePath());
|
||||
if (code != 0) {
|
||||
emit error(QStringLiteral("start_indexer failed (code %1)").arg(code));
|
||||
return false;
|
||||
|
||||
@ -39,7 +39,7 @@ public:
|
||||
void onContextReady() override;
|
||||
|
||||
// .rep SLOTs.
|
||||
bool applyConfigJson(QString json, QString port) override;
|
||||
bool applyConfigJson(QString json) override;
|
||||
void refreshBlocks() override;
|
||||
void loadMoreBlocks() override;
|
||||
QVariantMap getBlockById(QString id) override;
|
||||
@ -52,6 +52,9 @@ public:
|
||||
private:
|
||||
// Tip poll: detect new heads and append/rebuild the recent-blocks feed.
|
||||
void pollTip();
|
||||
// Parse the indexer's status JSON, publish the `syncStatus` PROP + the
|
||||
// legacy `connectionStatus`, and return the indexed tip (0 if none yet).
|
||||
quint64 applySyncStatus(const QString& json);
|
||||
// Fetch a single block as a normalized map ({} if missing).
|
||||
QVariantMap fetchBlock(quint64 blockId);
|
||||
// Parse a getBlocks(...) JSON array payload into a list of block maps.
|
||||
@ -65,7 +68,6 @@ private:
|
||||
// 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
|
||||
|
||||
@ -109,6 +109,8 @@ Rectangle {
|
||||
|
||||
SearchBar {
|
||||
Layout.fillWidth: true
|
||||
// Only searchable once the indexer is actually answering.
|
||||
enabled: root.backend && root.backend.connectionStatus === "Connected"
|
||||
onSearchRequested: function (query) {
|
||||
if (!root.backend || query.trim().length === 0)
|
||||
return;
|
||||
|
||||
@ -5,12 +5,16 @@ import Logos.Controls
|
||||
import Logos.Theme
|
||||
|
||||
// Search input + button. Emits searchRequested(query) on Enter or button click.
|
||||
// Bind the built-in `enabled` (e.g. to the indexer connection state) to block
|
||||
// searching when there's nothing to query; it dims and goes non-interactive.
|
||||
RowLayout {
|
||||
id: root
|
||||
|
||||
signal searchRequested(string query)
|
||||
|
||||
spacing: Theme.spacing.small
|
||||
opacity: enabled ? 1.0 : 0.5
|
||||
Behavior on opacity { NumberAnimation { duration: 120 } }
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
@ -40,7 +44,9 @@ RowLayout {
|
||||
id: field
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
placeholderText: "Search by block id / block hash / tx hash / account id…"
|
||||
placeholderText: root.enabled
|
||||
? "Search by block id / block hash / tx hash / account id…"
|
||||
: "Connect an indexer to search…"
|
||||
color: Theme.palette.text
|
||||
placeholderTextColor: Theme.palette.textMuted
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
|
||||
@ -11,13 +11,47 @@ 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"
|
||||
|
||||
// Ingestion status (from backend.syncStatus) drives the health/sync banner.
|
||||
readonly property var sync: backend ? backend.syncStatus : null
|
||||
readonly property string syncState: (sync && sync.state) ? sync.state : "starting"
|
||||
readonly property string syncError: (sync && sync.error) ? sync.error : ""
|
||||
|
||||
// Human-readable line for the banner: the current phase (or the error), so
|
||||
// the user can tell catching-up from a real failure on first launch.
|
||||
function statusLine() {
|
||||
if (!backend)
|
||||
return "";
|
||||
var height = backend.chainHeight > 0 ? backend.chainHeight : "—";
|
||||
if (page.syncState === "error")
|
||||
return page.syncError !== "" ? page.syncError : "Indexer error";
|
||||
if (page.syncState === "caught_up")
|
||||
return "Up to date · block " + height;
|
||||
if (page.syncState === "syncing")
|
||||
return "Syncing… · block " + height;
|
||||
if (page.syncState === "stopped")
|
||||
return "Indexer not running";
|
||||
return "Starting indexer…";
|
||||
}
|
||||
|
||||
// Message for the empty block list, tailored to the current phase. Settings
|
||||
// lives only in the top-right gear, so we point there rather than add a button.
|
||||
function emptyStateText() {
|
||||
if (page.syncState === "error")
|
||||
return (page.syncError !== "" ? "Indexer error: " + page.syncError : "Indexer error.")
|
||||
+ "\nOpen Settings to reconfigure and restart.";
|
||||
if (page.syncState === "syncing" || page.syncState === "starting")
|
||||
return "Indexer is syncing, please wait…";
|
||||
if (page.syncState === "caught_up")
|
||||
return "No blocks indexed yet.";
|
||||
return "No indexer running.\nOpen Settings to configure and start it.";
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Health bar.
|
||||
// Health / sync bar.
|
||||
Card {
|
||||
Layout.fillWidth: true
|
||||
|
||||
@ -30,39 +64,17 @@ Item {
|
||||
height: 9
|
||||
radius: 4.5
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
color: page.connected ? Theme.palette.success : Theme.palette.warning
|
||||
color: page.syncState === "error" ? Theme.palette.error
|
||||
: page.syncState === "caught_up" ? Theme.palette.success
|
||||
: Theme.palette.warning
|
||||
}
|
||||
LogosText {
|
||||
text: {
|
||||
if (!page.backend)
|
||||
return "";
|
||||
var height = page.backend.chainHeight > 0 ? page.backend.chainHeight : "—";
|
||||
return "Chain height: " + height + " · " + page.backend.connectionStatus;
|
||||
}
|
||||
color: Theme.palette.textMuted
|
||||
text: page.statusLine()
|
||||
color: page.syncState === "error" ? Theme.palette.error : 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
|
||||
}
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -112,43 +124,16 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state.
|
||||
ColumnLayout {
|
||||
// Empty state — phase-aware message; Settings lives in the top-right gear.
|
||||
LogosText {
|
||||
anchors.centerIn: parent
|
||||
visible: blockList.count === 0
|
||||
width: parent.width * 0.8
|
||||
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
|
||||
}
|
||||
}
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
text: page.emptyStateText()
|
||||
color: Theme.palette.textMuted
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,9 +140,7 @@ Item {
|
||||
return;
|
||||
}
|
||||
page.setStatus("Saving…", false);
|
||||
// Port is an internal detail of the indexer's own RPC listener; the
|
||||
// explorer reads over the Logos protocol, so we pass "" (keep default).
|
||||
logos.watch(page.backend.applyConfigJson(editor.text, ""),
|
||||
logos.watch(page.backend.applyConfigJson(editor.text),
|
||||
function (ok) {
|
||||
if (ok) {
|
||||
page.setStatus("Saved. Indexer starting…", false);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user