sqlite_dashboard 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +9 -5
- data/app/controllers/sqlite_dashboard/databases_controller.rb +51 -0
- data/app/javascript/controllers/saved_queries_controller.js +379 -0
- data/app/models/sqlite_dashboard/application_record.rb +7 -0
- data/app/models/sqlite_dashboard/saved_query.rb +14 -0
- data/app/views/layouts/sqlite_dashboard/application.html.erb +512 -4
- data/app/views/sqlite_dashboard/databases/index.html.erb +53 -4
- data/app/views/sqlite_dashboard/databases/show.html.erb +36 -2
- data/app/views/sqlite_dashboard/databases/worksheet.html.erb +111 -0
- data/config/routes.rb +9 -0
- data/db/migrate/20250101000001_create_sqlite_dashboard_saved_queries.rb +17 -0
- data/lib/generators/sqlite_dashboard/install_generator.rb +4 -0
- data/lib/sqlite_dashboard/version.rb +1 -1
- data/sqlite_dashboard.gemspec +1 -0
- metadata +22 -3
|
@@ -400,6 +400,33 @@
|
|
|
400
400
|
color: #93c5fd;
|
|
401
401
|
}
|
|
402
402
|
|
|
403
|
+
.saved-query-item {
|
|
404
|
+
padding: 0.75rem;
|
|
405
|
+
margin-bottom: 0.5rem;
|
|
406
|
+
background-color: #334155;
|
|
407
|
+
border-radius: 0.375rem;
|
|
408
|
+
border: 1px solid #475569;
|
|
409
|
+
transition: all 0.2s ease;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.saved-query-item:hover {
|
|
413
|
+
background-color: #475569;
|
|
414
|
+
border-color: #64748b;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.saved-query-item .fw-bold {
|
|
418
|
+
color: var(--sidebar-text);
|
|
419
|
+
margin-bottom: 0.25rem;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.saved-query-item .text-muted {
|
|
423
|
+
color: #94a3b8;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.saved-query-item .text-info {
|
|
427
|
+
color: #60a5fa;
|
|
428
|
+
}
|
|
429
|
+
|
|
403
430
|
.error-message {
|
|
404
431
|
background-color: var(--error-bg);
|
|
405
432
|
border: 2px solid var(--error-border);
|
|
@@ -656,6 +683,83 @@
|
|
|
656
683
|
transform: translateX(4px);
|
|
657
684
|
}
|
|
658
685
|
|
|
686
|
+
/* Saved Queries Grid */
|
|
687
|
+
.saved-queries-grid {
|
|
688
|
+
display: grid;
|
|
689
|
+
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
|
690
|
+
gap: 1.5rem;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
.saved-query-card {
|
|
694
|
+
background-color: var(--card-bg);
|
|
695
|
+
border: 2px solid var(--border-color);
|
|
696
|
+
border-radius: 0.75rem;
|
|
697
|
+
padding: 1.25rem;
|
|
698
|
+
transition: all 0.2s ease;
|
|
699
|
+
box-shadow: var(--shadow-sm);
|
|
700
|
+
display: flex;
|
|
701
|
+
flex-direction: column;
|
|
702
|
+
gap: 0.75rem;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.saved-query-card:hover {
|
|
706
|
+
transform: translateY(-2px);
|
|
707
|
+
box-shadow: var(--shadow-md);
|
|
708
|
+
border-color: #94a3b8;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
.saved-query-header {
|
|
712
|
+
display: flex;
|
|
713
|
+
justify-content: space-between;
|
|
714
|
+
align-items: start;
|
|
715
|
+
gap: 0.75rem;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
.saved-query-title {
|
|
719
|
+
font-size: 1rem;
|
|
720
|
+
font-weight: 600;
|
|
721
|
+
color: var(--text-primary);
|
|
722
|
+
margin: 0;
|
|
723
|
+
display: flex;
|
|
724
|
+
align-items: center;
|
|
725
|
+
gap: 0.5rem;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.saved-query-title i {
|
|
729
|
+
color: var(--primary-color);
|
|
730
|
+
font-size: 0.875rem;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
.saved-query-description {
|
|
734
|
+
color: var(--text-secondary);
|
|
735
|
+
font-size: 0.875rem;
|
|
736
|
+
margin: 0;
|
|
737
|
+
line-height: 1.5;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.saved-query-sql {
|
|
741
|
+
background-color: #f8fafc;
|
|
742
|
+
border: 1px solid var(--border-color);
|
|
743
|
+
border-radius: 0.375rem;
|
|
744
|
+
padding: 0.75rem;
|
|
745
|
+
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
|
746
|
+
font-size: 0.75rem;
|
|
747
|
+
overflow-x: auto;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
.saved-query-sql code {
|
|
751
|
+
color: #1e293b;
|
|
752
|
+
background: none;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.saved-query-meta {
|
|
756
|
+
display: flex;
|
|
757
|
+
justify-content: space-between;
|
|
758
|
+
align-items: center;
|
|
759
|
+
padding-top: 0.75rem;
|
|
760
|
+
border-top: 1px solid var(--border-color);
|
|
761
|
+
}
|
|
762
|
+
|
|
659
763
|
/* Empty state */
|
|
660
764
|
.empty-state-modern {
|
|
661
765
|
max-width: 700px;
|
|
@@ -819,7 +923,7 @@
|
|
|
819
923
|
|
|
820
924
|
// Query Executor Controller
|
|
821
925
|
class QueryExecutorController extends Controller {
|
|
822
|
-
static targets = ["queryInput"]
|
|
926
|
+
static targets = ["queryInput", "queryName", "queryDescription", "saveError"]
|
|
823
927
|
|
|
824
928
|
initialize() {
|
|
825
929
|
this.currentPage = 1
|
|
@@ -858,7 +962,7 @@
|
|
|
858
962
|
}
|
|
859
963
|
|
|
860
964
|
executeQuery() {
|
|
861
|
-
const form = this.element
|
|
965
|
+
const form = this.element.querySelector('form') || this.element
|
|
862
966
|
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
863
967
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
864
968
|
|
|
@@ -1133,7 +1237,7 @@
|
|
|
1133
1237
|
const separator = document.getElementById('csv-separator').value
|
|
1134
1238
|
const includeHeaders = document.getElementById('csv-headers').checked
|
|
1135
1239
|
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
1136
|
-
const form = this.element
|
|
1240
|
+
const form = this.element.querySelector('form') || this.element
|
|
1137
1241
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
1138
1242
|
|
|
1139
1243
|
const formData = new FormData()
|
|
@@ -1177,7 +1281,7 @@
|
|
|
1177
1281
|
const format = document.getElementById('json-format').value
|
|
1178
1282
|
const prettyPrint = document.getElementById('json-pretty').checked
|
|
1179
1283
|
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
1180
|
-
const form = this.element
|
|
1284
|
+
const form = this.element.querySelector('form') || this.element
|
|
1181
1285
|
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content
|
|
1182
1286
|
|
|
1183
1287
|
const formData = new FormData()
|
|
@@ -1216,6 +1320,75 @@
|
|
|
1216
1320
|
alert('Export error: ' + error.message)
|
|
1217
1321
|
})
|
|
1218
1322
|
}
|
|
1323
|
+
|
|
1324
|
+
async saveQuery() {
|
|
1325
|
+
const name = this.queryNameTarget.value.trim()
|
|
1326
|
+
const description = this.queryDescriptionTarget.value.trim()
|
|
1327
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
1328
|
+
const databaseName = this.element.dataset.databaseName
|
|
1329
|
+
|
|
1330
|
+
if (!name) {
|
|
1331
|
+
this.showSaveError('Query name is required')
|
|
1332
|
+
return
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
if (!description) {
|
|
1336
|
+
this.showSaveError('Description is required')
|
|
1337
|
+
return
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
if (!query.trim()) {
|
|
1341
|
+
this.showSaveError('Query cannot be empty')
|
|
1342
|
+
return
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
try {
|
|
1346
|
+
const response = await fetch('/sqlite_dashboard/saved_queries', {
|
|
1347
|
+
method: 'POST',
|
|
1348
|
+
headers: {
|
|
1349
|
+
'Content-Type': 'application/json',
|
|
1350
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
|
1351
|
+
},
|
|
1352
|
+
body: JSON.stringify({
|
|
1353
|
+
saved_query: {
|
|
1354
|
+
name: name,
|
|
1355
|
+
description: description,
|
|
1356
|
+
query: query,
|
|
1357
|
+
database_name: databaseName
|
|
1358
|
+
}
|
|
1359
|
+
})
|
|
1360
|
+
})
|
|
1361
|
+
|
|
1362
|
+
const data = await response.json()
|
|
1363
|
+
|
|
1364
|
+
if (response.ok) {
|
|
1365
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById('saveQueryModal'))
|
|
1366
|
+
modal.hide()
|
|
1367
|
+
this.queryNameTarget.value = ''
|
|
1368
|
+
this.queryDescriptionTarget.value = ''
|
|
1369
|
+
this.hideSaveError()
|
|
1370
|
+
alert('Query saved successfully!')
|
|
1371
|
+
} else {
|
|
1372
|
+
this.showSaveError(data.error || 'Failed to save query')
|
|
1373
|
+
}
|
|
1374
|
+
} catch (error) {
|
|
1375
|
+
console.error('Error saving query:', error)
|
|
1376
|
+
this.showSaveError(error.message)
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
showSaveError(message) {
|
|
1381
|
+
if (this.hasSaveErrorTarget) {
|
|
1382
|
+
this.saveErrorTarget.textContent = message
|
|
1383
|
+
this.saveErrorTarget.classList.remove('d-none')
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
hideSaveError() {
|
|
1388
|
+
if (this.hasSaveErrorTarget) {
|
|
1389
|
+
this.saveErrorTarget.classList.add('d-none')
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1219
1392
|
}
|
|
1220
1393
|
|
|
1221
1394
|
// Table Selector Controller
|
|
@@ -1257,10 +1430,345 @@
|
|
|
1257
1430
|
}
|
|
1258
1431
|
}
|
|
1259
1432
|
|
|
1433
|
+
// Saved Queries Controller
|
|
1434
|
+
class SavedQueriesController extends Controller {
|
|
1435
|
+
static targets = ["queryInput", "queryName", "queryDescription", "databaseSelector", "list", "saveError"]
|
|
1436
|
+
|
|
1437
|
+
connect() {
|
|
1438
|
+
console.log("SavedQueries controller connected")
|
|
1439
|
+
this.loadSavedQueries()
|
|
1440
|
+
this.initializeCodeMirror()
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
initializeCodeMirror() {
|
|
1444
|
+
if (typeof CodeMirror !== 'undefined' && this.hasQueryInputTarget) {
|
|
1445
|
+
this.editor = CodeMirror.fromTextArea(this.queryInputTarget, {
|
|
1446
|
+
mode: 'text/x-sql',
|
|
1447
|
+
theme: 'default',
|
|
1448
|
+
lineNumbers: true,
|
|
1449
|
+
lineWrapping: true,
|
|
1450
|
+
autoCloseBrackets: true,
|
|
1451
|
+
matchBrackets: true,
|
|
1452
|
+
indentWithTabs: true,
|
|
1453
|
+
smartIndent: true,
|
|
1454
|
+
extraKeys: {
|
|
1455
|
+
"Ctrl-Enter": () => this.execute(),
|
|
1456
|
+
"Cmd-Enter": () => this.execute(),
|
|
1457
|
+
"Ctrl-Space": "autocomplete"
|
|
1458
|
+
}
|
|
1459
|
+
})
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
async loadSavedQueries() {
|
|
1464
|
+
try {
|
|
1465
|
+
const response = await fetch('/sqlite_dashboard/saved_queries')
|
|
1466
|
+
const queries = await response.json()
|
|
1467
|
+
this.renderSavedQueries(queries)
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
console.error('Error loading saved queries:', error)
|
|
1470
|
+
if (this.hasListTarget) {
|
|
1471
|
+
this.listTarget.innerHTML = `<div class="text-danger small text-center py-3"><i class="fas fa-exclamation-triangle"></i> Error loading queries</div>`
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
renderSavedQueries(queries) {
|
|
1477
|
+
if (!this.hasListTarget) return
|
|
1478
|
+
|
|
1479
|
+
if (queries.length === 0) {
|
|
1480
|
+
this.listTarget.innerHTML = `<div class="text-muted small text-center py-3">No saved queries</div>`
|
|
1481
|
+
return
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
const html = queries.map(query => `
|
|
1485
|
+
<div class="saved-query-item" data-query-id="${query.id}">
|
|
1486
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
1487
|
+
<div class="flex-grow-1" style="cursor: pointer;"
|
|
1488
|
+
data-action="click->saved-queries#loadQuery"
|
|
1489
|
+
data-query-id="${query.id}"
|
|
1490
|
+
data-query-name="${this.escapeHtml(query.name)}"
|
|
1491
|
+
data-query-sql="${this.escapeHtml(query.query)}"
|
|
1492
|
+
data-query-database="${query.database_name || ''}">
|
|
1493
|
+
<div class="fw-bold small">${this.escapeHtml(query.name)}</div>
|
|
1494
|
+
${query.description ? `<div class="text-muted" style="font-size: 0.75rem;">${this.escapeHtml(query.description)}</div>` : ''}
|
|
1495
|
+
${query.database_name ? `<div class="text-info" style="font-size: 0.7rem;"><i class="fas fa-database"></i> ${this.escapeHtml(query.database_name)}</div>` : ''}
|
|
1496
|
+
</div>
|
|
1497
|
+
<button class="btn btn-sm btn-link text-danger p-0" data-action="click->saved-queries#deleteQuery" data-query-id="${query.id}">
|
|
1498
|
+
<i class="fas fa-trash"></i>
|
|
1499
|
+
</button>
|
|
1500
|
+
</div>
|
|
1501
|
+
</div>
|
|
1502
|
+
`).join('')
|
|
1503
|
+
|
|
1504
|
+
this.listTarget.innerHTML = html
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
escapeHtml(text) {
|
|
1508
|
+
const div = document.createElement('div')
|
|
1509
|
+
div.textContent = text
|
|
1510
|
+
return div.innerHTML
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
async execute() {
|
|
1514
|
+
const databaseId = this.databaseSelectorTarget.value
|
|
1515
|
+
if (!databaseId) {
|
|
1516
|
+
alert('Please select a database first')
|
|
1517
|
+
return
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
1521
|
+
if (!query.trim()) {
|
|
1522
|
+
alert('Please enter a query')
|
|
1523
|
+
return
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
try {
|
|
1527
|
+
const response = await fetch(`/sqlite_dashboard/databases/${databaseId}/execute_query`, {
|
|
1528
|
+
method: 'POST',
|
|
1529
|
+
headers: {
|
|
1530
|
+
'Content-Type': 'application/json',
|
|
1531
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
|
1532
|
+
},
|
|
1533
|
+
body: JSON.stringify({ query })
|
|
1534
|
+
})
|
|
1535
|
+
|
|
1536
|
+
const data = await response.json()
|
|
1537
|
+
|
|
1538
|
+
if (data.error) {
|
|
1539
|
+
this.renderError(data.error)
|
|
1540
|
+
} else {
|
|
1541
|
+
this.renderResults(data, query, databaseId)
|
|
1542
|
+
}
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
console.error('Error executing query:', error)
|
|
1545
|
+
this.renderError(error.message)
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
renderResults(data) {
|
|
1550
|
+
const resultsContainer = document.getElementById('worksheet-results')
|
|
1551
|
+
|
|
1552
|
+
if (data.message) {
|
|
1553
|
+
resultsContainer.innerHTML = `<div class="alert alert-success"><i class="fas fa-check-circle"></i> ${data.message}</div>`
|
|
1554
|
+
return
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
const { columns, rows } = data
|
|
1558
|
+
|
|
1559
|
+
let html = `
|
|
1560
|
+
<div class="results-header">
|
|
1561
|
+
<h6>Query Results</h6>
|
|
1562
|
+
<div class="text-muted small">${rows.length} row(s) returned</div>
|
|
1563
|
+
</div>
|
|
1564
|
+
<div class="table-responsive">
|
|
1565
|
+
<table class="table table-striped table-hover">
|
|
1566
|
+
<thead><tr>${columns.map(col => `<th>${this.escapeHtml(col)}</th>`).join('')}</tr></thead>
|
|
1567
|
+
<tbody>
|
|
1568
|
+
${rows.map(row => `<tr>${row.map(cell => `<td>${cell !== null ? this.escapeHtml(String(cell)) : '<span class="text-muted">NULL</span>'}</td>`).join('')}</tr>`).join('')}
|
|
1569
|
+
</tbody>
|
|
1570
|
+
</table>
|
|
1571
|
+
</div>
|
|
1572
|
+
`
|
|
1573
|
+
|
|
1574
|
+
resultsContainer.innerHTML = html
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
renderError(error) {
|
|
1578
|
+
const resultsContainer = document.getElementById('worksheet-results')
|
|
1579
|
+
resultsContainer.innerHTML = `<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> <strong>Error:</strong> ${this.escapeHtml(error)}</div>`
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
clear() {
|
|
1583
|
+
if (this.editor) {
|
|
1584
|
+
this.editor.setValue('')
|
|
1585
|
+
} else {
|
|
1586
|
+
this.queryInputTarget.value = ''
|
|
1587
|
+
}
|
|
1588
|
+
document.getElementById('worksheet-results').innerHTML = `<div class="text-muted text-center py-5"><i class="fas fa-database fa-3x mb-3"></i><p>Select a database and execute a query to see results</p></div>`
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
async saveQuery() {
|
|
1592
|
+
const name = this.queryNameTarget.value.trim()
|
|
1593
|
+
const description = this.queryDescriptionTarget.value.trim()
|
|
1594
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
1595
|
+
const databaseId = this.databaseSelectorTarget.value
|
|
1596
|
+
const databaseName = databaseId ? this.databaseSelectorTarget.options[this.databaseSelectorTarget.selectedIndex].text : null
|
|
1597
|
+
|
|
1598
|
+
if (!name) {
|
|
1599
|
+
this.showSaveError('Query name is required')
|
|
1600
|
+
return
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
if (!description) {
|
|
1604
|
+
this.showSaveError('Description is required')
|
|
1605
|
+
return
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
if (!query.trim()) {
|
|
1609
|
+
this.showSaveError('Query cannot be empty')
|
|
1610
|
+
return
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
try {
|
|
1614
|
+
const response = await fetch('/sqlite_dashboard/saved_queries', {
|
|
1615
|
+
method: 'POST',
|
|
1616
|
+
headers: {
|
|
1617
|
+
'Content-Type': 'application/json',
|
|
1618
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
|
1619
|
+
},
|
|
1620
|
+
body: JSON.stringify({
|
|
1621
|
+
saved_query: { name, description, query, database_name: databaseName }
|
|
1622
|
+
})
|
|
1623
|
+
})
|
|
1624
|
+
|
|
1625
|
+
const data = await response.json()
|
|
1626
|
+
|
|
1627
|
+
if (response.ok) {
|
|
1628
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById('saveQueryModal'))
|
|
1629
|
+
modal.hide()
|
|
1630
|
+
this.queryNameTarget.value = ''
|
|
1631
|
+
this.queryDescriptionTarget.value = ''
|
|
1632
|
+
this.hideSaveError()
|
|
1633
|
+
await this.loadSavedQueries()
|
|
1634
|
+
alert('Query saved successfully!')
|
|
1635
|
+
} else {
|
|
1636
|
+
this.showSaveError(data.error || 'Failed to save query')
|
|
1637
|
+
}
|
|
1638
|
+
} catch (error) {
|
|
1639
|
+
console.error('Error saving query:', error)
|
|
1640
|
+
this.showSaveError(error.message)
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
showSaveError(message) {
|
|
1645
|
+
if (this.hasSaveErrorTarget) {
|
|
1646
|
+
this.saveErrorTarget.textContent = message
|
|
1647
|
+
this.saveErrorTarget.classList.remove('d-none')
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
hideSaveError() {
|
|
1652
|
+
if (this.hasSaveErrorTarget) {
|
|
1653
|
+
this.saveErrorTarget.classList.add('d-none')
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
loadQuery(event) {
|
|
1658
|
+
const element = event.currentTarget
|
|
1659
|
+
const queryName = element.dataset.queryName
|
|
1660
|
+
const querySql = element.dataset.querySql
|
|
1661
|
+
const queryDatabase = element.dataset.queryDatabase
|
|
1662
|
+
|
|
1663
|
+
if (!confirm(`Load query "${queryName}" into the editor?`)) {
|
|
1664
|
+
return
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
if (this.editor) {
|
|
1668
|
+
this.editor.setValue(querySql)
|
|
1669
|
+
} else {
|
|
1670
|
+
this.queryInputTarget.value = querySql
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
if (queryDatabase && this.hasDatabaseSelectorTarget) {
|
|
1674
|
+
const option = Array.from(this.databaseSelectorTarget.options).find(opt => opt.text === queryDatabase)
|
|
1675
|
+
if (option) {
|
|
1676
|
+
this.databaseSelectorTarget.value = option.value
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
async deleteQuery(event) {
|
|
1682
|
+
event.stopPropagation()
|
|
1683
|
+
const queryId = event.currentTarget.dataset.queryId
|
|
1684
|
+
|
|
1685
|
+
if (!confirm('Are you sure you want to delete this saved query?')) {
|
|
1686
|
+
return
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
try {
|
|
1690
|
+
const response = await fetch(`/sqlite_dashboard/saved_queries/${queryId}`, {
|
|
1691
|
+
method: 'DELETE',
|
|
1692
|
+
headers: { 'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content }
|
|
1693
|
+
})
|
|
1694
|
+
|
|
1695
|
+
if (response.ok) {
|
|
1696
|
+
await this.loadSavedQueries()
|
|
1697
|
+
} else {
|
|
1698
|
+
const data = await response.json()
|
|
1699
|
+
alert(data.error || 'Failed to delete query')
|
|
1700
|
+
}
|
|
1701
|
+
} catch (error) {
|
|
1702
|
+
console.error('Error deleting query:', error)
|
|
1703
|
+
alert('Failed to delete query')
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
refresh() {
|
|
1708
|
+
this.loadSavedQueries()
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
async exportCSV() {
|
|
1712
|
+
const databaseId = this.databaseSelectorTarget.value
|
|
1713
|
+
if (!databaseId) {
|
|
1714
|
+
alert('Please select a database first')
|
|
1715
|
+
return
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
1719
|
+
if (!query.trim()) {
|
|
1720
|
+
alert('Please enter a query')
|
|
1721
|
+
return
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const form = document.createElement('form')
|
|
1725
|
+
form.method = 'POST'
|
|
1726
|
+
form.action = `/sqlite_dashboard/databases/${databaseId}/export_csv`
|
|
1727
|
+
const csrfToken = document.querySelector('[name="csrf-token"]').content
|
|
1728
|
+
form.innerHTML = `
|
|
1729
|
+
<input type="hidden" name="authenticity_token" value="${csrfToken}">
|
|
1730
|
+
<input type="hidden" name="query" value="${this.escapeHtml(query)}">
|
|
1731
|
+
<input type="hidden" name="include_headers" value="true">
|
|
1732
|
+
`
|
|
1733
|
+
document.body.appendChild(form)
|
|
1734
|
+
form.submit()
|
|
1735
|
+
document.body.removeChild(form)
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
async exportJSON() {
|
|
1739
|
+
const databaseId = this.databaseSelectorTarget.value
|
|
1740
|
+
if (!databaseId) {
|
|
1741
|
+
alert('Please select a database first')
|
|
1742
|
+
return
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
1746
|
+
if (!query.trim()) {
|
|
1747
|
+
alert('Please enter a query')
|
|
1748
|
+
return
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
const form = document.createElement('form')
|
|
1752
|
+
form.method = 'POST'
|
|
1753
|
+
form.action = `/sqlite_dashboard/databases/${databaseId}/export_json`
|
|
1754
|
+
const csrfToken = document.querySelector('[name="csrf-token"]').content
|
|
1755
|
+
form.innerHTML = `
|
|
1756
|
+
<input type="hidden" name="authenticity_token" value="${csrfToken}">
|
|
1757
|
+
<input type="hidden" name="query" value="${this.escapeHtml(query)}">
|
|
1758
|
+
<input type="hidden" name="format" value="array">
|
|
1759
|
+
<input type="hidden" name="pretty_print" value="true">
|
|
1760
|
+
`
|
|
1761
|
+
document.body.appendChild(form)
|
|
1762
|
+
form.submit()
|
|
1763
|
+
document.body.removeChild(form)
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1260
1767
|
// Initialize Stimulus
|
|
1261
1768
|
window.Stimulus = Application.start()
|
|
1262
1769
|
Stimulus.register("query-executor", QueryExecutorController)
|
|
1263
1770
|
Stimulus.register("table-selector", TableSelectorController)
|
|
1771
|
+
Stimulus.register("saved-queries", SavedQueriesController)
|
|
1264
1772
|
</script>
|
|
1265
1773
|
</head>
|
|
1266
1774
|
|
|
@@ -44,10 +44,19 @@
|
|
|
44
44
|
<div class="col-md-10" style="background-color: var(--main-bg); min-height: 100vh;">
|
|
45
45
|
<div class="index-main-content">
|
|
46
46
|
<div class="content-header">
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
48
|
+
<div>
|
|
49
|
+
<h1 class="page-title mb-0">
|
|
50
|
+
<i class="fas fa-server"></i>
|
|
51
|
+
Your Databases
|
|
52
|
+
</h1>
|
|
53
|
+
</div>
|
|
54
|
+
<div>
|
|
55
|
+
<%= link_to worksheet_path, class: "btn btn-success" do %>
|
|
56
|
+
<i class="fas fa-file-code"></i> SQL Worksheet
|
|
57
|
+
<% end %>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
51
60
|
<p class="page-subtitle">Select a database to explore and manage</p>
|
|
52
61
|
</div>
|
|
53
62
|
|
|
@@ -74,6 +83,46 @@
|
|
|
74
83
|
</div>
|
|
75
84
|
<% end %>
|
|
76
85
|
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Saved Queries Section -->
|
|
88
|
+
<% if @saved_queries.any? %>
|
|
89
|
+
<div class="mt-5">
|
|
90
|
+
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
91
|
+
<h2 class="h4">
|
|
92
|
+
<i class="fas fa-bookmark"></i> Recent Saved Queries
|
|
93
|
+
</h2>
|
|
94
|
+
<%= link_to worksheet_path, class: "btn btn-sm btn-outline-primary" do %>
|
|
95
|
+
<i class="fas fa-eye"></i> View All
|
|
96
|
+
<% end %>
|
|
97
|
+
</div>
|
|
98
|
+
<div class="saved-queries-grid">
|
|
99
|
+
<% @saved_queries.each do |query| %>
|
|
100
|
+
<div class="saved-query-card">
|
|
101
|
+
<div class="saved-query-header">
|
|
102
|
+
<h5 class="saved-query-title">
|
|
103
|
+
<i class="fas fa-code"></i> <%= query.name %>
|
|
104
|
+
</h5>
|
|
105
|
+
<% if query.database_name.present? %>
|
|
106
|
+
<span class="badge bg-info"><%= query.database_name %></span>
|
|
107
|
+
<% end %>
|
|
108
|
+
</div>
|
|
109
|
+
<p class="saved-query-description"><%= query.description %></p>
|
|
110
|
+
<div class="saved-query-sql">
|
|
111
|
+
<code><%= truncate(query.query, length: 100) %></code>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="saved-query-meta">
|
|
114
|
+
<small class="text-muted">
|
|
115
|
+
<i class="fas fa-clock"></i> <%= time_ago_in_words(query.created_at) %> ago
|
|
116
|
+
</small>
|
|
117
|
+
<%= link_to worksheet_path, class: "btn btn-sm btn-outline-success" do %>
|
|
118
|
+
<i class="fas fa-play"></i> Load in Worksheet
|
|
119
|
+
<% end %>
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<% end %>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<% end %>
|
|
77
126
|
<% else %>
|
|
78
127
|
<div class="empty-state-modern">
|
|
79
128
|
<div class="empty-state-icon">
|