|
83 | 83 | placeholder="-- click an example chip above, or type a query here. ⌘/Ctrl+Enter to run."></textarea> |
84 | 84 | </div> |
85 | 85 |
|
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 | | - |
91 | 86 | <div class="flex gap-2 mt-3" style="align-items: center;"> |
92 | 87 | <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;"></></button> |
93 | 89 | <input style="margin-left: 12px;" placeholder="save to library as…" x-model="saveName" /> |
94 | 90 | <button class="btn" @click="save()" :disabled="!saveName">Save to library</button> |
95 | 91 | <span class="card-meta ml-auto" x-text="resultMeta()"></span> |
96 | 92 | </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 | + |
97 | 108 | <div x-show="error" class="card tone-bad mt-3" style="padding: 8px 12px;" x-text="error"></div> |
98 | 109 | </div> |
99 | 110 | </div> |
|
448 | 459 | neo4j_user: {{ default_neo4j_user|default('neo4j')|tojson }}, |
449 | 460 | neo4j_password: {{ default_neo4j_password|default('')|tojson }}, |
450 | 461 | }; |
| 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 | +} |
451 | 629 | function dbConsole() { |
452 | 630 | return { |
453 | 631 | engines: ['sparql', 'cypher', 'opensearch'], |
454 | 632 | engine: 'sparql', |
455 | | - user: '', password: '', |
| 633 | + // Snippets panel state (curl / python / js examples). |
| 634 | + showSnippets: false, snippetLang: 'curl', snippetCopied: false, |
456 | 635 | osMode: 'sql', |
457 | 636 | busy: false, error: '', |
458 | 637 | columns: [], rows: [], truncated: false, rawDump: '', |
|
504 | 683 | this.engine = first.engine; |
505 | 684 | this.switchTab(first.id); |
506 | 685 |
|
507 | | - this.applyEngineAuth(); |
508 | | - window.addEventListener('op-secrets-changed', () => this.applyEngineAuth()); |
509 | | - |
510 | 686 | this.$watch('query', () => { this.recomputeGutter(); }); |
511 | 687 | this.$watch('engine', () => { |
512 | | - this.applyEngineAuth(); |
513 | 688 | this.error = ''; this.rows = []; this.columns = []; this.rawDump = ''; |
514 | 689 | }); |
515 | 690 |
|
|
678 | 853 | let path; |
679 | 854 | if (this.engine === 'sparql') { |
680 | 855 | path = '/api/databases/sparql/query'; |
681 | | - body.auth_user = this.user || null; body.auth_password = this.password || null; |
682 | 856 | } else if (this.engine === 'cypher') { |
683 | 857 | path = '/api/databases/cypher/query'; |
684 | | - body.auth_user = this.user || null; body.auth_password = this.password || null; |
685 | 858 | } else if (this.engine === 'opensearch') { |
686 | 859 | path = '/api/databases/opensearch/query'; |
687 | 860 | body.mode = this.osMode; |
688 | | - body.auth_user = this.user || null; body.auth_password = this.password || null; |
689 | 861 | } |
690 | 862 | const r = await fetch(path, { |
691 | 863 | method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(body), |
|
0 commit comments