Skip to content

Commit 890b86f

Browse files
committed
feat(hub): drop /databases auth inputs and add curl/python/js snippet help
The user/password inputs on the SPARQL / Cypher / OpenSearch consoles were a holdover from before the route handlers learned to fall back to server-side credentials parsed from `SPARQL_AUTH`, `NEO4J_AUTH` and `OPENSEARCH_PASSWORD`. Now that those defaults are wired through `window.opDefaults` and respected by the API handlers, the visible inputs were duplicate state with no value-add for the operator. Removed: - The user/password input row from each console. - `user` / `password` state and the `applyEngineAuth` / `needsAuthInputs` / `userPlaceholder` helpers (all dead code now). - `body.auth_user` / `body.auth_password` from the API request payloads; the server's settings fallback fills them. Added: - A small `</>` button next to Run that toggles a panel showing how to replicate the current query from outside the Hub. Tabs for curl / python / js; the snippet reflects the current engine (SPARQL / Cypher / OpenSearch) and, for SPARQL, distinguishes reads (anonymous-friendly) from updates (admin auth required). A Copy button lifts the snippet to the clipboard. The snippet uses `window.location.hostname` so it always matches the host the operator is currently looking at (e.g. `openpulse.epfl.ch`), and points at the firewall-open ports defined in `infra/env/.env`.
1 parent d36b84e commit 890b86f

1 file changed

Lines changed: 185 additions & 13 deletions

File tree

src/open_pulse/gui/hub/templates/databases.html

Lines changed: 185 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -83,17 +83,28 @@
8383
placeholder="-- click an example chip above, or type a query here. ⌘/Ctrl+Enter to run."></textarea>
8484
</div>
8585

86-
<div x-show="needsAuthInputs()" style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px;">
87-
<input :placeholder="userPlaceholder()" x-model="user" />
88-
<input type="password" placeholder="password (defaults to Settings)" x-model="password" />
89-
</div>
90-
9186
<div class="flex gap-2 mt-3" style="align-items: center;">
9287
<button class="btn btn-primary" @click="run()" :disabled="busy">Run <span class="kbd ml-1">⌘↵</span></button>
88+
<button class="btn" @click="showSnippets = !showSnippets" :title="showSnippets ? 'Hide examples' : 'Show curl / python / js examples'" style="padding-left: 10px; padding-right: 10px;">&lt;/&gt;</button>
9389
<input style="margin-left: 12px;" placeholder="save to library as…" x-model="saveName" />
9490
<button class="btn" @click="save()" :disabled="!saveName">Save to library</button>
9591
<span class="card-meta ml-auto" x-text="resultMeta()"></span>
9692
</div>
93+
94+
<div x-show="showSnippets" x-cloak class="card mt-3" style="padding: 10px 14px; background: var(--panel-bg, #0f1218);">
95+
<div class="flex gap-2" style="align-items: center; margin-bottom: 8px;">
96+
<span class="card-meta">Run this query from outside the Hub:</span>
97+
<template x-for="lang in ['curl','python','js']" :key="lang">
98+
<button class="btn" :class="snippetLang === lang ? 'btn-primary' : ''"
99+
@click="snippetLang = lang" x-text="lang" style="padding: 2px 10px;"></button>
100+
</template>
101+
<button class="btn ml-auto" @click="copySnippet()" :title="snippetCopied ? 'Copied!' : 'Copy snippet'"
102+
x-text="snippetCopied ? '✓ copied' : 'Copy'" style="padding: 2px 10px;"></button>
103+
</div>
104+
<pre class="console" style="margin: 0; padding: 10px 12px; white-space: pre-wrap; max-height: 240px; overflow: auto;" x-text="renderSnippet()"></pre>
105+
<p class="card-meta" style="margin-top: 6px;">Replace placeholder credentials with the values from <code>infra/env/.env</code>. Reads are public by default; writes require admin auth.</p>
106+
</div>
107+
97108
<div x-show="error" class="card tone-bad mt-3" style="padding: 8px 12px;" x-text="error"></div>
98109
</div>
99110
</div>
@@ -448,11 +459,179 @@
448459
neo4j_user: {{ default_neo4j_user|default('neo4j')|tojson }},
449460
neo4j_password: {{ default_neo4j_password|default('')|tojson }},
450461
};
462+
// ── Snippet generators (called from dbConsole().renderSnippet) ───────
463+
function _jsStr(s) { return JSON.stringify(s ?? ''); }
464+
function _shQuote(s) { return "'" + String(s).replace(/'/g, "'\\''") + "'"; }
465+
function _looksLikeSparqlUpdate(q) {
466+
const stripped = q.replace(/^\s*PREFIX[^\n]*\n?/gmi, '').trim().toUpperCase();
467+
return /^(INSERT|DELETE|LOAD|CLEAR|CREATE|DROP|COPY|MOVE|ADD)\b/.test(stripped);
468+
}
469+
function _sparqlSnippet(lang, q, host) {
470+
const isUpdate = _looksLikeSparqlUpdate(q);
471+
const base = 'http://' + host + ':7502';
472+
const path = isUpdate ? '/update' : '/query';
473+
const endpoint = base + path;
474+
if (lang === 'curl') {
475+
if (isUpdate) {
476+
return [
477+
'# SPARQL update — admin Basic Auth required',
478+
'curl -X POST -u ' + _shQuote('admin_user:password') + ' \\',
479+
" -H 'Content-Type: application/sparql-update' \\",
480+
' --data ' + _shQuote(q) + ' \\',
481+
' ' + _shQuote(endpoint),
482+
].join('\n');
483+
}
484+
return [
485+
'# SPARQL query (public read by default)',
486+
'curl -G ' + _shQuote(endpoint) + ' \\',
487+
" -H 'Accept: application/sparql-results+json' \\",
488+
' --data-urlencode ' + _shQuote('query=' + q),
489+
].join('\n');
490+
}
491+
if (lang === 'python') {
492+
if (isUpdate) {
493+
return [
494+
'import httpx',
495+
'',
496+
'r = httpx.post(',
497+
' ' + _jsStr(endpoint) + ',',
498+
' content=' + _jsStr(q) + ',',
499+
' headers={"Content-Type": "application/sparql-update"},',
500+
' auth=("admin_user", "password"), # SPARQL_AUTH',
501+
')',
502+
'r.raise_for_status()',
503+
].join('\n');
504+
}
505+
return [
506+
'import httpx',
507+
'',
508+
'r = httpx.get(',
509+
' ' + _jsStr(endpoint) + ',',
510+
' params={"query": ' + _jsStr(q) + '},',
511+
' headers={"Accept": "application/sparql-results+json"},',
512+
')',
513+
'r.raise_for_status()',
514+
'print(r.json())',
515+
].join('\n');
516+
}
517+
if (lang === 'js') {
518+
if (isUpdate) {
519+
return [
520+
'// SPARQL update — admin Basic Auth required',
521+
"const r = await fetch(" + _jsStr(endpoint) + ", {",
522+
" method: 'POST',",
523+
" headers: {",
524+
" 'Content-Type': 'application/sparql-update',",
525+
" Authorization: 'Basic ' + btoa('admin_user:password'), // SPARQL_AUTH",
526+
" },",
527+
' body: ' + _jsStr(q) + ',',
528+
'});',
529+
].join('\n');
530+
}
531+
return [
532+
'// SPARQL query (public read by default)',
533+
"const url = " + _jsStr(endpoint) + " + '?' + new URLSearchParams({ query: " + _jsStr(q) + " });",
534+
"const r = await fetch(url, { headers: { Accept: 'application/sparql-results+json' } });",
535+
'console.log(await r.json());',
536+
].join('\n');
537+
}
538+
return '';
539+
}
540+
function _cypherSnippet(lang, q, host) {
541+
const bolt = 'bolt://' + host + ':7504';
542+
const httpEndpoint = 'http://' + host + ':7503/db/neo4j/tx/commit';
543+
if (lang === 'curl') {
544+
return [
545+
'# Cypher via the Neo4j HTTP API',
546+
'curl -X POST -u ' + _shQuote('neo4j:password') + ' \\',
547+
" -H 'Content-Type: application/json' \\",
548+
" -H 'Accept: application/json' \\",
549+
' --data ' + _shQuote(JSON.stringify({statements: [{statement: q}]})) + ' \\',
550+
' ' + _shQuote(httpEndpoint),
551+
].join('\n');
552+
}
553+
if (lang === 'python') {
554+
return [
555+
'# pip install neo4j',
556+
'from neo4j import GraphDatabase',
557+
'',
558+
'with GraphDatabase.driver(' + _jsStr(bolt) + ', auth=("neo4j", "password")) as driver:',
559+
' with driver.session() as session:',
560+
' for record in session.run(' + _jsStr(q) + '):',
561+
' print(record)',
562+
].join('\n');
563+
}
564+
if (lang === 'js') {
565+
return [
566+
'// Cypher via the Neo4j HTTP API',
567+
'const body = { statements: [{ statement: ' + _jsStr(q) + ' }] };',
568+
"const r = await fetch(" + _jsStr(httpEndpoint) + ", {",
569+
" method: 'POST',",
570+
" headers: {",
571+
" 'Content-Type': 'application/json',",
572+
" Authorization: 'Basic ' + btoa('neo4j:password'),",
573+
" },",
574+
' body: JSON.stringify(body),',
575+
'});',
576+
'console.log(await r.json());',
577+
].join('\n');
578+
}
579+
return '';
580+
}
581+
function _opensearchSnippet(lang, q, host, osMode) {
582+
// OpenSearch is currently in-network only. Show the internal URL plus a
583+
// note. For external access route through the GrimoireLab nginx instead.
584+
const endpoint = 'https://opensearch-node1:9200/_search';
585+
const note = '# Internal-only — run from a host on the docker network, or proxy via GrimoireLab nginx.\n';
586+
const payload = (osMode === 'sql') ? '/_plugins/_sql' : '/_search';
587+
const url = 'https://opensearch-node1:9200' + payload;
588+
if (lang === 'curl') {
589+
return [
590+
note.trimEnd(),
591+
'curl -k -X POST -u ' + _shQuote('admin:password') + ' \\',
592+
" -H 'Content-Type: application/json' \\",
593+
' --data ' + _shQuote(q) + ' \\',
594+
' ' + _shQuote(url),
595+
].join('\n');
596+
}
597+
if (lang === 'python') {
598+
return [
599+
note.trimEnd(),
600+
'import httpx',
601+
'',
602+
'r = httpx.post(',
603+
' ' + _jsStr(url) + ',',
604+
' content=' + _jsStr(q) + ',',
605+
' headers={"Content-Type": "application/json"},',
606+
' auth=("admin", "password"),',
607+
' verify=False, # self-signed cert in dev',
608+
')',
609+
'r.raise_for_status()',
610+
'print(r.json())',
611+
].join('\n');
612+
}
613+
if (lang === 'js') {
614+
return [
615+
note.trimEnd(),
616+
"const r = await fetch(" + _jsStr(url) + ", {",
617+
" method: 'POST',",
618+
" headers: {",
619+
" 'Content-Type': 'application/json',",
620+
" Authorization: 'Basic ' + btoa('admin:password'),",
621+
" },",
622+
' body: ' + _jsStr(q) + ',',
623+
'});',
624+
'console.log(await r.json());',
625+
].join('\n');
626+
}
627+
return '';
628+
}
451629
function dbConsole() {
452630
return {
453631
engines: ['sparql', 'cypher', 'opensearch'],
454632
engine: 'sparql',
455-
user: '', password: '',
633+
// Snippets panel state (curl / python / js examples).
634+
showSnippets: false, snippetLang: 'curl', snippetCopied: false,
456635
osMode: 'sql',
457636
busy: false, error: '',
458637
columns: [], rows: [], truncated: false, rawDump: '',
@@ -504,12 +683,8 @@
504683
this.engine = first.engine;
505684
this.switchTab(first.id);
506685

507-
this.applyEngineAuth();
508-
window.addEventListener('op-secrets-changed', () => this.applyEngineAuth());
509-
510686
this.$watch('query', () => { this.recomputeGutter(); });
511687
this.$watch('engine', () => {
512-
this.applyEngineAuth();
513688
this.error = ''; this.rows = []; this.columns = []; this.rawDump = '';
514689
});
515690

@@ -678,14 +853,11 @@
678853
let path;
679854
if (this.engine === 'sparql') {
680855
path = '/api/databases/sparql/query';
681-
body.auth_user = this.user || null; body.auth_password = this.password || null;
682856
} else if (this.engine === 'cypher') {
683857
path = '/api/databases/cypher/query';
684-
body.auth_user = this.user || null; body.auth_password = this.password || null;
685858
} else if (this.engine === 'opensearch') {
686859
path = '/api/databases/opensearch/query';
687860
body.mode = this.osMode;
688-
body.auth_user = this.user || null; body.auth_password = this.password || null;
689861
}
690862
const r = await fetch(path, {
691863
method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body),

0 commit comments

Comments
 (0)