sqlite_dashboard 1.0.0 → 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.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SqliteDashboard
4
+ class ApplicationRecord < ActiveRecord::Base
5
+ self.abstract_class = true
6
+ end
7
+ end
@@ -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