dbviewer 0.6.7 → 0.6.8
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/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +553 -0
- data/app/assets/javascripts/dbviewer/home.js +287 -0
- data/app/assets/javascripts/dbviewer/layout.js +194 -0
- data/app/assets/javascripts/dbviewer/query.js +277 -0
- data/app/assets/javascripts/dbviewer/table.js +1563 -0
- data/app/assets/stylesheets/dbviewer/application.css +1460 -21
- data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +181 -0
- data/app/assets/stylesheets/dbviewer/home.css +229 -0
- data/app/assets/stylesheets/dbviewer/logs.css +64 -0
- data/app/assets/stylesheets/dbviewer/query.css +171 -0
- data/app/assets/stylesheets/dbviewer/table.css +1144 -0
- data/app/views/dbviewer/connections/index.html.erb +0 -30
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +14 -713
- data/app/views/dbviewer/home/index.html.erb +9 -499
- data/app/views/dbviewer/logs/index.html.erb +5 -220
- data/app/views/dbviewer/tables/index.html.erb +0 -65
- data/app/views/dbviewer/tables/query.html.erb +129 -565
- data/app/views/dbviewer/tables/show.html.erb +4 -2429
- data/app/views/layouts/dbviewer/application.html.erb +13 -1544
- data/lib/dbviewer/version.rb +1 -1
- metadata +12 -7
- data/app/assets/javascripts/dbviewer/connections.js +0 -70
- data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
- data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
- data/app/views/dbviewer/connections/new.html.erb +0 -79
- data/app/views/dbviewer/tables/mini_erd.html.erb +0 -517
@@ -4,183 +4,10 @@
|
|
4
4
|
|
5
5
|
<% content_for :head do %>
|
6
6
|
<link href="https://cdn.jsdelivr.net/npm/vscode-codicons@0.0.17/dist/codicon.min.css" rel="stylesheet">
|
7
|
-
|
8
|
-
|
9
|
-
#monaco-editor {
|
10
|
-
margin-bottom: 1rem;
|
11
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
12
|
-
}
|
13
|
-
|
14
|
-
.monaco-editor-container {
|
15
|
-
border: 1px solid #ced4da;
|
16
|
-
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
17
|
-
}
|
18
|
-
|
19
|
-
[data-bs-theme="dark"] .monaco-editor-container {
|
20
|
-
border: 1px solid #495057;
|
21
|
-
}
|
22
|
-
|
23
|
-
[data-bs-theme="dark"] #monaco-editor {
|
24
|
-
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
|
25
|
-
}
|
26
|
-
|
27
|
-
.example-queries {
|
28
|
-
display: flex;
|
29
|
-
flex-wrap: wrap;
|
30
|
-
gap: 5px;
|
31
|
-
margin-top: 8px;
|
32
|
-
}
|
33
|
-
|
34
|
-
.example-query {
|
35
|
-
display: inline-block;
|
36
|
-
transition: all 0.2s ease;
|
37
|
-
cursor: pointer;
|
38
|
-
font-size: 0.85rem;
|
39
|
-
white-space: nowrap;
|
40
|
-
overflow: hidden;
|
41
|
-
text-overflow: ellipsis;
|
42
|
-
max-width: 100%;
|
43
|
-
}
|
44
|
-
|
45
|
-
[data-bs-theme="light"] .example-query {
|
46
|
-
border-color: #ced4da;
|
47
|
-
}
|
48
|
-
|
49
|
-
[data-bs-theme="dark"] .example-query {
|
50
|
-
border-color: #495057;
|
51
|
-
color: #f8f9fa;
|
52
|
-
}
|
53
|
-
|
54
|
-
/* Result table styling */
|
55
|
-
.results-table {
|
56
|
-
border-collapse: collapse;
|
57
|
-
}
|
58
|
-
|
59
|
-
[data-bs-theme="dark"] .results-table {
|
60
|
-
border-color: #495057;
|
61
|
-
}
|
62
|
-
|
63
|
-
.example-query:hover {
|
64
|
-
background-color: #0d6efd;
|
65
|
-
color: white;
|
66
|
-
border-color: #0d6efd;
|
67
|
-
}
|
68
|
-
|
69
|
-
/* Keyboard shortcut helper */
|
70
|
-
.keyboard-hint {
|
71
|
-
font-size: 0.8rem;
|
72
|
-
margin-left: 8px;
|
73
|
-
opacity: 0.7;
|
74
|
-
}
|
75
|
-
|
76
|
-
[data-bs-theme="light"] .shortcut-hints {
|
77
|
-
color: #6c757d;
|
78
|
-
}
|
79
|
-
|
80
|
-
[data-bs-theme="dark"] .shortcut-hints {
|
81
|
-
color: #adb5bd;
|
82
|
-
}
|
83
|
-
|
84
|
-
/* Monaco status bar */
|
85
|
-
.monaco-status-bar {
|
86
|
-
display: flex;
|
87
|
-
justify-content: space-between;
|
88
|
-
padding: 3px 8px;
|
89
|
-
font-size: 0.75rem;
|
90
|
-
border-top: none;
|
91
|
-
border-bottom-left-radius: 4px;
|
92
|
-
border-bottom-right-radius: 4px;
|
93
|
-
}
|
94
|
-
|
95
|
-
[data-bs-theme="light"] .monaco-status-bar {
|
96
|
-
background-color: #f8f9fa;
|
97
|
-
border: 1px solid #ced4da;
|
98
|
-
color: #6c757d;
|
99
|
-
}
|
100
|
-
|
101
|
-
[data-bs-theme="dark"] .monaco-status-bar {
|
102
|
-
background-color: #343a40;
|
103
|
-
border: 1px solid #495057;
|
104
|
-
color: #adb5bd;
|
105
|
-
}
|
106
|
-
|
107
|
-
.monaco-status-bar .column-info {
|
108
|
-
font-weight: 500;
|
109
|
-
}
|
110
|
-
|
111
|
-
/* Table structure styles */
|
112
|
-
#tableStructureHeader .btn-link {
|
113
|
-
font-weight: 500;
|
114
|
-
display: flex;
|
115
|
-
align-items: center;
|
116
|
-
width: 100%;
|
117
|
-
text-align: left;
|
118
|
-
}
|
119
|
-
|
120
|
-
[data-bs-theme="light"] #tableStructureHeader .btn-link {
|
121
|
-
color: #212529;
|
122
|
-
}
|
123
|
-
|
124
|
-
[data-bs-theme="dark"] #tableStructureHeader .btn-link {
|
125
|
-
color: #f8f9fa;
|
126
|
-
}
|
127
|
-
|
128
|
-
[data-bs-theme="light"] .table-columns-count {
|
129
|
-
color: #6c757d;
|
130
|
-
}
|
131
|
-
|
132
|
-
[data-bs-theme="dark"] .table-columns-count {
|
133
|
-
color: #adb5bd;
|
134
|
-
}
|
135
|
-
|
136
|
-
#tableStructureHeader .btn-link:hover,
|
137
|
-
#tableStructureHeader .btn-link:focus {
|
138
|
-
text-decoration: none;
|
139
|
-
color: #0d6efd;
|
140
|
-
}
|
141
|
-
|
142
|
-
#tableStructureHeader .btn-link i {
|
143
|
-
transition: transform 0.2s ease-in-out;
|
144
|
-
}
|
145
|
-
|
146
|
-
/* Table style overrides for query page */
|
147
|
-
[data-bs-theme="dark"] .table-sm th,
|
148
|
-
[data-bs-theme="dark"] .table-sm td {
|
149
|
-
border-color: #495057;
|
150
|
-
}
|
151
|
-
|
152
|
-
/* Results card styling */
|
153
|
-
[data-bs-theme="dark"] .card-header h5 {
|
154
|
-
color: #f8f9fa;
|
155
|
-
}
|
156
|
-
|
157
|
-
/* Alert styling for dark mode */
|
158
|
-
[data-bs-theme="dark"] .alert-warning {
|
159
|
-
background-color: rgba(255, 193, 7, 0.15);
|
160
|
-
border-color: rgba(255, 193, 7, 0.4);
|
161
|
-
color: #ffc107;
|
162
|
-
}
|
163
|
-
|
164
|
-
[data-bs-theme="dark"] .alert-danger {
|
165
|
-
background-color: rgba(220, 53, 69, 0.15);
|
166
|
-
border-color: rgba(220, 53, 69, 0.4);
|
167
|
-
color: #f8d7da;
|
168
|
-
}
|
169
|
-
|
170
|
-
/* Make headings stand out in dark mode */
|
171
|
-
[data-bs-theme="dark"] h1,
|
172
|
-
[data-bs-theme="dark"] h2,
|
173
|
-
[data-bs-theme="dark"] h3,
|
174
|
-
[data-bs-theme="dark"] h4,
|
175
|
-
[data-bs-theme="dark"] h5,
|
176
|
-
[data-bs-theme="dark"] h6 {
|
177
|
-
color: #f8f9fa;
|
178
|
-
}
|
179
|
-
</style>
|
7
|
+
<%= stylesheet_link_tag "dbviewer/query", "data-turbo-track": "reload" %>
|
8
|
+
<%= javascript_include_tag "dbviewer/query", "data-turbo-track": "reload", type: :module %>
|
180
9
|
<% end %>
|
181
10
|
|
182
|
-
<% content_for :sidebar_active do %>active<% end %>
|
183
|
-
|
184
11
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
185
12
|
<h1>Query: <%= @table_name %></h1>
|
186
13
|
<div>
|
@@ -189,408 +16,145 @@
|
|
189
16
|
<% end %>
|
190
17
|
</div>
|
191
18
|
</div>
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
</div>
|
229
|
-
</div>
|
230
|
-
<div>
|
231
|
-
<%= form.submit "Run Query", class: "btn btn-primary" %>
|
232
|
-
<span class="keyboard-hint d-none d-md-inline">(or press Cmd+Enter / Ctrl+Enter)</span>
|
233
|
-
<div class="small mt-2 d-none d-md-block shortcut-hints">
|
234
|
-
<strong>Shortcuts:</strong>
|
235
|
-
<span class="me-2">Cmd+Alt+T: Toggle table structure</span>
|
236
|
-
<span class="me-2">Cmd+Alt+S: Insert SELECT</span>
|
237
|
-
<span>Cmd+Alt+W: Insert WHERE</span>
|
238
|
-
</div>
|
19
|
+
|
20
|
+
<% if flash[:warning].present? %>
|
21
|
+
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
22
|
+
<%= flash[:warning] %>
|
23
|
+
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
24
|
+
</div>
|
25
|
+
<% end %>
|
26
|
+
|
27
|
+
<div class="card mb-4">
|
28
|
+
<div class="card-header">
|
29
|
+
<h5>SQL Query (Read-Only)</h5>
|
30
|
+
</div>
|
31
|
+
<div class="card-body">
|
32
|
+
<%= form_with url: query_table_path(@table_name), method: :post, local: true, id: "sql-query-form" do |form| %>
|
33
|
+
<div class="mb-3">
|
34
|
+
<div id="monaco-editor" class="monaco-editor-container" style="min-height: 200px; border-radius: 4px; margin-bottom: 0rem;"
|
35
|
+
data-initial-query="<%= CGI.escapeHTML(@query.to_s) %>"></div>
|
36
|
+
<%= form.hidden_field :query, id: "query-input", value: @query.to_s %>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<div class="d-flex justify-content-between align-items-start">
|
40
|
+
<div class="form-text">
|
41
|
+
<strong>Examples:</strong><br>
|
42
|
+
<div class="example-queries">
|
43
|
+
<code class="example-query btn btn-sm btn-outline-secondary mb-1">SELECT * FROM <%= @table_name %> LIMIT 100</code>
|
44
|
+
<code class="example-query btn btn-sm btn-outline-secondary mb-1">SELECT
|
45
|
+
<%
|
46
|
+
# Display first 3 columns or all if less than 3
|
47
|
+
display_cols = @columns.present? ? @columns.first(3).map { |c| c[:name] }.join(", ") : "column1, column2"
|
48
|
+
# Get first non-ID column for WHERE example if available
|
49
|
+
where_col = @columns.present? ? (@columns.find { |c| !c[:name].to_s.downcase.include?("id") } || @columns.first)[:name] : "column_name"
|
50
|
+
# Get a numeric column for aggregation if available
|
51
|
+
num_col = @columns.present? ? (@columns.find { |c| c[:type].to_s.downcase.include?("int") || c[:type].to_s.downcase.include?("num") } || @columns.first)[:name] : "id"
|
52
|
+
%>
|
53
|
+
<%= display_cols %> FROM <%= @table_name %> WHERE <%= where_col %> = 'value'</code>
|
54
|
+
<code class="example-query btn btn-sm btn-outline-secondary mb-1">SELECT COUNT(*) FROM <%= @table_name %> GROUP BY <%= num_col %></code>
|
239
55
|
</div>
|
240
56
|
</div>
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
Table Structure Reference
|
250
|
-
<small class="table-columns-count ms-2">(<%= @columns.present? ? @columns.size : 0 %> columns)</small>
|
251
|
-
</button>
|
252
|
-
</h6>
|
253
|
-
</div>
|
254
|
-
<div id="tableStructureContent" class="collapse" aria-labelledby="tableStructureHeader">
|
255
|
-
<div class="card-body p-2">
|
256
|
-
<% if @columns.present? %>
|
257
|
-
<div class="table-responsive">
|
258
|
-
<table class="table table-sm table-bordered mb-0">
|
259
|
-
<thead>
|
260
|
-
<tr>
|
261
|
-
<th>Column</th>
|
262
|
-
<th>Type</th>
|
263
|
-
</tr>
|
264
|
-
</thead>
|
265
|
-
<tbody>
|
266
|
-
<% @columns.each do |column| %>
|
267
|
-
<tr>
|
268
|
-
<td><code><%= column[:name] %><%= " (PK)" if column[:primary] %></code></td>
|
269
|
-
<td><span class="badge bg-secondary"><%= column[:type] %></span></td>
|
270
|
-
</tr>
|
271
|
-
<% end %>
|
272
|
-
</tbody>
|
273
|
-
</table>
|
57
|
+
<div>
|
58
|
+
<%= form.submit "Run Query", class: "btn btn-primary" %>
|
59
|
+
<span class="keyboard-hint d-none d-md-inline">(or press Cmd+Enter / Ctrl+Enter)</span>
|
60
|
+
<div class="small mt-2 d-none d-md-block shortcut-hints">
|
61
|
+
<strong>Shortcuts:</strong>
|
62
|
+
<span class="me-2">Cmd+Alt+T: Toggle table structure</span>
|
63
|
+
<span class="me-2">Cmd+Alt+S: Insert SELECT</span>
|
64
|
+
<span>Cmd+Alt+W: Insert WHERE</span>
|
274
65
|
</div>
|
275
|
-
|
276
|
-
<p class="mb-0">No column information available.</p>
|
277
|
-
<% end %>
|
66
|
+
</div>
|
278
67
|
</div>
|
279
|
-
|
68
|
+
<% end %>
|
280
69
|
</div>
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
70
|
+
</div>
|
71
|
+
|
72
|
+
<div class="card mb-3">
|
73
|
+
<div class="card-header" id="tableStructureHeader">
|
74
|
+
<h6 class="mb-0">
|
75
|
+
<button class="btn btn-link btn-sm text-decoration-none p-0 collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#tableStructureContent" aria-expanded="false" aria-controls="tableStructureContent">
|
76
|
+
<i class="bi bi-chevron-down me-1"></i>
|
77
|
+
Table Structure Reference
|
78
|
+
<small class="table-columns-count ms-2">(<%= @columns.present? ? @columns.size : 0 %> columns)</small>
|
79
|
+
</button>
|
80
|
+
</h6>
|
81
|
+
</div>
|
82
|
+
<div id="tableStructureContent" class="collapse" aria-labelledby="tableStructureHeader">
|
83
|
+
<div class="card-body p-2">
|
84
|
+
<% if @columns.present? %>
|
296
85
|
<div class="table-responsive">
|
297
|
-
<table class="table table-
|
298
|
-
|
299
|
-
<thead>
|
300
|
-
<tr>
|
301
|
-
<% @records.columns.each do |column_name| %>
|
302
|
-
<th><%= column_name %></th>
|
303
|
-
<% end %>
|
304
|
-
</tr>
|
305
|
-
</thead>
|
306
|
-
<tbody>
|
307
|
-
<% if @records.rows.any? %>
|
308
|
-
<% @records.rows.each do |row| %>
|
309
|
-
<tr>
|
310
|
-
<% row.each do |cell| %>
|
311
|
-
<td><%= format_cell_value(cell) %></td>
|
312
|
-
<% end %>
|
313
|
-
</tr>
|
314
|
-
<% end %>
|
315
|
-
<% else %>
|
316
|
-
<tr>
|
317
|
-
<td colspan="<%= @records.columns.count %>">Query executed successfully, but returned no rows.</td>
|
318
|
-
</tr>
|
319
|
-
<% end %>
|
320
|
-
</tbody>
|
321
|
-
<% else %>
|
86
|
+
<table class="table table-sm table-bordered mb-0">
|
87
|
+
<thead>
|
322
88
|
<tr>
|
323
|
-
<
|
89
|
+
<th>Column</th>
|
90
|
+
<th>Type</th>
|
324
91
|
</tr>
|
325
|
-
|
92
|
+
</thead>
|
93
|
+
<tbody>
|
94
|
+
<% @columns.each do |column| %>
|
95
|
+
<tr>
|
96
|
+
<td><code><%= column[:name] %><%= " (PK)" if column[:primary] %></code></td>
|
97
|
+
<td><span class="badge bg-secondary"><%= column[:type] %></span></td>
|
98
|
+
</tr>
|
99
|
+
<% end %>
|
100
|
+
</tbody>
|
326
101
|
</table>
|
327
102
|
</div>
|
328
|
-
|
103
|
+
<% else %>
|
104
|
+
<p class="mb-0">No column information available.</p>
|
105
|
+
<% end %>
|
329
106
|
</div>
|
330
|
-
|
107
|
+
</div>
|
331
108
|
</div>
|
332
109
|
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
const textarea = document.createElement('textarea');
|
339
|
-
textarea.innerHTML = text;
|
340
|
-
return textarea.value;
|
341
|
-
}
|
342
|
-
|
343
|
-
// Get initial query value from a data attribute to avoid string escaping issues
|
344
|
-
const initialQueryEncoded = document.getElementById('monaco-editor').getAttribute('data-initial-query');
|
345
|
-
const initialQuery = decodeHTMLEntities(initialQueryEncoded);
|
346
|
-
|
347
|
-
// Determine initial theme based on document theme
|
348
|
-
const initialTheme = document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'vs-dark' : 'vs';
|
349
|
-
|
350
|
-
// Initialize Monaco Editor with SQL syntax highlighting
|
351
|
-
const editor = monaco.editor.create(document.getElementById('monaco-editor'), {
|
352
|
-
value: initialQuery || '',
|
353
|
-
language: 'sql',
|
354
|
-
theme: initialTheme,
|
355
|
-
automaticLayout: true, // Resize automatically
|
356
|
-
minimap: { enabled: true },
|
357
|
-
scrollBeyondLastLine: false,
|
358
|
-
lineNumbers: 'on',
|
359
|
-
renderLineHighlight: 'all',
|
360
|
-
tabSize: 2,
|
361
|
-
wordWrap: 'on',
|
362
|
-
formatOnPaste: true,
|
363
|
-
formatOnType: true,
|
364
|
-
autoIndent: 'full',
|
365
|
-
folding: true,
|
366
|
-
glyphMargin: false,
|
367
|
-
suggestOnTriggerCharacters: true,
|
368
|
-
fixedOverflowWidgets: true,
|
369
|
-
quickSuggestions: {
|
370
|
-
other: true,
|
371
|
-
comments: true,
|
372
|
-
strings: true
|
373
|
-
},
|
374
|
-
suggest: {
|
375
|
-
showKeywords: true,
|
376
|
-
showSnippets: true,
|
377
|
-
preview: true,
|
378
|
-
showIcons: true,
|
379
|
-
maxVisibleSuggestions: 12
|
380
|
-
}
|
381
|
-
});
|
382
|
-
|
383
|
-
// Theme change listener
|
384
|
-
document.addEventListener('dbviewerThemeChanged', (event) => {
|
385
|
-
const newTheme = event.detail.theme === 'dark' ? 'vs-dark' : 'vs';
|
386
|
-
monaco.editor.setTheme(newTheme);
|
387
|
-
|
388
|
-
// Update editor container border color
|
389
|
-
const editorContainer = document.querySelector('.monaco-editor-container');
|
390
|
-
if (editorContainer) {
|
391
|
-
editorContainer.style.borderColor = event.detail.theme === 'dark' ? '#495057' : '#ced4da';
|
392
|
-
}
|
393
|
-
|
394
|
-
// Update status bar styling based on theme
|
395
|
-
updateStatusBarTheme(event.detail.theme);
|
396
|
-
|
397
|
-
// Update example query buttons
|
398
|
-
const exampleQueries = document.querySelectorAll('.example-query');
|
399
|
-
exampleQueries.forEach(query => {
|
400
|
-
if (event.detail.theme === 'dark') {
|
401
|
-
query.style.borderColor = '#495057';
|
402
|
-
if (!query.classList.contains('btn-primary')) {
|
403
|
-
query.style.color = '#f8f9fa';
|
404
|
-
}
|
405
|
-
} else {
|
406
|
-
query.style.borderColor = '#ced4da';
|
407
|
-
if (!query.classList.contains('btn-primary')) {
|
408
|
-
query.style.color = '';
|
409
|
-
}
|
410
|
-
}
|
411
|
-
});
|
412
|
-
});
|
413
|
-
|
414
|
-
function updateStatusBarTheme(theme) {
|
415
|
-
const statusBar = document.querySelector('.monaco-status-bar');
|
416
|
-
if (!statusBar) return;
|
417
|
-
|
418
|
-
if (theme === 'dark') {
|
419
|
-
statusBar.style.backgroundColor = '#343a40';
|
420
|
-
statusBar.style.borderColor = '#495057';
|
421
|
-
statusBar.style.color = '#adb5bd';
|
422
|
-
} else {
|
423
|
-
statusBar.style.backgroundColor = '#f8f9fa';
|
424
|
-
statusBar.style.borderColor = '#ced4da';
|
425
|
-
statusBar.style.color = '#6c757d';
|
426
|
-
}
|
427
|
-
}
|
428
|
-
|
429
|
-
// Set up SQL intellisense with table/column completions
|
430
|
-
const tableName = "<%= @table_name %>";
|
431
|
-
const columns = [
|
432
|
-
<% if @columns.present? %>
|
433
|
-
<% @columns.each do |column| %>
|
434
|
-
{ name: "<%= column[:name] %>", type: "<%= column[:type] %>" },
|
435
|
-
<% end %>
|
436
|
-
<% end %>
|
437
|
-
];
|
438
|
-
|
439
|
-
// Register SQL completion providers
|
440
|
-
monaco.languages.registerCompletionItemProvider('sql', {
|
441
|
-
provideCompletionItems: function(model, position) {
|
442
|
-
const textUntilPosition = model.getValueInRange({
|
443
|
-
startLineNumber: position.lineNumber,
|
444
|
-
startColumn: 1,
|
445
|
-
endLineNumber: position.lineNumber,
|
446
|
-
endColumn: position.column
|
447
|
-
});
|
448
|
-
|
449
|
-
const suggestions = [];
|
450
|
-
|
451
|
-
// Add table name suggestion
|
452
|
-
suggestions.push({
|
453
|
-
label: tableName,
|
454
|
-
kind: monaco.languages.CompletionItemKind.Class,
|
455
|
-
insertText: tableName,
|
456
|
-
detail: 'Table name'
|
457
|
-
});
|
458
|
-
|
459
|
-
// Add column name suggestions
|
460
|
-
columns.forEach(col => {
|
461
|
-
suggestions.push({
|
462
|
-
label: col.name,
|
463
|
-
kind: monaco.languages.CompletionItemKind.Field,
|
464
|
-
insertText: col.name,
|
465
|
-
detail: `Column (${col.type})`
|
466
|
-
});
|
467
|
-
});
|
468
|
-
|
469
|
-
// Add common SQL keywords
|
470
|
-
const keywords = [
|
471
|
-
{ label: 'SELECT', insertText: 'SELECT ' },
|
472
|
-
{ label: 'FROM', insertText: 'FROM ' },
|
473
|
-
{ label: 'WHERE', insertText: 'WHERE ' },
|
474
|
-
{ label: 'ORDER BY', insertText: 'ORDER BY ' },
|
475
|
-
{ label: 'GROUP BY', insertText: 'GROUP BY ' },
|
476
|
-
{ label: 'HAVING', insertText: 'HAVING ' },
|
477
|
-
{ label: 'LIMIT', insertText: 'LIMIT ' },
|
478
|
-
{ label: 'JOIN', insertText: 'JOIN ' },
|
479
|
-
{ label: 'LEFT JOIN', insertText: 'LEFT JOIN ' },
|
480
|
-
{ label: 'INNER JOIN', insertText: 'INNER JOIN ' }
|
481
|
-
];
|
482
|
-
|
483
|
-
keywords.forEach(kw => {
|
484
|
-
suggestions.push({
|
485
|
-
label: kw.label,
|
486
|
-
kind: monaco.languages.CompletionItemKind.Keyword,
|
487
|
-
insertText: kw.insertText
|
488
|
-
});
|
489
|
-
});
|
490
|
-
|
491
|
-
return { suggestions };
|
492
|
-
}
|
493
|
-
});
|
494
|
-
|
495
|
-
// Handle form submission - transfer content to hidden input before submitting
|
496
|
-
document.getElementById('sql-query-form').addEventListener('submit', function(event) {
|
497
|
-
// Stop the form from submitting immediately
|
498
|
-
event.preventDefault();
|
499
|
-
|
500
|
-
// Get the query value from the editor and set it to the hidden input
|
501
|
-
const queryValue = editor.getValue();
|
502
|
-
document.getElementById('query-input').value = queryValue;
|
503
|
-
|
504
|
-
// Log for debugging
|
505
|
-
console.log('Submitting query:', queryValue);
|
506
|
-
|
507
|
-
// Now manually submit the form
|
508
|
-
this.submit();
|
509
|
-
});
|
110
|
+
<% if @error.present? %>
|
111
|
+
<div class="alert alert-danger" role="alert">
|
112
|
+
<strong>Error:</strong> <%= @error %>
|
113
|
+
</div>
|
114
|
+
<% end %>
|
510
115
|
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
116
|
+
<% if @records.present? %>
|
117
|
+
<div class="card">
|
118
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
119
|
+
<h5>Results</h5>
|
120
|
+
<span class="badge bg-info">Rows: <%= @records.rows.count %></span>
|
121
|
+
</div>
|
122
|
+
<div class="card-body">
|
123
|
+
<div class="table-responsive">
|
124
|
+
<table class="table table-bordered table-striped">
|
125
|
+
<% if @records.columns.any? %>
|
126
|
+
<thead>
|
127
|
+
<tr>
|
128
|
+
<% @records.columns.each do |column_name| %>
|
129
|
+
<th><%= column_name %></th>
|
130
|
+
<% end %>
|
131
|
+
</tr>
|
132
|
+
</thead>
|
133
|
+
<tbody>
|
134
|
+
<% if @records.rows.any? %>
|
135
|
+
<% @records.rows.each do |row| %>
|
136
|
+
<tr>
|
137
|
+
<% row.each do |cell| %>
|
138
|
+
<td><%= format_cell_value(cell) %></td>
|
139
|
+
<% end %>
|
140
|
+
</tr>
|
141
|
+
<% end %>
|
142
|
+
<% else %>
|
143
|
+
<tr>
|
144
|
+
<td colspan="<%= @records.columns.count %>">Query executed successfully, but returned no rows.</td>
|
145
|
+
</tr>
|
146
|
+
<% end %>
|
147
|
+
</tbody>
|
148
|
+
<% else %>
|
149
|
+
<tr>
|
150
|
+
<td>Query executed successfully, but returned no columns.</td>
|
151
|
+
</tr>
|
152
|
+
<% end %>
|
153
|
+
</table>
|
154
|
+
</div>
|
155
|
+
</div>
|
156
|
+
</div>
|
157
|
+
<% end %>
|
520
158
|
|
521
|
-
|
522
|
-
|
523
|
-
// Get the query value from the editor and set it to the hidden input
|
524
|
-
const queryValue = editor.getValue();
|
525
|
-
document.getElementById('query-input').value = queryValue;
|
526
|
-
|
527
|
-
// Log for debugging
|
528
|
-
console.log('Submitting query via keyboard shortcut:', queryValue);
|
529
|
-
|
530
|
-
// Submit the form
|
531
|
-
document.getElementById('sql-query-form').submit();
|
532
|
-
});
|
533
|
-
|
534
|
-
// Add keyboard shortcuts for common SQL statements
|
535
|
-
editor.addAction({
|
536
|
-
id: 'insert-select-all',
|
537
|
-
label: 'Insert SELECT * FROM statement',
|
538
|
-
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KeyS],
|
539
|
-
run: function() {
|
540
|
-
editor.trigger('keyboard', 'type', { text: `SELECT * FROM ${tableName} LIMIT 100` });
|
541
|
-
return null;
|
542
|
-
}
|
543
|
-
});
|
544
|
-
|
545
|
-
editor.addAction({
|
546
|
-
id: 'insert-where',
|
547
|
-
label: 'Insert WHERE clause',
|
548
|
-
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KeyW],
|
549
|
-
run: function() {
|
550
|
-
editor.trigger('keyboard', 'type', { text: ' WHERE ' });
|
551
|
-
return null;
|
552
|
-
}
|
553
|
-
});
|
554
|
-
|
555
|
-
editor.addAction({
|
556
|
-
id: 'toggle-table-structure',
|
557
|
-
label: 'Toggle Table Structure Reference',
|
558
|
-
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyMod.Alt | monaco.KeyCode.KeyT],
|
559
|
-
run: function() {
|
560
|
-
// Use Bootstrap's collapse API to toggle
|
561
|
-
bootstrap.Collapse.getOrCreateInstance(document.getElementById('tableStructureContent')).toggle();
|
562
|
-
return null;
|
563
|
-
}
|
564
|
-
});
|
565
|
-
|
566
|
-
// Create a status bar showing cursor position and columns info
|
567
|
-
const statusBarDiv = document.createElement('div');
|
568
|
-
statusBarDiv.className = 'monaco-status-bar';
|
569
|
-
statusBarDiv.innerHTML = `<div class="status-info">Ready</div>
|
570
|
-
<div class="column-info">Table: ${tableName} (${columns.length} columns)</div>`;
|
571
|
-
document.getElementById('monaco-editor').after(statusBarDiv);
|
572
|
-
|
573
|
-
// Apply initial theme to status bar
|
574
|
-
const currentTheme = document.documentElement.getAttribute('data-bs-theme') || 'light';
|
575
|
-
updateStatusBarTheme(currentTheme);
|
576
|
-
|
577
|
-
// Update status bar with cursor position
|
578
|
-
editor.onDidChangeCursorPosition(e => {
|
579
|
-
const position = `Ln ${e.position.lineNumber}, Col ${e.position.column}`;
|
580
|
-
statusBarDiv.querySelector('.status-info').textContent = position;
|
581
|
-
});
|
582
|
-
|
583
|
-
// Focus the editor when page loads
|
584
|
-
window.addEventListener('load', () => {
|
585
|
-
editor.focus();
|
586
|
-
});
|
587
|
-
|
588
|
-
// Toggle icon when table structure collapses or expands
|
589
|
-
document.getElementById('tableStructureContent').addEventListener('show.bs.collapse', function() {
|
590
|
-
document.querySelector('#tableStructureHeader button i').classList.replace('bi-chevron-down', 'bi-chevron-up');
|
591
|
-
});
|
592
|
-
|
593
|
-
document.getElementById('tableStructureContent').addEventListener('hide.bs.collapse', function() {
|
594
|
-
document.querySelector('#tableStructureHeader button i').classList.replace('bi-chevron-up', 'bi-chevron-down');
|
595
|
-
});
|
596
|
-
</script>
|
159
|
+
<input type="text" id="table_name" class="d-none" value="<%= @table_name %>">
|
160
|
+
<input type="text" id="columns_data" class="d-none" value="<%= @columns.map { |column| { name: column[:name], type: column[:type] } }.to_json %>">
|