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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b1922be7426fe6320e16ad206a78512a2d4c70ab16b5193def19470ce5db413
|
|
4
|
+
data.tar.gz: 2d32502f6e3eb01188b9bc9091bc80c3e5dc1bb2bbc10de717c9c3c74c4730e7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50368ebf1eaff776214f1c2602a1372b4d33d3ea660674a001d982091383562fc0eb0084ff46dfa11bbe7c5e61c83f215b96f678c22817ae827a8304abb3bd81
|
|
7
|
+
data.tar.gz: 4342fdd6a7043c89053ef5b004ca1726d1a4380d0f0cd93a81eff40a6fe347d2e7cbecd45cee1f056a04bddd248dc5a06d771a39e3f751dd88663917cf6749a4
|
data/README.md
CHANGED
|
@@ -5,7 +5,13 @@
|
|
|
5
5
|
|
|
6
6
|
A beautiful, feature-rich SQLite database browser and query interface for Rails applications. Mount it as an engine in your Rails app to inspect and query your SQLite databases through a clean, modern interface.
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## Screenshots
|
|
9
|
+
|
|
10
|
+
### Database Selection
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
### Query Interface & Results
|
|
14
|
+

|
|
9
15
|
|
|
10
16
|
## Features
|
|
11
17
|
|
|
@@ -416,7 +422,7 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
416
422
|
|
|
417
423
|
## Credits
|
|
418
424
|
|
|
419
|
-
Created by [
|
|
425
|
+
Created by [Giovanni Panasiti](https://github.com/giovapanasiti)
|
|
420
426
|
|
|
421
427
|
Special thanks to:
|
|
422
428
|
- [CodeMirror](https://codemirror.net/) for the SQL editor
|
|
@@ -425,9 +431,7 @@ Special thanks to:
|
|
|
425
431
|
|
|
426
432
|
## Support
|
|
427
433
|
|
|
428
|
-
-
|
|
429
|
-
- 💡 [Request features](https://github.com/yourusername/sqlite_dashboard/issues)
|
|
430
|
-
- 📧 [Email support](mailto:your.email@example.com)
|
|
434
|
+
- 📧 [Email support](mailto:giova.panasiti@hey.com)
|
|
431
435
|
|
|
432
436
|
---
|
|
433
437
|
|
|
@@ -5,15 +5,22 @@ require 'json'
|
|
|
5
5
|
module SqliteDashboard
|
|
6
6
|
class DatabasesController < ApplicationController
|
|
7
7
|
before_action :set_database, only: [:show, :execute_query, :export_csv, :export_json, :tables, :table_schema]
|
|
8
|
+
before_action :set_saved_query, only: [:destroy_saved_query]
|
|
8
9
|
|
|
9
10
|
def index
|
|
10
11
|
@databases = SqliteDashboard.configuration.databases
|
|
12
|
+
@saved_queries = SavedQuery.recent.limit(10)
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def show
|
|
14
16
|
@tables = fetch_tables
|
|
15
17
|
end
|
|
16
18
|
|
|
19
|
+
def worksheet
|
|
20
|
+
@databases = SqliteDashboard.configuration.databases
|
|
21
|
+
@saved_queries = SavedQuery.recent.limit(20)
|
|
22
|
+
end
|
|
23
|
+
|
|
17
24
|
def execute_query
|
|
18
25
|
@query = params[:query]
|
|
19
26
|
|
|
@@ -166,8 +173,52 @@ module SqliteDashboard
|
|
|
166
173
|
end
|
|
167
174
|
end
|
|
168
175
|
|
|
176
|
+
# Saved Queries actions
|
|
177
|
+
def saved_queries
|
|
178
|
+
@saved_queries = SavedQuery.recent
|
|
179
|
+
database_name = params[:database_name]
|
|
180
|
+
@saved_queries = @saved_queries.for_database(database_name) if database_name.present?
|
|
181
|
+
|
|
182
|
+
render json: @saved_queries
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def create_saved_query
|
|
186
|
+
@saved_query = SavedQuery.new(saved_query_params)
|
|
187
|
+
|
|
188
|
+
if @saved_query.save
|
|
189
|
+
render json: @saved_query, status: :created
|
|
190
|
+
else
|
|
191
|
+
render json: { error: @saved_query.errors.full_messages.join(", ") }, status: :unprocessable_entity
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def show_saved_query
|
|
196
|
+
@saved_query = SavedQuery.find(params[:id])
|
|
197
|
+
render json: @saved_query
|
|
198
|
+
rescue ActiveRecord::RecordNotFound
|
|
199
|
+
render json: { error: "Saved query not found" }, status: :not_found
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def destroy_saved_query
|
|
203
|
+
if @saved_query.destroy
|
|
204
|
+
render json: { message: "Query deleted successfully" }
|
|
205
|
+
else
|
|
206
|
+
render json: { error: "Failed to delete query" }, status: :unprocessable_entity
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
169
210
|
private
|
|
170
211
|
|
|
212
|
+
def set_saved_query
|
|
213
|
+
@saved_query = SavedQuery.find(params[:id])
|
|
214
|
+
rescue ActiveRecord::RecordNotFound
|
|
215
|
+
render json: { error: "Saved query not found" }, status: :not_found
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def saved_query_params
|
|
219
|
+
params.require(:saved_query).permit(:name, :query, :database_name, :description)
|
|
220
|
+
end
|
|
221
|
+
|
|
171
222
|
def set_database
|
|
172
223
|
@database = SqliteDashboard.configuration.databases.find { |db| db[:id] == params[:id].to_i }
|
|
173
224
|
redirect_to databases_path, alert: "Database not found" unless @database
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
|
|
3
|
+
export default class extends Controller {
|
|
4
|
+
static targets = [
|
|
5
|
+
"queryInput",
|
|
6
|
+
"queryName",
|
|
7
|
+
"queryDescription",
|
|
8
|
+
"databaseSelector",
|
|
9
|
+
"list",
|
|
10
|
+
"saveError"
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
connect() {
|
|
14
|
+
console.log("SavedQueries controller connected")
|
|
15
|
+
this.loadSavedQueries()
|
|
16
|
+
this.initializeCodeMirror()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
initializeCodeMirror() {
|
|
20
|
+
if (typeof CodeMirror !== 'undefined' && this.hasQueryInputTarget) {
|
|
21
|
+
this.editor = CodeMirror.fromTextArea(this.queryInputTarget, {
|
|
22
|
+
mode: 'text/x-sql',
|
|
23
|
+
theme: 'default',
|
|
24
|
+
lineNumbers: true,
|
|
25
|
+
lineWrapping: true,
|
|
26
|
+
autoCloseBrackets: true,
|
|
27
|
+
matchBrackets: true,
|
|
28
|
+
indentWithTabs: true,
|
|
29
|
+
smartIndent: true,
|
|
30
|
+
extraKeys: {
|
|
31
|
+
"Ctrl-Enter": () => this.execute(),
|
|
32
|
+
"Cmd-Enter": () => this.execute(),
|
|
33
|
+
"Ctrl-Space": "autocomplete"
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async loadSavedQueries() {
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch('/sqlite_dashboard/saved_queries')
|
|
42
|
+
const queries = await response.json()
|
|
43
|
+
this.renderSavedQueries(queries)
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error('Error loading saved queries:', error)
|
|
46
|
+
if (this.hasListTarget) {
|
|
47
|
+
this.listTarget.innerHTML = `
|
|
48
|
+
<div class="text-danger small text-center py-3">
|
|
49
|
+
<i class="fas fa-exclamation-triangle"></i> Error loading queries
|
|
50
|
+
</div>
|
|
51
|
+
`
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
renderSavedQueries(queries) {
|
|
57
|
+
if (!this.hasListTarget) return
|
|
58
|
+
|
|
59
|
+
if (queries.length === 0) {
|
|
60
|
+
this.listTarget.innerHTML = `
|
|
61
|
+
<div class="text-muted small text-center py-3">
|
|
62
|
+
No saved queries
|
|
63
|
+
</div>
|
|
64
|
+
`
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const html = queries.map(query => `
|
|
69
|
+
<div class="saved-query-item" data-query-id="${query.id}">
|
|
70
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
71
|
+
<div class="flex-grow-1" style="cursor: pointer;" data-action="click->saved-queries#loadQuery" data-query='${JSON.stringify(query)}'>
|
|
72
|
+
<div class="fw-bold small">${this.escapeHtml(query.name)}</div>
|
|
73
|
+
${query.description ? `<div class="text-muted" style="font-size: 0.75rem;">${this.escapeHtml(query.description)}</div>` : ''}
|
|
74
|
+
${query.database_name ? `<div class="text-info" style="font-size: 0.7rem;"><i class="fas fa-database"></i> ${this.escapeHtml(query.database_name)}</div>` : ''}
|
|
75
|
+
</div>
|
|
76
|
+
<button class="btn btn-sm btn-link text-danger p-0" data-action="click->saved-queries#deleteQuery" data-query-id="${query.id}">
|
|
77
|
+
<i class="fas fa-trash"></i>
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
`).join('')
|
|
82
|
+
|
|
83
|
+
this.listTarget.innerHTML = html
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
escapeHtml(text) {
|
|
87
|
+
const div = document.createElement('div')
|
|
88
|
+
div.textContent = text
|
|
89
|
+
return div.innerHTML
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async execute() {
|
|
93
|
+
const databaseId = this.databaseSelectorTarget.value
|
|
94
|
+
if (!databaseId) {
|
|
95
|
+
alert('Please select a database first')
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
100
|
+
if (!query.trim()) {
|
|
101
|
+
alert('Please enter a query')
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const response = await fetch(`/sqlite_dashboard/databases/${databaseId}/execute_query`, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: {
|
|
109
|
+
'Content-Type': 'application/json',
|
|
110
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
|
111
|
+
},
|
|
112
|
+
body: JSON.stringify({ query })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
const data = await response.json()
|
|
116
|
+
|
|
117
|
+
if (data.error) {
|
|
118
|
+
this.renderError(data.error)
|
|
119
|
+
} else {
|
|
120
|
+
this.renderResults(data, query, databaseId)
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error executing query:', error)
|
|
124
|
+
this.renderError(error.message)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
renderResults(data, query, databaseId) {
|
|
129
|
+
const resultsContainer = document.getElementById('worksheet-results')
|
|
130
|
+
|
|
131
|
+
if (data.message) {
|
|
132
|
+
resultsContainer.innerHTML = `
|
|
133
|
+
<div class="alert alert-success">
|
|
134
|
+
<i class="fas fa-check-circle"></i> ${data.message}
|
|
135
|
+
</div>
|
|
136
|
+
`
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { columns, rows } = data
|
|
141
|
+
|
|
142
|
+
let html = `
|
|
143
|
+
<div class="results-header">
|
|
144
|
+
<h6>Query Results</h6>
|
|
145
|
+
<div class="text-muted small">${rows.length} row(s) returned</div>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="table-responsive">
|
|
148
|
+
<table class="table table-striped table-hover">
|
|
149
|
+
<thead>
|
|
150
|
+
<tr>
|
|
151
|
+
${columns.map(col => `<th>${this.escapeHtml(col)}</th>`).join('')}
|
|
152
|
+
</tr>
|
|
153
|
+
</thead>
|
|
154
|
+
<tbody>
|
|
155
|
+
${rows.map(row => `
|
|
156
|
+
<tr>
|
|
157
|
+
${row.map(cell => `<td>${cell !== null ? this.escapeHtml(String(cell)) : '<span class="text-muted">NULL</span>'}</td>`).join('')}
|
|
158
|
+
</tr>
|
|
159
|
+
`).join('')}
|
|
160
|
+
</tbody>
|
|
161
|
+
</table>
|
|
162
|
+
</div>
|
|
163
|
+
`
|
|
164
|
+
|
|
165
|
+
resultsContainer.innerHTML = html
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
renderError(error) {
|
|
169
|
+
const resultsContainer = document.getElementById('worksheet-results')
|
|
170
|
+
resultsContainer.innerHTML = `
|
|
171
|
+
<div class="alert alert-danger">
|
|
172
|
+
<i class="fas fa-exclamation-circle"></i> <strong>Error:</strong> ${this.escapeHtml(error)}
|
|
173
|
+
</div>
|
|
174
|
+
`
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
clear() {
|
|
178
|
+
if (this.editor) {
|
|
179
|
+
this.editor.setValue('')
|
|
180
|
+
} else {
|
|
181
|
+
this.queryInputTarget.value = ''
|
|
182
|
+
}
|
|
183
|
+
document.getElementById('worksheet-results').innerHTML = `
|
|
184
|
+
<div class="text-muted text-center py-5">
|
|
185
|
+
<i class="fas fa-database fa-3x mb-3"></i>
|
|
186
|
+
<p>Select a database and execute a query to see results</p>
|
|
187
|
+
</div>
|
|
188
|
+
`
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async saveQuery() {
|
|
192
|
+
const name = this.queryNameTarget.value.trim()
|
|
193
|
+
const description = this.queryDescriptionTarget.value.trim()
|
|
194
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
195
|
+
const databaseId = this.databaseSelectorTarget.value
|
|
196
|
+
|
|
197
|
+
// Get database name from selector
|
|
198
|
+
const databaseName = databaseId ?
|
|
199
|
+
this.databaseSelectorTarget.options[this.databaseSelectorTarget.selectedIndex].text :
|
|
200
|
+
null
|
|
201
|
+
|
|
202
|
+
if (!name) {
|
|
203
|
+
this.showSaveError('Query name is required')
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (!query.trim()) {
|
|
208
|
+
this.showSaveError('Query cannot be empty')
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
const response = await fetch('/sqlite_dashboard/saved_queries', {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: {
|
|
216
|
+
'Content-Type': 'application/json',
|
|
217
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
saved_query: {
|
|
221
|
+
name,
|
|
222
|
+
description,
|
|
223
|
+
query,
|
|
224
|
+
database_name: databaseName
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
const data = await response.json()
|
|
230
|
+
|
|
231
|
+
if (response.ok) {
|
|
232
|
+
// Close modal
|
|
233
|
+
const modal = bootstrap.Modal.getInstance(document.getElementById('saveQueryModal'))
|
|
234
|
+
modal.hide()
|
|
235
|
+
|
|
236
|
+
// Clear form
|
|
237
|
+
this.queryNameTarget.value = ''
|
|
238
|
+
this.queryDescriptionTarget.value = ''
|
|
239
|
+
this.hideSaveError()
|
|
240
|
+
|
|
241
|
+
// Reload saved queries
|
|
242
|
+
await this.loadSavedQueries()
|
|
243
|
+
|
|
244
|
+
// Show success message
|
|
245
|
+
alert('Query saved successfully!')
|
|
246
|
+
} else {
|
|
247
|
+
this.showSaveError(data.error || 'Failed to save query')
|
|
248
|
+
}
|
|
249
|
+
} catch (error) {
|
|
250
|
+
console.error('Error saving query:', error)
|
|
251
|
+
this.showSaveError(error.message)
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
showSaveError(message) {
|
|
256
|
+
if (this.hasSaveErrorTarget) {
|
|
257
|
+
this.saveErrorTarget.textContent = message
|
|
258
|
+
this.saveErrorTarget.classList.remove('d-none')
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
hideSaveError() {
|
|
263
|
+
if (this.hasSaveErrorTarget) {
|
|
264
|
+
this.saveErrorTarget.classList.add('d-none')
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
loadQuery(event) {
|
|
269
|
+
const queryData = JSON.parse(event.currentTarget.dataset.query)
|
|
270
|
+
|
|
271
|
+
if (this.editor) {
|
|
272
|
+
this.editor.setValue(queryData.query)
|
|
273
|
+
} else {
|
|
274
|
+
this.queryInputTarget.value = queryData.query
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// If database_name is present, try to select it
|
|
278
|
+
if (queryData.database_name && this.hasDatabaseSelectorTarget) {
|
|
279
|
+
const option = Array.from(this.databaseSelectorTarget.options)
|
|
280
|
+
.find(opt => opt.text === queryData.database_name)
|
|
281
|
+
if (option) {
|
|
282
|
+
this.databaseSelectorTarget.value = option.value
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async deleteQuery(event) {
|
|
288
|
+
event.stopPropagation()
|
|
289
|
+
const queryId = event.currentTarget.dataset.queryId
|
|
290
|
+
|
|
291
|
+
if (!confirm('Are you sure you want to delete this saved query?')) {
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const response = await fetch(`/sqlite_dashboard/saved_queries/${queryId}`, {
|
|
297
|
+
method: 'DELETE',
|
|
298
|
+
headers: {
|
|
299
|
+
'X-CSRF-Token': document.querySelector('[name="csrf-token"]').content
|
|
300
|
+
}
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
if (response.ok) {
|
|
304
|
+
await this.loadSavedQueries()
|
|
305
|
+
} else {
|
|
306
|
+
const data = await response.json()
|
|
307
|
+
alert(data.error || 'Failed to delete query')
|
|
308
|
+
}
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error('Error deleting query:', error)
|
|
311
|
+
alert('Failed to delete query')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
refresh() {
|
|
316
|
+
this.loadSavedQueries()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
async exportCSV() {
|
|
320
|
+
const databaseId = this.databaseSelectorTarget.value
|
|
321
|
+
if (!databaseId) {
|
|
322
|
+
alert('Please select a database first')
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
327
|
+
if (!query.trim()) {
|
|
328
|
+
alert('Please enter a query')
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// For CSV export, we'll make a form submission since we need to download a file
|
|
333
|
+
const form = document.createElement('form')
|
|
334
|
+
form.method = 'POST'
|
|
335
|
+
form.action = `/sqlite_dashboard/databases/${databaseId}/export_csv`
|
|
336
|
+
|
|
337
|
+
const csrfToken = document.querySelector('[name="csrf-token"]').content
|
|
338
|
+
form.innerHTML = `
|
|
339
|
+
<input type="hidden" name="authenticity_token" value="${csrfToken}">
|
|
340
|
+
<input type="hidden" name="query" value="${this.escapeHtml(query)}">
|
|
341
|
+
<input type="hidden" name="include_headers" value="true">
|
|
342
|
+
`
|
|
343
|
+
|
|
344
|
+
document.body.appendChild(form)
|
|
345
|
+
form.submit()
|
|
346
|
+
document.body.removeChild(form)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async exportJSON() {
|
|
350
|
+
const databaseId = this.databaseSelectorTarget.value
|
|
351
|
+
if (!databaseId) {
|
|
352
|
+
alert('Please select a database first')
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const query = this.editor ? this.editor.getValue() : this.queryInputTarget.value
|
|
357
|
+
if (!query.trim()) {
|
|
358
|
+
alert('Please enter a query')
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// For JSON export, we'll make a form submission since we need to download a file
|
|
363
|
+
const form = document.createElement('form')
|
|
364
|
+
form.method = 'POST'
|
|
365
|
+
form.action = `/sqlite_dashboard/databases/${databaseId}/export_json`
|
|
366
|
+
|
|
367
|
+
const csrfToken = document.querySelector('[name="csrf-token"]').content
|
|
368
|
+
form.innerHTML = `
|
|
369
|
+
<input type="hidden" name="authenticity_token" value="${csrfToken}">
|
|
370
|
+
<input type="hidden" name="query" value="${this.escapeHtml(query)}">
|
|
371
|
+
<input type="hidden" name="format" value="array">
|
|
372
|
+
<input type="hidden" name="pretty_print" value="true">
|
|
373
|
+
`
|
|
374
|
+
|
|
375
|
+
document.body.appendChild(form)
|
|
376
|
+
form.submit()
|
|
377
|
+
document.body.removeChild(form)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SqliteDashboard
|
|
4
|
+
class SavedQuery < ApplicationRecord
|
|
5
|
+
self.table_name = "dashboard_saved_queries"
|
|
6
|
+
|
|
7
|
+
validates :name, presence: true, uniqueness: true
|
|
8
|
+
validates :query, presence: true
|
|
9
|
+
validates :description, presence: true
|
|
10
|
+
|
|
11
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
12
|
+
scope :for_database, ->(database_name) { where(database_name: database_name) }
|
|
13
|
+
end
|
|
14
|
+
end
|