dbviewer 0.3.6 → 0.3.16
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 +34 -2
- data/app/controllers/concerns/dbviewer/database_operations.rb +160 -21
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +8 -0
- data/app/controllers/dbviewer/tables_controller.rb +130 -17
- data/app/helpers/dbviewer/application_helper.rb +73 -0
- data/app/views/dbviewer/home/index.html.erb +37 -16
- data/app/views/dbviewer/shared/_sidebar.html.erb +146 -1
- data/app/views/dbviewer/tables/index.html.erb +7 -1
- data/app/views/dbviewer/tables/mini_erd.html.erb +517 -0
- data/app/views/dbviewer/tables/show.html.erb +918 -40
- data/app/views/layouts/dbviewer/application.html.erb +19 -0
- data/config/routes.rb +1 -0
- data/lib/dbviewer/database_manager.rb +31 -115
- data/lib/dbviewer/query_analyzer.rb +130 -0
- data/lib/dbviewer/table_query_operations.rb +621 -0
- data/lib/dbviewer/table_query_params.rb +39 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/dbviewer.rb +1 -0
- metadata +5 -9
- data/app/services/dbviewer/file_storage.rb +0 -0
- data/app/services/dbviewer/in_memory_storage.rb +0 -0
- data/app/services/dbviewer/query_analyzer.rb +0 -0
- data/app/services/dbviewer/query_collection.rb +0 -0
- data/app/services/dbviewer/query_logger.rb +0 -0
- data/app/services/dbviewer/query_parser.rb +0 -82
- data/app/services/dbviewer/query_storage.rb +0 -0
@@ -2,15 +2,136 @@
|
|
2
2
|
Table: <%= @table_name %>
|
3
3
|
<% end %>
|
4
4
|
|
5
|
+
<% content_for :head do %>
|
6
|
+
<!-- Mermaid.js library for ERD diagrams -->
|
7
|
+
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
8
|
+
<!-- SVG-Pan-Zoom for interactive diagram navigation -->
|
9
|
+
<script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
|
10
|
+
<style>
|
11
|
+
/* Column sorting styles */
|
12
|
+
.sortable-column {
|
13
|
+
cursor: pointer;
|
14
|
+
position: relative;
|
15
|
+
transition: background-color 0.2s ease;
|
16
|
+
background-color: inherit;
|
17
|
+
}
|
18
|
+
|
19
|
+
.sortable-column:hover {
|
20
|
+
background-color: #f5f5f5;
|
21
|
+
}
|
22
|
+
|
23
|
+
.sortable-column.sorted {
|
24
|
+
background-color: #f0f0f0;
|
25
|
+
}
|
26
|
+
|
27
|
+
.sortable-column .column-sort-link {
|
28
|
+
display: flex;
|
29
|
+
align-items: center;
|
30
|
+
justify-content: space-between;
|
31
|
+
width: 100%;
|
32
|
+
height: 100%;
|
33
|
+
padding: 4px 0;
|
34
|
+
}
|
35
|
+
|
36
|
+
.sortable-column .column-name {
|
37
|
+
flex: 1;
|
38
|
+
overflow: hidden;
|
39
|
+
text-overflow: ellipsis;
|
40
|
+
white-space: nowrap;
|
41
|
+
}
|
42
|
+
|
43
|
+
.sortable-column .sort-icon-container {
|
44
|
+
flex: 0 0 auto;
|
45
|
+
width: 20px;
|
46
|
+
text-align: center;
|
47
|
+
margin-left: 4px;
|
48
|
+
}
|
49
|
+
|
50
|
+
.sortable-column .sort-icon {
|
51
|
+
font-size: 0.8em;
|
52
|
+
opacity: 0.7;
|
53
|
+
transition: opacity 0.2s ease, color 0.2s ease;
|
54
|
+
}
|
55
|
+
|
56
|
+
.sortable-column:hover .sort-icon.invisible {
|
57
|
+
visibility: visible !important;
|
58
|
+
opacity: 0.3;
|
59
|
+
}
|
60
|
+
|
61
|
+
/* Fix scrolling issues with sticky header */
|
62
|
+
.dbviewer-table-header {
|
63
|
+
position: sticky !important;
|
64
|
+
top: 0;
|
65
|
+
z-index: 10;
|
66
|
+
background-color: var(--bs-table-striped-bg, #f2f2f2) !important;
|
67
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
|
68
|
+
}
|
69
|
+
|
70
|
+
[data-bs-theme="dark"] .dbviewer-table-header {
|
71
|
+
background-color: var(--bs-dark-bg-subtle, #343a40) !important;
|
72
|
+
}
|
73
|
+
|
74
|
+
/* Improve mobile display for sort headers */
|
75
|
+
@media (max-width: 767.98px) {
|
76
|
+
.sortable-column .column-sort-link {
|
77
|
+
flex-direction: row;
|
78
|
+
align-items: center;
|
79
|
+
}
|
80
|
+
|
81
|
+
.sortable-column .sort-icon-container {
|
82
|
+
width: 16px;
|
83
|
+
}
|
84
|
+
}
|
85
|
+
|
86
|
+
/* Dark mode compatibility */
|
87
|
+
[data-bs-theme="dark"] .sortable-column:hover {
|
88
|
+
background-color: rgba(255, 255, 255, 0.05);
|
89
|
+
}
|
90
|
+
|
91
|
+
[data-bs-theme="dark"] .sortable-column.sorted {
|
92
|
+
background-color: rgba(255, 255, 255, 0.1);
|
93
|
+
}
|
94
|
+
</style>
|
95
|
+
|
96
|
+
<script>
|
97
|
+
// Initialize mermaid when document is ready
|
98
|
+
document.addEventListener('DOMContentLoaded', function() {
|
99
|
+
// Configure Mermaid for better ERD diagrams
|
100
|
+
mermaid.initialize({
|
101
|
+
startOnLoad: false,
|
102
|
+
theme: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default',
|
103
|
+
securityLevel: 'loose',
|
104
|
+
er: {
|
105
|
+
diagramPadding: 20,
|
106
|
+
layoutDirection: 'TB',
|
107
|
+
minEntityWidth: 100,
|
108
|
+
minEntityHeight: 75,
|
109
|
+
entityPadding: 15,
|
110
|
+
stroke: 'gray',
|
111
|
+
fill: document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2D3748' : '#f5f5f5',
|
112
|
+
fontSize: 14,
|
113
|
+
useMaxWidth: true,
|
114
|
+
wrapiength: 30
|
115
|
+
}
|
116
|
+
});
|
117
|
+
console.log('Mermaid initialized with theme:', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? 'dark' : 'default');
|
118
|
+
});
|
119
|
+
</script>
|
120
|
+
<% end %>
|
121
|
+
|
5
122
|
<% content_for :sidebar_active do %>active<% end %>
|
6
123
|
|
7
124
|
<% content_for :sidebar do %>
|
8
125
|
<%= render 'dbviewer/shared/sidebar' %>
|
9
|
-
<% end %>
|
10
|
-
|
126
|
+
<% end %>
|
11
127
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
12
|
-
<
|
128
|
+
<div>
|
129
|
+
<h1>Table: <%= @table_name %></h1>
|
130
|
+
</div>
|
13
131
|
<div class="d-flex gap-2">
|
132
|
+
<button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#miniErdModal">
|
133
|
+
<i class="bi bi-diagram-3 me-1"></i> View Relationships
|
134
|
+
</button>
|
14
135
|
<% if Dbviewer.configuration.enable_data_export %>
|
15
136
|
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#csvExportModal">
|
16
137
|
<i class="bi bi-file-earmark-spreadsheet me-1"></i> Export CSV
|
@@ -63,19 +184,72 @@
|
|
63
184
|
</div>
|
64
185
|
<% end %>
|
65
186
|
|
187
|
+
<!-- Mini ERD Modal -->
|
188
|
+
<div class="modal fade" id="miniErdModal" tabindex="-1" aria-labelledby="miniErdModalLabel" aria-hidden="true">
|
189
|
+
<div class="modal-dialog modal-xl modal-dialog-centered">
|
190
|
+
<div class="modal-content" id="miniErdModalContent">
|
191
|
+
<!-- Content will be loaded dynamically -->
|
192
|
+
<div class="modal-body text-center p-0">
|
193
|
+
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
|
194
|
+
<div class="text-center">
|
195
|
+
<div class="spinner-border text-primary" role="status">
|
196
|
+
<span class="visually-hidden">Loading...</span>
|
197
|
+
</div>
|
198
|
+
<p class="mt-2">Loading relationships diagram...</p>
|
199
|
+
</div>
|
200
|
+
</div>
|
201
|
+
</div>
|
202
|
+
</div>
|
203
|
+
</div>
|
204
|
+
</div>
|
205
|
+
|
66
206
|
<!-- Records Section -->
|
67
207
|
<div class="dbviewer-card card mb-4">
|
68
208
|
<div class="card-header d-flex justify-content-between align-items-center">
|
69
|
-
<h5 class="mb-0"
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
209
|
+
<h5 class="mb-0">
|
210
|
+
<%
|
211
|
+
# Build the URL query parameters including creation filters
|
212
|
+
url_params = "per_page=' + this.value + '&page=1&order_by=#{@order_by}&order_direction=#{@order_direction}"
|
213
|
+
|
214
|
+
# Add column filters
|
215
|
+
if @column_filters.reject { |_, v| v.blank? }.any?
|
216
|
+
url_params += "&" + @column_filters.reject { |_, v| v.blank? }.map { |k, v| "column_filters[#{k}]=#{CGI.escape(v.to_s)}" }.join("&")
|
217
|
+
end
|
218
|
+
|
219
|
+
# Add creation filters
|
220
|
+
url_params += "&creation_filter_start=#{@creation_filter_start}" if @creation_filter_start.present?
|
221
|
+
url_params += "&creation_filter_end=#{@creation_filter_end}" if @creation_filter_end.present?
|
222
|
+
%>
|
223
|
+
<select id="per-page-select" class="form-select form-select-sm" onchange="window.location.href='<%= table_path(@table_name) %>?<%= url_params %>'">
|
224
|
+
<% Dbviewer::TablesController.per_page_options.each do |option| %>
|
225
|
+
<option value="<%= option %>" <%= 'selected' if @per_page == option %>><%= option %></option>
|
226
|
+
<% end %>
|
227
|
+
</select>
|
228
|
+
</h5>
|
229
|
+
<div class="d-flex align-items-center table-actions">
|
230
|
+
<% if @order_by.present? %>
|
231
|
+
<span class="badge bg-primary me-2" title="Sort order">
|
232
|
+
<i class="bi bi-sort-<%= @order_direction == "ASC" ? "up" : "down" %> me-1"></i>
|
233
|
+
<%= @order_by %> (<%= @order_direction == "ASC" ? "ascending" : "descending" %>)
|
234
|
+
</span>
|
235
|
+
<% end %>
|
236
|
+
|
237
|
+
<%
|
238
|
+
clear_sort_params = {
|
239
|
+
page: 1,
|
240
|
+
per_page: @per_page,
|
241
|
+
column_filters: @column_filters
|
242
|
+
}
|
243
|
+
|
244
|
+
# Add creation filters if they exist
|
245
|
+
clear_sort_params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
246
|
+
clear_sort_params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
247
|
+
%>
|
248
|
+
<%= link_to "×", table_path(@table_name, clear_sort_params),
|
249
|
+
class: "text-light ms-2",
|
250
|
+
style: "text-decoration: none;",
|
251
|
+
title: "Clear sorting" %>
|
252
|
+
</span>
|
79
253
|
<span class="badge bg-secondary">Total: <%= @total_count %> records</span>
|
80
254
|
<% active_filters = @column_filters.reject { |_, v| v.blank? }.size %>
|
81
255
|
<% if active_filters > 0 %>
|
@@ -95,48 +269,173 @@
|
|
95
269
|
<table class="table table-bordered table-striped rounded-none">
|
96
270
|
<thead class="dbviewer-table-header">
|
97
271
|
<tr>
|
98
|
-
<% @records.columns
|
99
|
-
|
100
|
-
|
101
|
-
|
272
|
+
<% if @records && @records.columns %>
|
273
|
+
<% @records.columns.each do |column_name| %>
|
274
|
+
<% is_sorted = @order_by == column_name %>
|
275
|
+
<th class="px-3 py-2 sortable-column <%= 'sorted' if is_sorted %>">
|
276
|
+
<%= sortable_column_header(column_name, @order_by, @order_direction, @table_name, @current_page, @per_page, @column_filters) %>
|
277
|
+
</th>
|
278
|
+
<% end %>
|
279
|
+
<% else %>
|
280
|
+
<th>No columns available</th>
|
102
281
|
<% end %>
|
103
282
|
</tr>
|
104
283
|
<tr class="column-filters">
|
105
|
-
<% @records.columns
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
284
|
+
<% if @records && @records.columns %>
|
285
|
+
<% @records.columns.each do |column_name| %>
|
286
|
+
<th class="p-0">
|
287
|
+
<%
|
288
|
+
# Find column info to detect the type
|
289
|
+
column_info = @columns.find { |c| c[:name].to_s == column_name.to_s } if @columns
|
290
|
+
column_type = column_info ? column_info[:type].to_s.downcase : nil
|
291
|
+
%>
|
292
|
+
|
293
|
+
<div class="filter-input-group">
|
294
|
+
<%
|
295
|
+
# Define the default operator and available operators based on column type
|
296
|
+
default_operator = if column_type && column_type =~ /char|text|string|uuid|enum/i
|
297
|
+
"contains"
|
298
|
+
else
|
299
|
+
"eq"
|
300
|
+
end
|
301
|
+
|
302
|
+
# Get previously selected operator or default
|
303
|
+
selected_operator = @column_filters["#{column_name}_operator"]
|
304
|
+
selected_operator = default_operator if selected_operator.nil? || selected_operator == "default"
|
305
|
+
|
306
|
+
# Define operator options based on column type
|
307
|
+
if column_type && (column_type =~ /datetime/ || column_type =~ /^date$/ || column_type =~ /^time$/)
|
308
|
+
# Date/Time operators
|
309
|
+
operator_options = [
|
310
|
+
["=", "eq"],
|
311
|
+
["≠", "neq"],
|
312
|
+
["<", "lt"],
|
313
|
+
[">", "gt"],
|
314
|
+
["≤", "lte"],
|
315
|
+
["≥", "gte"]
|
316
|
+
]
|
317
|
+
elsif column_type && column_type =~ /int|float|decimal|double|number|numeric|real|money|bigint|smallint|tinyint|mediumint|bit/i
|
318
|
+
# Numeric operators
|
319
|
+
operator_options = [
|
320
|
+
["=", "eq"],
|
321
|
+
["≠", "neq"],
|
322
|
+
["<", "lt"],
|
323
|
+
[">", "gt"],
|
324
|
+
["≤", "lte"],
|
325
|
+
["≥", "gte"]
|
326
|
+
]
|
327
|
+
else
|
328
|
+
# Text operators
|
329
|
+
operator_options = [
|
330
|
+
["contains", "contains"],
|
331
|
+
["not contains", "not_contains"],
|
332
|
+
["=", "eq"],
|
333
|
+
["≠", "neq"],
|
334
|
+
["starts with", "starts_with"],
|
335
|
+
["ends with", "ends_with"]
|
336
|
+
]
|
337
|
+
end
|
338
|
+
%>
|
339
|
+
|
340
|
+
<%= form.select "column_filters[#{column_name}_operator]",
|
341
|
+
options_for_select(operator_options, selected_operator),
|
342
|
+
{ include_blank: false },
|
343
|
+
{ class: "form-select form-select-sm operator-select" } %>
|
344
|
+
|
345
|
+
<% if column_type && column_type =~ /datetime/ %>
|
346
|
+
<%= form.datetime_local_field "column_filters[#{column_name}]",
|
347
|
+
value: @column_filters[column_name],
|
348
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
349
|
+
data: { column: column_name } %>
|
350
|
+
<% elsif column_type && column_type =~ /^date$/ %>
|
351
|
+
<%= form.date_field "column_filters[#{column_name}]",
|
352
|
+
value: @column_filters[column_name],
|
353
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
354
|
+
data: { column: column_name } %>
|
355
|
+
<% elsif column_type && column_type =~ /^time$/ %>
|
356
|
+
<%= form.time_field "column_filters[#{column_name}]",
|
357
|
+
value: @column_filters[column_name],
|
358
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
359
|
+
data: { column: column_name } %>
|
360
|
+
<% else %>
|
361
|
+
<%= form.text_field "column_filters[#{column_name}]",
|
362
|
+
value: @column_filters[column_name],
|
363
|
+
placeholder: "",
|
364
|
+
class: "form-control form-control-sm column-filter rounded-0",
|
365
|
+
data: { column: column_name } %>
|
366
|
+
<% end %>
|
367
|
+
</div>
|
368
|
+
</th>
|
369
|
+
<% end %>
|
370
|
+
<% else %>
|
371
|
+
<th></th>
|
113
372
|
<% end %>
|
114
373
|
</tr>
|
115
374
|
</thead>
|
116
375
|
<tbody>
|
117
|
-
<% if @records.empty? %>
|
376
|
+
<% if @records.nil? || @records.rows.nil? || @records.empty? %>
|
118
377
|
<tr>
|
119
378
|
<td colspan="100%" class="text-center">No records found or table is empty.</td>
|
120
379
|
</tr>
|
121
380
|
<% end %>
|
122
|
-
<% @records.rows
|
123
|
-
|
124
|
-
|
125
|
-
<%
|
126
|
-
|
127
|
-
|
128
|
-
|
381
|
+
<% if @records && @records.rows %>
|
382
|
+
<% @records.rows.each do |row| %>
|
383
|
+
<tr>
|
384
|
+
<% row.each_with_index do |cell, cell_index| %>
|
385
|
+
<%
|
386
|
+
column_name = @records.columns[cell_index]
|
387
|
+
cell_value = format_cell_value(cell)
|
388
|
+
|
389
|
+
# Check if this column is a foreign key
|
390
|
+
foreign_key = @metadata && @metadata[:foreign_keys] ?
|
391
|
+
@metadata[:foreign_keys].find { |fk| fk[:column] == column_name } :
|
392
|
+
nil
|
393
|
+
%>
|
394
|
+
<% if foreign_key && !cell.nil? %>
|
395
|
+
<%
|
396
|
+
# Build params for foreign key links with creation filters
|
397
|
+
fk_params = { column_filters: { foreign_key[:primary_key] => cell } }
|
398
|
+
|
399
|
+
# Add creation filters if they exist
|
400
|
+
fk_params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
401
|
+
fk_params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
402
|
+
%>
|
403
|
+
<td title="<%= cell_value %> (Click to view referenced record)">
|
404
|
+
<%= link_to cell_value,
|
405
|
+
table_path(foreign_key[:to_table], fk_params),
|
406
|
+
class: "text-decoration-none foreign-key-link" %>
|
407
|
+
<i class="bi bi-link-45deg text-muted small"></i>
|
408
|
+
</td>
|
409
|
+
<% else %>
|
410
|
+
<td title="<%= cell_value %>"><%= cell_value %></td>
|
411
|
+
<% end %>
|
412
|
+
<% end %>
|
413
|
+
</tr>
|
414
|
+
<% end %>
|
129
415
|
<% end %>
|
130
416
|
</tbody>
|
131
417
|
</table>
|
132
418
|
<% end %> <!-- End of form_with -->
|
133
419
|
</div>
|
134
420
|
|
135
|
-
<% if @total_pages > 1 %>
|
421
|
+
<% if @total_pages && @total_pages > 1 %>
|
422
|
+
<%
|
423
|
+
# Prepare common URL params including creation filters
|
424
|
+
common_params = {
|
425
|
+
order_by: @order_by,
|
426
|
+
order_direction: @order_direction,
|
427
|
+
per_page: @per_page,
|
428
|
+
column_filters: @column_filters
|
429
|
+
}
|
430
|
+
|
431
|
+
# Add creation filters if they exist
|
432
|
+
common_params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
433
|
+
common_params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
434
|
+
%>
|
136
435
|
<nav aria-label="Page navigation">
|
137
436
|
<ul class="pagination justify-content-center">
|
138
437
|
<li class="page-item <%= 'disabled' if @current_page == 1 %>">
|
139
|
-
<%= link_to '«', table_path(@table_name, page: [@current_page - 1, 1].max
|
438
|
+
<%= link_to '«', table_path(@table_name, common_params.merge(page: [@current_page - 1, 1].max)), class: 'page-link' %>
|
140
439
|
</li>
|
141
440
|
|
142
441
|
<% start_page = [1, @current_page - 2].max %>
|
@@ -145,12 +444,12 @@
|
|
145
444
|
|
146
445
|
<% (start_page..end_page).each do |page_num| %>
|
147
446
|
<li class="page-item <%= 'active' if page_num == @current_page %>">
|
148
|
-
<%= link_to page_num, table_path(@table_name, page: page_num
|
447
|
+
<%= link_to page_num, table_path(@table_name, common_params.merge(page: page_num)), class: 'page-link' %>
|
149
448
|
</li>
|
150
449
|
<% end %>
|
151
450
|
|
152
451
|
<li class="page-item <%= 'disabled' if @current_page == @total_pages %>">
|
153
|
-
<%= link_to '»', table_path(@table_name, page: [@current_page + 1, @total_pages].min
|
452
|
+
<%= link_to '»', table_path(@table_name, common_params.merge(page: [@current_page + 1, @total_pages].min)), class: 'page-link' %>
|
154
453
|
</li>
|
155
454
|
</ul>
|
156
455
|
</nav>
|
@@ -168,10 +467,23 @@
|
|
168
467
|
<div class="card-header d-flex justify-content-between align-items-center">
|
169
468
|
<h5 class="mb-0"><i class="bi bi-graph-up me-2"></i>Record Creation Timeline</h5>
|
170
469
|
<div>
|
470
|
+
<%
|
471
|
+
# Build common params for time grouping links
|
472
|
+
time_params = {
|
473
|
+
page: @current_page,
|
474
|
+
order_by: @order_by,
|
475
|
+
order_direction: @order_direction,
|
476
|
+
per_page: @per_page
|
477
|
+
}
|
478
|
+
|
479
|
+
# Add creation filters if they exist
|
480
|
+
time_params[:creation_filter_start] = @creation_filter_start if @creation_filter_start.present?
|
481
|
+
time_params[:creation_filter_end] = @creation_filter_end if @creation_filter_end.present?
|
482
|
+
%>
|
171
483
|
<div class="btn-group btn-group-sm" role="group" aria-label="Time grouping">
|
172
|
-
<%= link_to "Hourly", table_path(@table_name, time_group: "hourly"
|
173
|
-
<%= link_to "Daily", table_path(@table_name, time_group: "daily"
|
174
|
-
<%= link_to "Weekly", table_path(@table_name, time_group: "weekly"
|
484
|
+
<%= link_to "Hourly", table_path(@table_name, time_params.merge(time_group: "hourly")), class: "btn btn-outline-primary #{@time_grouping == 'hourly' ? 'active' : ''}" %>
|
485
|
+
<%= link_to "Daily", table_path(@table_name, time_params.merge(time_group: "daily")), class: "btn btn-outline-primary #{@time_grouping == 'daily' ? 'active' : ''}" %>
|
486
|
+
<%= link_to "Weekly", table_path(@table_name, time_params.merge(time_group: "weekly")), class: "btn btn-outline-primary #{@time_grouping == 'weekly' ? 'active' : ''}" %>
|
175
487
|
</div>
|
176
488
|
</div>
|
177
489
|
</div>
|
@@ -220,6 +532,7 @@
|
|
220
532
|
document.addEventListener('DOMContentLoaded', function() {
|
221
533
|
// Column filter functionality
|
222
534
|
const columnFilters = document.querySelectorAll('.column-filter');
|
535
|
+
const operatorSelects = document.querySelectorAll('.operator-select');
|
223
536
|
const filterForm = document.getElementById('column-filters-form');
|
224
537
|
|
225
538
|
// Add debounce function to reduce form submissions
|
@@ -242,7 +555,18 @@
|
|
242
555
|
|
243
556
|
// Add event listeners to all filter inputs
|
244
557
|
columnFilters.forEach(function(filter) {
|
558
|
+
// For text fields use input event
|
245
559
|
filter.addEventListener('input', submitForm);
|
560
|
+
|
561
|
+
// For date/time fields also use change event since they have calendar/time pickers
|
562
|
+
if (filter.type === 'date' || filter.type === 'datetime-local' || filter.type === 'time') {
|
563
|
+
filter.addEventListener('change', submitForm);
|
564
|
+
}
|
565
|
+
});
|
566
|
+
|
567
|
+
// Add event listeners to operator selects
|
568
|
+
operatorSelects.forEach(function(select) {
|
569
|
+
select.addEventListener('change', submitForm);
|
246
570
|
});
|
247
571
|
|
248
572
|
// Add clear button functionality if there are any filters applied
|
@@ -262,11 +586,441 @@
|
|
262
586
|
paginationContainer.insertAdjacentHTML('afterend', clearButton.outerHTML);
|
263
587
|
|
264
588
|
document.getElementById('clear-filters').addEventListener('click', function() {
|
589
|
+
// Reset all input values
|
265
590
|
columnFilters.forEach(filter => filter.value = '');
|
591
|
+
|
592
|
+
// Reset operator selects to their default values
|
593
|
+
operatorSelects.forEach(select => {
|
594
|
+
// Find the first option of the select (usually the default)
|
595
|
+
if (select.options.length > 0) {
|
596
|
+
select.selectedIndex = 0;
|
597
|
+
}
|
598
|
+
});
|
599
|
+
|
266
600
|
submitForm();
|
267
601
|
});
|
268
602
|
}
|
269
603
|
}
|
604
|
+
|
605
|
+
// Load Mini ERD when modal is opened
|
606
|
+
const miniErdModal = document.getElementById('miniErdModal');
|
607
|
+
if (miniErdModal) {
|
608
|
+
let isModalLoaded = false;
|
609
|
+
let erdData = null;
|
610
|
+
|
611
|
+
miniErdModal.addEventListener('show.bs.modal', function(event) {
|
612
|
+
const modalContent = document.getElementById('miniErdModalContent');
|
613
|
+
|
614
|
+
// Set loading state
|
615
|
+
modalContent.innerHTML = `
|
616
|
+
<div class="modal-header">
|
617
|
+
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
618
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
619
|
+
</div>
|
620
|
+
<div class="modal-body p-0">
|
621
|
+
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
|
622
|
+
<div class="text-center">
|
623
|
+
<div class="spinner-border text-primary mb-3" role="status">
|
624
|
+
<span class="visually-hidden">Loading...</span>
|
625
|
+
</div>
|
626
|
+
<p class="mt-2">Loading relationships diagram...</p>
|
627
|
+
<small class="text-muted">This may take a moment for tables with many relationships</small>
|
628
|
+
</div>
|
629
|
+
</div>
|
630
|
+
</div>
|
631
|
+
`;
|
632
|
+
|
633
|
+
// Always fetch fresh data when modal is opened
|
634
|
+
fetchErdData();
|
635
|
+
});
|
636
|
+
|
637
|
+
// Function to fetch ERD data
|
638
|
+
function fetchErdData() {
|
639
|
+
// Add cache-busting timestamp to prevent browser caching
|
640
|
+
const cacheBuster = new Date().getTime();
|
641
|
+
const fetchUrl = `<%= dbviewer.mini_erd_table_path(@table_name, format: :json) %>?_=${cacheBuster}`;
|
642
|
+
|
643
|
+
// Log loading message
|
644
|
+
console.log('Loading fresh Mini ERD data from:', fetchUrl);
|
645
|
+
|
646
|
+
// Set a timeout to handle long-running requests
|
647
|
+
const timeoutPromise = new Promise((_, reject) =>
|
648
|
+
setTimeout(() => reject(new Error('Request timeout after 10 seconds')), 10000)
|
649
|
+
);
|
650
|
+
|
651
|
+
// Race the fetch against a timeout
|
652
|
+
Promise.race([
|
653
|
+
fetch(fetchUrl),
|
654
|
+
timeoutPromise
|
655
|
+
])
|
656
|
+
.then(response => {
|
657
|
+
if (!response.ok) {
|
658
|
+
throw new Error(`Server returned ${response.status} ${response.statusText}`);
|
659
|
+
}
|
660
|
+
return response.json(); // Parse as JSON instead of text
|
661
|
+
})
|
662
|
+
.then(data => {
|
663
|
+
isModalLoaded = true;
|
664
|
+
erdData = data; // Store the data
|
665
|
+
renderMiniErd(data);
|
666
|
+
})
|
667
|
+
.catch(error => {
|
668
|
+
console.error('Error loading mini ERD:', error);
|
669
|
+
showErdError(error);
|
670
|
+
});
|
671
|
+
}
|
672
|
+
|
673
|
+
// Function to show error modal
|
674
|
+
function showErdError(error) {
|
675
|
+
const modalContent = document.getElementById('miniErdModalContent');
|
676
|
+
modalContent.innerHTML = `
|
677
|
+
<div class="modal-header">
|
678
|
+
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
679
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
680
|
+
</div>
|
681
|
+
<div class="modal-body p-0">
|
682
|
+
<div class="alert alert-danger m-3">
|
683
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
684
|
+
<strong>Error loading relationship diagram</strong>
|
685
|
+
<p class="mt-2 mb-0">${error.message}</p>
|
686
|
+
</div>
|
687
|
+
<div class="m-3">
|
688
|
+
<p><strong>Debug Information:</strong></p>
|
689
|
+
<code>GET <%= dbviewer.mini_erd_table_path(@table_name, format: :json) %></code> failed
|
690
|
+
<p class="mt-3">
|
691
|
+
<button class="btn btn-sm btn-primary" onclick="retryLoadingMiniERD()">
|
692
|
+
<i class="bi bi-arrow-clockwise me-1"></i> Retry
|
693
|
+
</button>
|
694
|
+
</p>
|
695
|
+
</div>
|
696
|
+
</div>
|
697
|
+
<div class="modal-footer">
|
698
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
699
|
+
</div>
|
700
|
+
`;
|
701
|
+
}
|
702
|
+
|
703
|
+
// Function to render the ERD with Mermaid
|
704
|
+
function renderMiniErd(data) {
|
705
|
+
const modalContent = document.getElementById('miniErdModalContent');
|
706
|
+
|
707
|
+
// Set up the modal content with container for ERD
|
708
|
+
modalContent.innerHTML = `
|
709
|
+
<div class="modal-header">
|
710
|
+
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
711
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
712
|
+
</div>
|
713
|
+
<div class="modal-body p-0"> <!-- Removed padding for full width -->
|
714
|
+
<div id="mini-erd-container" class="w-100" style="min-height: 450px; height: 100%;"> <!-- Increased height -->
|
715
|
+
<div id="mini-erd-loading" class="d-flex justify-content-center align-items-center" style="height: 100%; min-height: 450px;">
|
716
|
+
<div class="text-center">
|
717
|
+
<div class="spinner-border text-primary mb-3" role="status">
|
718
|
+
<span class="visually-hidden">Loading...</span>
|
719
|
+
</div>
|
720
|
+
<p>Generating Relationships Diagram...</p>
|
721
|
+
</div>
|
722
|
+
</div>
|
723
|
+
<div id="mini-erd-error" class="alert alert-danger m-3 d-none">
|
724
|
+
<h5>Error generating diagram</h5>
|
725
|
+
<p id="mini-erd-error-message">There was an error rendering the relationships diagram.</p>
|
726
|
+
<pre id="mini-erd-error-details" class="bg-light p-2 small mt-2"></pre>
|
727
|
+
</div>
|
728
|
+
</div>
|
729
|
+
<div id="debug-data" class="d-none m-3 border-top pt-3">
|
730
|
+
<details>
|
731
|
+
<summary>Debug Information</summary>
|
732
|
+
<div class="alert alert-info small">
|
733
|
+
<pre id="erd-data-debug" style="max-height: 100px; overflow: auto;">${JSON.stringify(data, null, 2)}</pre>
|
734
|
+
</div>
|
735
|
+
</details>
|
736
|
+
</div>
|
737
|
+
</div>
|
738
|
+
<div class="modal-footer">
|
739
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
740
|
+
<a href="<%= dbviewer.entity_relationship_diagrams_path %>" class="btn btn-primary">View Full ERD</a>
|
741
|
+
</div>
|
742
|
+
`;
|
743
|
+
|
744
|
+
try {
|
745
|
+
const tables = data.tables || [];
|
746
|
+
const relationships = data.relationships || [];
|
747
|
+
|
748
|
+
// Validate data before proceeding
|
749
|
+
if (!Array.isArray(tables) || !Array.isArray(relationships)) {
|
750
|
+
showDiagramError('Invalid data format', 'The relationship data is not in the expected format.');
|
751
|
+
console.error('Invalid data format received:', data);
|
752
|
+
return;
|
753
|
+
}
|
754
|
+
|
755
|
+
console.log(`Found ${tables.length} tables and ${relationships.length} relationships`);
|
756
|
+
|
757
|
+
// Create the ER diagram definition in Mermaid syntax
|
758
|
+
let mermaidDefinition = 'erDiagram\n';
|
759
|
+
|
760
|
+
// Add tables to the diagram - ensure we have at least one table
|
761
|
+
if (tables.length === 0) {
|
762
|
+
mermaidDefinition += ` <%= @table_name.gsub(/[^\w]/, '_') %> {\n`;
|
763
|
+
mermaidDefinition += ` string id PK\n`;
|
764
|
+
mermaidDefinition += ` }\n`;
|
765
|
+
} else {
|
766
|
+
tables.forEach(function(table) {
|
767
|
+
const tableName = table.name;
|
768
|
+
|
769
|
+
if (!tableName) {
|
770
|
+
console.warn('Table with no name found:', table);
|
771
|
+
return; // Skip this table
|
772
|
+
}
|
773
|
+
|
774
|
+
// Clean table name for mermaid (remove special characters)
|
775
|
+
const cleanTableName = tableName.replace(/[^\w]/g, '_');
|
776
|
+
|
777
|
+
// Make the current table stand out with a different visualization
|
778
|
+
if (tableName === '<%= @table_name %>') {
|
779
|
+
mermaidDefinition += ` ${cleanTableName} {\n`;
|
780
|
+
mermaidDefinition += ` string id PK\n`;
|
781
|
+
mermaidDefinition += ` }\n`;
|
782
|
+
} else {
|
783
|
+
mermaidDefinition += ` ${cleanTableName} {\n`;
|
784
|
+
mermaidDefinition += ` string id\n`;
|
785
|
+
mermaidDefinition += ` }\n`;
|
786
|
+
}
|
787
|
+
});
|
788
|
+
}
|
789
|
+
|
790
|
+
// Add relationships
|
791
|
+
if (relationships && relationships.length > 0) {
|
792
|
+
relationships.forEach(function(rel) {
|
793
|
+
try {
|
794
|
+
// Ensure all required properties exist
|
795
|
+
if (!rel.from_table || !rel.to_table) {
|
796
|
+
console.error('Missing table in relationship:', rel);
|
797
|
+
return; // Skip this relationship
|
798
|
+
}
|
799
|
+
|
800
|
+
// Clean up table names for mermaid (remove special characters)
|
801
|
+
const fromTable = rel.from_table.replace(/[^\w]/g, '_');
|
802
|
+
const toTable = rel.to_table.replace(/[^\w]/g, '_');
|
803
|
+
const relationLabel = rel.from_column || '';
|
804
|
+
|
805
|
+
// Customize the display based on direction
|
806
|
+
mermaidDefinition += ` ${fromTable} }|--|| ${toTable} : "${relationLabel}"\n`;
|
807
|
+
} catch (err) {
|
808
|
+
console.error('Error processing relationship:', err, rel);
|
809
|
+
}
|
810
|
+
});
|
811
|
+
} else {
|
812
|
+
// Add a note if no relationships are found
|
813
|
+
mermaidDefinition += ' %% No relationships found for this table\n';
|
814
|
+
}
|
815
|
+
|
816
|
+
// Log the generated mermaid definition for debugging
|
817
|
+
console.log('Mermaid Definition:', mermaidDefinition);
|
818
|
+
|
819
|
+
// Hide the loading indicator first since render might take time
|
820
|
+
document.getElementById('mini-erd-loading').style.display = 'none';
|
821
|
+
|
822
|
+
// Render the diagram with Mermaid
|
823
|
+
mermaid.render('mini-erd-graph', mermaidDefinition)
|
824
|
+
.then(function(result) {
|
825
|
+
console.log('Mermaid rendering successful');
|
826
|
+
|
827
|
+
// Get the container
|
828
|
+
const container = document.getElementById('mini-erd-container');
|
829
|
+
|
830
|
+
// Insert the rendered SVG
|
831
|
+
container.innerHTML = result.svg;
|
832
|
+
|
833
|
+
// Style the SVG element for better fit
|
834
|
+
const svgElement = container.querySelector('svg');
|
835
|
+
if (svgElement) {
|
836
|
+
// Set size attributes for the SVG
|
837
|
+
svgElement.setAttribute('width', '100%');
|
838
|
+
svgElement.setAttribute('height', '100%');
|
839
|
+
svgElement.style.minHeight = '450px';
|
840
|
+
svgElement.style.width = '100%';
|
841
|
+
svgElement.style.height = '100%';
|
842
|
+
|
843
|
+
// Set viewBox if not present to enable proper scaling
|
844
|
+
if (!svgElement.getAttribute('viewBox')) {
|
845
|
+
const width = svgElement.getAttribute('width') || '100%';
|
846
|
+
const height = svgElement.getAttribute('height') || '100%';
|
847
|
+
svgElement.setAttribute('viewBox', `0 0 ${parseInt(width) || 1000} ${parseInt(height) || 800}`);
|
848
|
+
}
|
849
|
+
}
|
850
|
+
|
851
|
+
// Apply SVG-Pan-Zoom to make the diagram interactive
|
852
|
+
try {
|
853
|
+
const svgElement = container.querySelector('svg');
|
854
|
+
if (svgElement && typeof svgPanZoom !== 'undefined') {
|
855
|
+
// Make SVG take the full container width
|
856
|
+
svgElement.setAttribute('width', '100%');
|
857
|
+
svgElement.setAttribute('height', '100%');
|
858
|
+
|
859
|
+
// Initialize SVG Pan-Zoom
|
860
|
+
const panZoomInstance = svgPanZoom(svgElement, {
|
861
|
+
zoomEnabled: true,
|
862
|
+
controlIconsEnabled: true,
|
863
|
+
fit: true,
|
864
|
+
center: true,
|
865
|
+
minZoom: 0.5,
|
866
|
+
maxZoom: 2.5
|
867
|
+
});
|
868
|
+
|
869
|
+
// Store the panZoom instance for resize handling
|
870
|
+
container.panZoomInstance = panZoomInstance;
|
871
|
+
|
872
|
+
// Setup resize observer to maintain full size
|
873
|
+
const resizeObserver = new ResizeObserver(() => {
|
874
|
+
if (container.panZoomInstance) {
|
875
|
+
// Reset zoom and center when container is resized
|
876
|
+
container.panZoomInstance.resize();
|
877
|
+
container.panZoomInstance.fit();
|
878
|
+
container.panZoomInstance.center();
|
879
|
+
}
|
880
|
+
});
|
881
|
+
|
882
|
+
// Observe the container for size changes
|
883
|
+
resizeObserver.observe(container);
|
884
|
+
|
885
|
+
// Also handle manual resize on modal resize
|
886
|
+
miniErdModal.addEventListener('resize.bs.modal', function() {
|
887
|
+
if (container.panZoomInstance) {
|
888
|
+
setTimeout(() => {
|
889
|
+
container.panZoomInstance.resize();
|
890
|
+
container.panZoomInstance.fit();
|
891
|
+
container.panZoomInstance.center();
|
892
|
+
}, 100);
|
893
|
+
}
|
894
|
+
});
|
895
|
+
}
|
896
|
+
} catch (e) {
|
897
|
+
console.warn('Failed to initialize svg-pan-zoom:', e);
|
898
|
+
// Not critical, continue without pan-zoom
|
899
|
+
}
|
900
|
+
|
901
|
+
// Add highlighting for the current table
|
902
|
+
setTimeout(function() {
|
903
|
+
try {
|
904
|
+
const cleanTableName = '<%= @table_name %>'.replace(/[^\w]/g, '_');
|
905
|
+
const currentTableElement = container.querySelector(`[id*="${cleanTableName}"]`);
|
906
|
+
if (currentTableElement) {
|
907
|
+
const rect = currentTableElement.querySelector('rect');
|
908
|
+
if (rect) {
|
909
|
+
// Highlight the current table
|
910
|
+
rect.setAttribute('fill', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#2c3034' : '#e2f0ff');
|
911
|
+
rect.setAttribute('stroke', document.documentElement.getAttribute('data-bs-theme') === 'dark' ? '#6ea8fe' : '#0d6efd');
|
912
|
+
rect.setAttribute('stroke-width', '2');
|
913
|
+
}
|
914
|
+
}
|
915
|
+
} catch (e) {
|
916
|
+
console.error('Error highlighting current table:', e);
|
917
|
+
}
|
918
|
+
}, 100);
|
919
|
+
})
|
920
|
+
.catch(function(error) {
|
921
|
+
console.error('Error rendering mini ERD:', error);
|
922
|
+
showDiagramError(
|
923
|
+
'Error rendering diagram',
|
924
|
+
'There was an error rendering the relationships diagram.',
|
925
|
+
error.message || 'Unknown error'
|
926
|
+
);
|
927
|
+
|
928
|
+
// Show debug data when there's an error
|
929
|
+
document.getElementById('debug-data').classList.remove('d-none');
|
930
|
+
});
|
931
|
+
} catch (error) {
|
932
|
+
console.error('Exception in renderMiniErd function:', error);
|
933
|
+
showDiagramError(
|
934
|
+
'Exception generating diagram',
|
935
|
+
'There was an exception processing the relationships diagram.',
|
936
|
+
error.message || 'Unknown error'
|
937
|
+
);
|
938
|
+
|
939
|
+
// Show debug data when there's an error
|
940
|
+
document.getElementById('debug-data').classList.remove('d-none');
|
941
|
+
}
|
942
|
+
}
|
943
|
+
|
944
|
+
// Function to show diagram error
|
945
|
+
function showDiagramError(title, message, details = '') {
|
946
|
+
const errorContainer = document.getElementById('mini-erd-error');
|
947
|
+
const errorMessage = document.getElementById('mini-erd-error-message');
|
948
|
+
const errorDetails = document.getElementById('mini-erd-error-details');
|
949
|
+
const loadingIndicator = document.getElementById('mini-erd-loading');
|
950
|
+
|
951
|
+
if (loadingIndicator) {
|
952
|
+
loadingIndicator.style.display = 'none';
|
953
|
+
}
|
954
|
+
|
955
|
+
if (errorContainer && errorMessage) {
|
956
|
+
// Set error message
|
957
|
+
errorMessage.textContent = message;
|
958
|
+
|
959
|
+
// Set error details if provided
|
960
|
+
if (details && errorDetails) {
|
961
|
+
errorDetails.textContent = details;
|
962
|
+
errorDetails.classList.remove('d-none');
|
963
|
+
} else if (errorDetails) {
|
964
|
+
errorDetails.classList.add('d-none');
|
965
|
+
}
|
966
|
+
|
967
|
+
// Show the error container
|
968
|
+
errorContainer.classList.remove('d-none');
|
969
|
+
}
|
970
|
+
}
|
971
|
+
|
972
|
+
// Handle modal shown event - adjust size after the modal is fully visible
|
973
|
+
miniErdModal.addEventListener('shown.bs.modal', function(event) {
|
974
|
+
// After modal is fully shown, resize the diagram to fit
|
975
|
+
const container = document.getElementById('mini-erd-container');
|
976
|
+
if (container && container.panZoomInstance) {
|
977
|
+
setTimeout(() => {
|
978
|
+
container.panZoomInstance.resize();
|
979
|
+
container.panZoomInstance.fit();
|
980
|
+
container.panZoomInstance.center();
|
981
|
+
}, 200); // Small delay to ensure modal is fully transitioned
|
982
|
+
}
|
983
|
+
});
|
984
|
+
|
985
|
+
// Handle modal close to reset state for future opens
|
986
|
+
miniErdModal.addEventListener('hidden.bs.modal', function(event) {
|
987
|
+
// Reset flags and cached data to ensure fresh fetch on next open
|
988
|
+
isModalLoaded = false;
|
989
|
+
erdData = null;
|
990
|
+
console.log('Modal closed, diagram data will be refetched on next open');
|
991
|
+
});
|
992
|
+
}
|
993
|
+
|
994
|
+
// Function to retry loading the Mini ERD
|
995
|
+
function retryLoadingMiniERD() {
|
996
|
+
console.log('Retrying loading of mini ERD');
|
997
|
+
const modalContent = document.getElementById('miniErdModalContent');
|
998
|
+
|
999
|
+
// Set loading state again
|
1000
|
+
modalContent.innerHTML = `
|
1001
|
+
<div class="modal-header">
|
1002
|
+
<h5 class="modal-title">Relationships for <%= @table_name %></h5>
|
1003
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
1004
|
+
</div>
|
1005
|
+
<div class="modal-body p-0">
|
1006
|
+
<div id="mini-erd-container" class="w-100 d-flex justify-content-center align-items-center" style="min-height: 450px; height: 100%;">
|
1007
|
+
<div class="text-center">
|
1008
|
+
<div class="spinner-border text-primary mb-3" role="status">
|
1009
|
+
<span class="visually-hidden">Loading...</span>
|
1010
|
+
</div>
|
1011
|
+
<p>Retrying to load relationships diagram...</p>
|
1012
|
+
</div>
|
1013
|
+
</div>
|
1014
|
+
</div>
|
1015
|
+
`;
|
1016
|
+
|
1017
|
+
// Reset state to ensure fresh fetch
|
1018
|
+
isModalLoaded = false;
|
1019
|
+
erdData = null;
|
1020
|
+
|
1021
|
+
// Retry fetching data
|
1022
|
+
fetchErdData();
|
1023
|
+
}
|
270
1024
|
});
|
271
1025
|
</script>
|
272
1026
|
|
@@ -284,6 +1038,46 @@
|
|
284
1038
|
font-size: 0.85rem;
|
285
1039
|
}
|
286
1040
|
|
1041
|
+
/* Filter input group styling */
|
1042
|
+
.filter-input-group {
|
1043
|
+
display: flex;
|
1044
|
+
flex-direction: column;
|
1045
|
+
width: 100%;
|
1046
|
+
}
|
1047
|
+
|
1048
|
+
.filter-input-group .operator-select {
|
1049
|
+
font-size: 0.7rem;
|
1050
|
+
padding: 0.1rem 0.5rem;
|
1051
|
+
border-bottom: none;
|
1052
|
+
border-radius: 0.2rem 0.2rem 0 0;
|
1053
|
+
background-color: rgba(0,0,0,0.03);
|
1054
|
+
font-weight: normal;
|
1055
|
+
min-height: 22px; /* Ensure consistent height */
|
1056
|
+
}
|
1057
|
+
|
1058
|
+
.filter-input-group .operator-select option {
|
1059
|
+
font-size: 0.8rem;
|
1060
|
+
padding: 2px;
|
1061
|
+
}
|
1062
|
+
|
1063
|
+
[data-bs-theme="dark"] .filter-input-group .operator-select {
|
1064
|
+
background-color: rgba(255,255,255,0.05);
|
1065
|
+
color: rgba(255,255,255,0.9);
|
1066
|
+
border-color: rgba(255,255,255,0.15);
|
1067
|
+
}
|
1068
|
+
|
1069
|
+
.filter-input-group .column-filter {
|
1070
|
+
border-radius: 0 0 0.2rem 0.2rem;
|
1071
|
+
border-top: none;
|
1072
|
+
}
|
1073
|
+
|
1074
|
+
/* Ensure consistent sizing for date/time inputs */
|
1075
|
+
input[type="date"].column-filter,
|
1076
|
+
input[type="datetime-local"].column-filter,
|
1077
|
+
input[type="time"].column-filter {
|
1078
|
+
padding: 0.2rem 0.5rem;
|
1079
|
+
}
|
1080
|
+
|
287
1081
|
[data-bs-theme="dark"] .column-filters td {
|
288
1082
|
background-color: rgba(255,255,255,0.05);
|
289
1083
|
}
|
@@ -293,6 +1087,57 @@
|
|
293
1087
|
color: rgba(255,255,255,0.9);
|
294
1088
|
border-color: rgba(255,255,255,0.15);
|
295
1089
|
}
|
1090
|
+
|
1091
|
+
/* Special styling for date/time inputs in dark mode */
|
1092
|
+
[data-bs-theme="dark"] input[type="datetime-local"].column-filter::-webkit-calendar-picker-indicator,
|
1093
|
+
[data-bs-theme="dark"] input[type="date"].column-filter::-webkit-calendar-picker-indicator,
|
1094
|
+
[data-bs-theme="dark"] input[type="time"].column-filter::-webkit-calendar-picker-indicator {
|
1095
|
+
filter: invert(0.8);
|
1096
|
+
}
|
1097
|
+
|
1098
|
+
/* Mini ERD modal styling */
|
1099
|
+
#miniErdModal .modal-dialog {
|
1100
|
+
max-width: 90%;
|
1101
|
+
max-height: 90vh;
|
1102
|
+
height: 90vh;
|
1103
|
+
}
|
1104
|
+
|
1105
|
+
#miniErdModal .modal-content {
|
1106
|
+
height: 100%;
|
1107
|
+
}
|
1108
|
+
|
1109
|
+
#miniErdModal .modal-body {
|
1110
|
+
height: calc(100% - 130px); /* Account for header and footer */
|
1111
|
+
overflow: hidden; /* Prevent scrollbars within the modal body */
|
1112
|
+
}
|
1113
|
+
|
1114
|
+
#miniErdModal #mini-erd-container {
|
1115
|
+
height: 100%;
|
1116
|
+
width: 100%;
|
1117
|
+
}
|
1118
|
+
|
1119
|
+
#miniErdModal #mini-erd-container svg {
|
1120
|
+
width: 100%;
|
1121
|
+
height: 100%;
|
1122
|
+
max-height: unset;
|
1123
|
+
}
|
1124
|
+
|
1125
|
+
/* Foreign key link styling */
|
1126
|
+
.foreign-key-link {
|
1127
|
+
color: var(--bs-primary);
|
1128
|
+
position: relative;
|
1129
|
+
}
|
1130
|
+
|
1131
|
+
.foreign-key-link:hover {
|
1132
|
+
text-decoration: underline !important;
|
1133
|
+
}
|
1134
|
+
|
1135
|
+
.foreign-key-link + .bi-link-45deg {
|
1136
|
+
font-size: 0.75rem;
|
1137
|
+
margin-left: 0.25rem;
|
1138
|
+
position: relative;
|
1139
|
+
top: -1px;
|
1140
|
+
}
|
296
1141
|
</style>
|
297
1142
|
|
298
1143
|
<% if @timestamp_data.present? %>
|
@@ -371,6 +1216,39 @@
|
|
371
1216
|
}
|
372
1217
|
}
|
373
1218
|
});
|
1219
|
+
|
1220
|
+
// Column sorting enhancement
|
1221
|
+
const sortableColumns = document.querySelectorAll('.sortable-column');
|
1222
|
+
sortableColumns.forEach(column => {
|
1223
|
+
const link = column.querySelector('.column-sort-link');
|
1224
|
+
|
1225
|
+
// Mouse over effects
|
1226
|
+
column.addEventListener('mouseenter', () => {
|
1227
|
+
const sortIcon = column.querySelector('.sort-icon');
|
1228
|
+
if (sortIcon && sortIcon.classList.contains('invisible')) {
|
1229
|
+
sortIcon.style.visibility = 'visible';
|
1230
|
+
sortIcon.style.opacity = '0.3';
|
1231
|
+
}
|
1232
|
+
});
|
1233
|
+
|
1234
|
+
column.addEventListener('mouseleave', () => {
|
1235
|
+
const sortIcon = column.querySelector('.sort-icon');
|
1236
|
+
if (sortIcon && sortIcon.classList.contains('invisible')) {
|
1237
|
+
sortIcon.style.visibility = 'hidden';
|
1238
|
+
sortIcon.style.opacity = '0';
|
1239
|
+
}
|
1240
|
+
});
|
1241
|
+
|
1242
|
+
// Keyboard accessibility
|
1243
|
+
if (link) {
|
1244
|
+
link.addEventListener('keydown', (e) => {
|
1245
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
1246
|
+
e.preventDefault();
|
1247
|
+
link.click();
|
1248
|
+
}
|
1249
|
+
});
|
1250
|
+
}
|
1251
|
+
});
|
374
1252
|
});
|
375
1253
|
</script>
|
376
1254
|
<% end %>
|