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 A small Rust web server that logs in through `AuthService.Login`, forwards SQL
queries to `AnalyticsService.ExecuteAnalyticsQuery`, and displays the streamed queries to `AnalyticsService.ExecuteAnalyticsQuery`, and displays the streamed
result with HTMX and ECharts. result with HTMX, Alpine.js, and ECharts.
## Run ## 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 text summary that can be copied into an LLM prompt to request a valid analytics
SQL query. SQL query.
HTMX and ECharts are loaded from jsDelivr, so the browser needs network access HTMX, Alpine.js, and ECharts are loaded from jsDelivr, so the browser needs
when opening the page. 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"), _ => return error_fragment("Unknown chart type"),
}; };
let safe_option = option.to_string().replace('<', "\\u003c"); let option = escape_html(&option.to_string());
Html(format!( Html(format!(
"<p class=\"result-meta\">{metadata}</p><div id=\"chart\" class=\"chart\"></div>\ "<p class=\"result-meta\">{metadata}</p>\
<script>echarts.init(document.getElementById('chart')).setOption({safe_option});</script>" <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\">\ <details class=\"llm-context\">\
<summary>LLM schema context</summary>\ <summary>LLM schema context</summary>\
<p class=\"hint\">Copy this entire text and include it when asking an LLM to write a query.</p>\ <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>\ <textarea x-ref=\"llmSchema\" readonly>{}</textarea>\
<button type=\"button\" class=\"secondary\" data-copy=\"llm-schema\">Copy context</button>\ <button type=\"button\" class=\"secondary\" \
<span id=\"copy-status\" class=\"hint\"></span>\ 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>", </details>",
catalog.tables.len(), catalog.tables.len(),
escape_html(&llm_context), escape_html(&llm_context),
@@ -453,7 +455,8 @@ fn render_catalog_table(table: &AnalyticsTable) -> String {
details.push_str(&format!(", rounding {}", column.rounding)); details.push_str(&format!(", rounding {}", column.rounding));
} }
format!( 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(&quote_identifier(&column.name)),
escape_html(&column.name), escape_html(&column.name),
escape_html(&details), escape_html(&details),
@@ -489,7 +492,8 @@ fn render_catalog_table(table: &AnalyticsTable) -> String {
format!( format!(
"<details class=\"catalog-table\">\ "<details class=\"catalog-table\">\
<summary><code>{}</code>{currency}</summary>\ <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}\ <ul class=\"columns\">{columns}</ul>{links}\
</details>", </details>",
escape_html(&table.name), escape_html(&table.name),

View File

@@ -6,8 +6,10 @@
<title>Analytics graphs</title> <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/htmx.org@2/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@6/dist/echarts.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> <style>
* { box-sizing: border-box; } * { box-sizing: border-box; }
[x-cloak] { display: none !important; }
body { margin: 0; font: 14px system-ui, sans-serif; color: #17202a; background: #f4f6f8; } body { margin: 0; font: 14px system-ui, sans-serif; color: #17202a; background: #f4f6f8; }
main { width: min(1500px, calc(100% - 32px)); margin: 24px auto; } main { width: min(1500px, calc(100% - 32px)); margin: 24px auto; }
h1 { margin: 0 0 18px; font-size: 24px; } h1 { margin: 0 0 18px; font-size: 24px; }
@@ -61,7 +63,7 @@
</style> </style>
</head> </head>
<body> <body>
<main> <main x-data="{ sql: '', copied: false }">
<div class="top"><h1>Analytics graphs</h1><a href="/login">Login</a></div> <div class="top"><h1>Analytics graphs</h1><a href="/login">Login</a></div>
<div class="workspace"> <div class="workspace">
@@ -95,7 +97,7 @@
</label> </label>
<label>Max rows<input name="max_rows" type="number" min="1" value="1000"></label> <label>Max rows<input name="max_rows" type="number" min="1" value="1000"></label>
</div> </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"> <div class="actions">
<button type="submit">Run query</button> <button type="submit">Run query</button>
<span class="htmx-indicator hint">Running…</span> <span class="htmx-indicator hint">Running…</span>
@@ -106,32 +108,5 @@
</section> </section>
</div> </div>
</main> </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> </body>
</html> </html>