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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4be508c9964d5d3bc9e646bc297639297b468a52a3c38e9bae24d91579c63c0
4
- data.tar.gz: 403c530bbcaacfc2d7eca3181a32386971d5b1bf5bd7348ce8e7e70668cc08c6
3
+ metadata.gz: 9b1922be7426fe6320e16ad206a78512a2d4c70ab16b5193def19470ce5db413
4
+ data.tar.gz: 2d32502f6e3eb01188b9bc9091bc80c3e5dc1bb2bbc10de717c9c3c74c4730e7
5
5
  SHA512:
6
- metadata.gz: 85079f70645f372357282f1c5c506b261b8775f8d81e6b4284568ba1a8a1f294fcf789dc8162636815a20183b66b8b0056a5747b2574d94f9eec8f816fac6464
7
- data.tar.gz: c6b9832a41c012ab642d1981dfce35f13554fac43271336e818c1778b106452432900f48953a33a93f1802e7f0aca8f3d5919c667f5cce5b03c80ef5b1122bbf
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
- ![SQLite Dashboard Screenshot](https://via.placeholder.com/800x400)
8
+ ## Screenshots
9
+
10
+ ### Database Selection
11
+ ![Database Dashboard](screenshots/dashboard.png)
12
+
13
+ ### Query Interface & Results
14
+ ![Query Details](screenshots/details.png)
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 [Your Name](https://github.com/yourusername)
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
- - 🐛 [Report bugs](https://github.com/yourusername/sqlite_dashboard/issues)
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,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