htmx datafusion graphs via echarts3 - no custom scripts, only alpinejs

This commit is contained in:
Priec
2026-07-16 00:51:13 +02:00
parent f2e8db471c
commit 52d1064071
3 changed files with 20 additions and 40 deletions

View File

@@ -2,7 +2,7 @@
A small Rust web server that logs in through `AuthService.Login`, forwards SQL
queries to `AnalyticsService.ExecuteAnalyticsQuery`, and displays the streamed
result with HTMX and ECharts.
result with HTMX, Alpine.js, and ECharts.
## Run
@@ -33,5 +33,6 @@ create starter queries. Its **LLM schema context** section generates a complete
text summary that can be copied into an LLM prompt to request a valid analytics
SQL query.
HTMX and ECharts are loaded from jsDelivr, so the browser needs network access
when opening the page.
HTMX, Alpine.js, and ECharts are loaded from jsDelivr, so the browser needs
network access when opening the page. HTMX owns server communication and fragment
swaps; Alpine is limited to browser-local editor, clipboard, and chart behavior.

View File

@@ -404,11 +404,12 @@ fn render_result(result: &QueryOutput, chart_type: &str) -> Html<String> {
}
_ => return error_fragment("Unknown chart type"),
};
let safe_option = option.to_string().replace('<', "\\u003c");
let option = escape_html(&option.to_string());
Html(format!(
"<p class=\"result-meta\">{metadata}</p><div id=\"chart\" class=\"chart\"></div>\
<script>echarts.init(document.getElementById('chart')).setOption({safe_option});</script>"
"<p class=\"result-meta\">{metadata}</p>\
<div class=\"chart\" data-option=\"{option}\" \
x-init=\"echarts.init($el).setOption(JSON.parse($el.dataset.option))\"></div>"
))
}
@@ -429,9 +430,10 @@ fn render_catalog(catalog: &GetAnalyticsCatalogResponse) -> Html<String> {
<details class=\"llm-context\">\
<summary>LLM schema context</summary>\
<p class=\"hint\">Copy this entire text and include it when asking an LLM to write a query.</p>\
<textarea id=\"llm-schema\" readonly>{}</textarea>\
<button type=\"button\" class=\"secondary\" data-copy=\"llm-schema\">Copy context</button>\
<span id=\"copy-status\" class=\"hint\"></span>\
<textarea x-ref=\"llmSchema\" readonly>{}</textarea>\
<button type=\"button\" class=\"secondary\" \
x-on:click=\"navigator.clipboard.writeText($refs.llmSchema.value); copied = true\">Copy context</button>\
<span class=\"hint\" x-show=\"copied\" x-cloak>Copied</span>\
</details>",
catalog.tables.len(),
escape_html(&llm_context),
@@ -453,7 +455,8 @@ fn render_catalog_table(table: &AnalyticsTable) -> String {
details.push_str(&format!(", rounding {}", column.rounding));
}
format!(
"<li><button type=\"button\" class=\"column-name\" data-insert=\"{}\">{}</button><span>{}</span></li>",
"<li><button type=\"button\" class=\"column-name\" data-insert=\"{}\" \
x-on:click=\"$refs.sql.setRangeText($el.dataset.insert, $refs.sql.selectionStart, $refs.sql.selectionEnd, 'end'); sql = $refs.sql.value; $refs.sql.focus()\">{}</button><span>{}</span></li>",
escape_html(&quote_identifier(&column.name)),
escape_html(&column.name),
escape_html(&details),
@@ -489,7 +492,8 @@ fn render_catalog_table(table: &AnalyticsTable) -> String {
format!(
"<details class=\"catalog-table\">\
<summary><code>{}</code>{currency}</summary>\
<button type=\"button\" class=\"starter-query\" data-sql=\"{}\">Use starter query</button>\
<button type=\"button\" class=\"starter-query\" data-sql=\"{}\" \
x-on:click=\"sql = $el.dataset.sql; $refs.sql.focus()\">Use starter query</button>\
<ul class=\"columns\">{columns}</ul>{links}\
</details>",
escape_html(&table.name),

View File

@@ -6,8 +6,10 @@
<title>Analytics graphs</title>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3/dist/cdn.min.js"></script>
<style>
* { box-sizing: border-box; }
[x-cloak] { display: none !important; }
body { margin: 0; font: 14px system-ui, sans-serif; color: #17202a; background: #f4f6f8; }
main { width: min(1500px, calc(100% - 32px)); margin: 24px auto; }
h1 { margin: 0 0 18px; font-size: 24px; }
@@ -61,7 +63,7 @@
</style>
</head>
<body>
<main>
<main x-data="{ sql: '', copied: false }">
<div class="top"><h1>Analytics graphs</h1><a href="/login">Login</a></div>
<div class="workspace">
@@ -95,7 +97,7 @@
</label>
<label>Max rows<input name="max_rows" type="number" min="1" value="1000"></label>
</div>
<label>SQL<textarea id="sql" name="sql" required spellcheck="false" placeholder="Load the schema, then choose a starter query or write a SELECT query"></textarea></label>
<label>SQL<textarea id="sql" x-ref="sql" x-model="sql" name="sql" required spellcheck="false" placeholder="Load the schema, then choose a starter query or write a SELECT query"></textarea></label>
<div class="actions">
<button type="submit">Run query</button>
<span class="htmx-indicator hint">Running…</span>
@@ -106,32 +108,5 @@
</section>
</div>
</main>
<script>
document.addEventListener('click', async (event) => {
const queryButton = event.target.closest('[data-sql]');
if (queryButton) {
const editor = document.getElementById('sql');
editor.value = queryButton.dataset.sql;
editor.focus();
return;
}
const columnButton = event.target.closest('[data-insert]');
if (columnButton) {
const editor = document.getElementById('sql');
const start = editor.selectionStart;
editor.setRangeText(columnButton.dataset.insert, start, editor.selectionEnd, 'end');
editor.focus();
return;
}
const copyButton = event.target.closest('[data-copy]');
if (copyButton) {
const text = document.getElementById(copyButton.dataset.copy).value;
await navigator.clipboard.writeText(text);
document.getElementById('copy-status').textContent = 'Copied';
}
});
</script>
</body>
</html>