dbviewer 0.3.1
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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +250 -0
- data/Rakefile +8 -0
- data/app/assets/stylesheets/dbviewer/application.css +21 -0
- data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
- data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
- data/app/controllers/concerns/dbviewer/database_operations.rb +354 -0
- data/app/controllers/concerns/dbviewer/error_handling.rb +42 -0
- data/app/controllers/concerns/dbviewer/pagination_concern.rb +43 -0
- data/app/controllers/dbviewer/application_controller.rb +21 -0
- data/app/controllers/dbviewer/databases_controller.rb +0 -0
- data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +24 -0
- data/app/controllers/dbviewer/home_controller.rb +10 -0
- data/app/controllers/dbviewer/logs_controller.rb +39 -0
- data/app/controllers/dbviewer/tables_controller.rb +73 -0
- data/app/helpers/dbviewer/application_helper.rb +118 -0
- data/app/jobs/dbviewer/application_job.rb +4 -0
- data/app/mailers/dbviewer/application_mailer.rb +6 -0
- data/app/models/dbviewer/application_record.rb +5 -0
- 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 +82 -0
- data/app/services/dbviewer/query_storage.rb +0 -0
- data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +564 -0
- data/app/views/dbviewer/home/index.html.erb +237 -0
- data/app/views/dbviewer/logs/index.html.erb +614 -0
- data/app/views/dbviewer/shared/_sidebar.html.erb +177 -0
- data/app/views/dbviewer/tables/_table_structure.html.erb +102 -0
- data/app/views/dbviewer/tables/index.html.erb +128 -0
- data/app/views/dbviewer/tables/query.html.erb +600 -0
- data/app/views/dbviewer/tables/show.html.erb +271 -0
- data/app/views/layouts/dbviewer/application.html.erb +728 -0
- data/config/routes.rb +22 -0
- data/lib/dbviewer/configuration.rb +79 -0
- data/lib/dbviewer/database_manager.rb +450 -0
- data/lib/dbviewer/engine.rb +20 -0
- data/lib/dbviewer/initializer.rb +23 -0
- data/lib/dbviewer/logger.rb +102 -0
- data/lib/dbviewer/query_analyzer.rb +109 -0
- data/lib/dbviewer/query_collection.rb +41 -0
- data/lib/dbviewer/query_parser.rb +82 -0
- data/lib/dbviewer/sql_validator.rb +194 -0
- data/lib/dbviewer/storage/base.rb +31 -0
- data/lib/dbviewer/storage/file_storage.rb +96 -0
- data/lib/dbviewer/storage/in_memory_storage.rb +59 -0
- data/lib/dbviewer/version.rb +3 -0
- data/lib/dbviewer.rb +65 -0
- data/lib/tasks/dbviewer_tasks.rake +4 -0
- metadata +126 -0
@@ -0,0 +1,271 @@
|
|
1
|
+
<% content_for :title do %>
|
2
|
+
Table: <%= @table_name %>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<% content_for :sidebar_active do %>active<% end %>
|
6
|
+
|
7
|
+
<% content_for :sidebar do %>
|
8
|
+
<%= render 'dbviewer/shared/sidebar' %>
|
9
|
+
<% end %>
|
10
|
+
|
11
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
12
|
+
<h1>Table: <%= @table_name %></h1>
|
13
|
+
<div class="d-flex gap-2">
|
14
|
+
<button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#csvExportModal">
|
15
|
+
<i class="bi bi-file-earmark-spreadsheet me-1"></i> Export CSV
|
16
|
+
</button>
|
17
|
+
<%= link_to query_table_path(@table_name), class: "btn btn-primary" do %>
|
18
|
+
<i class="bi bi-code-square me-1"></i> Run SQL Query
|
19
|
+
<% end %>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<!-- CSV Export Modal -->
|
24
|
+
<div class="modal fade" id="csvExportModal" tabindex="-1" aria-labelledby="csvExportModalLabel" aria-hidden="true">
|
25
|
+
<div class="modal-dialog">
|
26
|
+
<div class="modal-content">
|
27
|
+
<div class="modal-header">
|
28
|
+
<h5 class="modal-title" id="csvExportModalLabel">Export <strong><%= @table_name %></strong> to CSV</h5>
|
29
|
+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
30
|
+
</div>
|
31
|
+
<div class="modal-body">
|
32
|
+
<%= form_with url: export_csv_table_path(@table_name), method: :get, id: "csvExportForm" do |form| %>
|
33
|
+
<div class="mb-3">
|
34
|
+
<label for="limit" class="form-label">Maximum number of records</label>
|
35
|
+
<input type="number" class="form-control" id="limit" name="limit" value="10000" min="1" max="100000">
|
36
|
+
<div class="form-text">Limit the number of records to export. Large exports may take some time.</div>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<% if @total_count > 10000 %>
|
40
|
+
<div class="alert alert-warning">
|
41
|
+
<i class="bi bi-exclamation-triangle-fill me-2"></i>
|
42
|
+
This table has <%= number_with_delimiter(@total_count) %> records. Exporting all records may be slow.
|
43
|
+
</div>
|
44
|
+
<% end %>
|
45
|
+
|
46
|
+
<div class="mb-3 form-check">
|
47
|
+
<input type="checkbox" class="form-check-input" id="includeHeaders" name="include_headers" checked>
|
48
|
+
<label class="form-check-label" for="includeHeaders">Include column headers</label>
|
49
|
+
</div>
|
50
|
+
<% end %>
|
51
|
+
</div>
|
52
|
+
<div class="modal-footer">
|
53
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
54
|
+
<button type="submit" form="csvExportForm" class="btn btn-success">
|
55
|
+
<i class="bi bi-download me-1"></i> Export CSV
|
56
|
+
</button>
|
57
|
+
</div>
|
58
|
+
</div>
|
59
|
+
</div>
|
60
|
+
</div>
|
61
|
+
|
62
|
+
<!-- Records Section -->
|
63
|
+
<div class="dbviewer-card card mb-4">
|
64
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
65
|
+
<h5 class="mb-0"><i class="bi bi-table me-2"></i>Records</h5>
|
66
|
+
<div class="d-flex align-items-center">
|
67
|
+
<div class="me-3">
|
68
|
+
<label for="per-page-select" class="me-2">Per page:</label>
|
69
|
+
<select id="per-page-select" class="form-select form-select-sm" onchange="window.location.href='<%= table_path(@table_name) %>?per_page=' + this.value + '&page=1&order_by=<%= @order_by %>&order_direction=<%= @order_direction %>'">
|
70
|
+
<% Dbviewer::TablesController.per_page_options.each do |option| %>
|
71
|
+
<option value="<%= option %>" <%= 'selected' if @per_page == option %>><%= option %></option>
|
72
|
+
<% end %>
|
73
|
+
</select>
|
74
|
+
</div>
|
75
|
+
<span class="badge bg-secondary">Total: <%= @total_count %> records</span>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
<div class="card-body p-0">
|
79
|
+
<div class="table-responsive dbviewer-scrollable">
|
80
|
+
<table class="table table-bordered table-striped rounded-none">
|
81
|
+
<% if @records.present? && @records.columns.any? %>
|
82
|
+
<thead class="dbviewer-table-header">
|
83
|
+
<tr>
|
84
|
+
<% @records.columns.each do |column_name| %>
|
85
|
+
<th>
|
86
|
+
<%= column_name %>
|
87
|
+
</th>
|
88
|
+
<% end %>
|
89
|
+
</tr>
|
90
|
+
</thead>
|
91
|
+
<tbody>
|
92
|
+
<% @records.rows.each do |row| %>
|
93
|
+
<tr>
|
94
|
+
<% row.each do |cell| %>
|
95
|
+
<% cell_value = format_cell_value(cell) %>
|
96
|
+
<td title="<%= cell_value %>"><%= cell_value %></td>
|
97
|
+
<% end %>
|
98
|
+
</tr>
|
99
|
+
<% end %>
|
100
|
+
</tbody>
|
101
|
+
<% else %>
|
102
|
+
<tr>
|
103
|
+
<td colspan="100%">No records found or table is empty.</td>
|
104
|
+
</tr>
|
105
|
+
<% end %>
|
106
|
+
</table>
|
107
|
+
</div>
|
108
|
+
|
109
|
+
<% if @total_pages > 1 %>
|
110
|
+
<nav aria-label="Page navigation">
|
111
|
+
<ul class="pagination justify-content-center">
|
112
|
+
<li class="page-item <%= 'disabled' if @current_page == 1 %>">
|
113
|
+
<%= link_to '«', table_path(@table_name, page: [@current_page - 1, 1].max, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: 'page-link' %>
|
114
|
+
</li>
|
115
|
+
|
116
|
+
<% start_page = [1, @current_page - 2].max %>
|
117
|
+
<% end_page = [start_page + 4, @total_pages].min %>
|
118
|
+
<% start_page = [1, end_page - 4].max %>
|
119
|
+
|
120
|
+
<% (start_page..end_page).each do |page_num| %>
|
121
|
+
<li class="page-item <%= 'active' if page_num == @current_page %>">
|
122
|
+
<%= link_to page_num, table_path(@table_name, page: page_num, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: 'page-link' %>
|
123
|
+
</li>
|
124
|
+
<% end %>
|
125
|
+
|
126
|
+
<li class="page-item <%= 'disabled' if @current_page == @total_pages %>">
|
127
|
+
<%= link_to '»', table_path(@table_name, page: [@current_page + 1, @total_pages].min, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: 'page-link' %>
|
128
|
+
</li>
|
129
|
+
</ul>
|
130
|
+
</nav>
|
131
|
+
<% end %>
|
132
|
+
</div>
|
133
|
+
</div>
|
134
|
+
</div>
|
135
|
+
|
136
|
+
<!-- Two-column layout for Timeline and Structure -->
|
137
|
+
<div class="row two-column-layout">
|
138
|
+
<!-- Timeline Column -->
|
139
|
+
<div class="col-md-6 mb-4">
|
140
|
+
<% if @timestamp_data.present? %>
|
141
|
+
<div class="dbviewer-card card h-100">
|
142
|
+
<div class="card-header d-flex justify-content-between align-items-center">
|
143
|
+
<h5 class="mb-0"><i class="bi bi-graph-up me-2"></i>Record Creation Timeline</h5>
|
144
|
+
<div>
|
145
|
+
<div class="btn-group btn-group-sm" role="group" aria-label="Time grouping">
|
146
|
+
<%= link_to "Hourly", table_path(@table_name, time_group: "hourly", page: @current_page, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: "btn btn-outline-primary #{@time_grouping == 'hourly' ? 'active' : ''}" %>
|
147
|
+
<%= link_to "Daily", table_path(@table_name, time_group: "daily", page: @current_page, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: "btn btn-outline-primary #{@time_grouping == 'daily' ? 'active' : ''}" %>
|
148
|
+
<%= link_to "Weekly", table_path(@table_name, time_group: "weekly", page: @current_page, order_by: @order_by, order_direction: @order_direction, per_page: @per_page), class: "btn btn-outline-primary #{@time_grouping == 'weekly' ? 'active' : ''}" %>
|
149
|
+
</div>
|
150
|
+
</div>
|
151
|
+
</div>
|
152
|
+
<div class="card-body">
|
153
|
+
<div class="chart-container">
|
154
|
+
<canvas id="timestampChart"></canvas>
|
155
|
+
</div>
|
156
|
+
<div class="mt-3 text-center">
|
157
|
+
<small class="text-muted">
|
158
|
+
<i class="bi bi-info-circle"></i>
|
159
|
+
Timeline shows <%= @time_grouping %> record creation patterns based on <code>created_at</code> column.
|
160
|
+
</small>
|
161
|
+
</div>
|
162
|
+
</div>
|
163
|
+
</div>
|
164
|
+
<% else %>
|
165
|
+
<div class="dbviewer-card card h-100">
|
166
|
+
<div class="card-header">
|
167
|
+
<h5 class="mb-0"><i class="bi bi-info-circle me-2"></i>Creation Timeline</h5>
|
168
|
+
</div>
|
169
|
+
<div class="card-body d-flex justify-content-center align-items-center text-center text-muted">
|
170
|
+
<div>
|
171
|
+
<i class="bi bi-calendar-x display-4 mb-3"></i>
|
172
|
+
<p>No creation timestamp data available for this table.</p>
|
173
|
+
<small>Timeline visualization is only available for tables with a <code>created_at</code> column.</small>
|
174
|
+
</div>
|
175
|
+
</div>
|
176
|
+
</div>
|
177
|
+
<% end %>
|
178
|
+
</div>
|
179
|
+
|
180
|
+
<!-- Structure Column -->
|
181
|
+
<div class="col-md-6 mb-4">
|
182
|
+
<div class="dbviewer-card card h-100">
|
183
|
+
<div class="card-header">
|
184
|
+
<h5 class="mb-0"><i class="bi bi-diagram-3 me-2"></i>Table Structure</h5>
|
185
|
+
</div>
|
186
|
+
<div class="card-body structure-container">
|
187
|
+
<%= render 'table_structure' %>
|
188
|
+
</div>
|
189
|
+
</div>
|
190
|
+
</div>
|
191
|
+
</div>
|
192
|
+
|
193
|
+
<% if @timestamp_data.present? %>
|
194
|
+
<script>
|
195
|
+
document.addEventListener('DOMContentLoaded', function() {
|
196
|
+
const timeGrouping = '<%= @time_grouping %>';
|
197
|
+
const chartData = <%= raw @timestamp_data.to_json %>;
|
198
|
+
|
199
|
+
// Reverse the data so it's chronological
|
200
|
+
const labels = chartData.map(item => item.label).reverse();
|
201
|
+
const values = chartData.map(item => item.value).reverse();
|
202
|
+
|
203
|
+
// Chart colors based on time grouping
|
204
|
+
let chartColor;
|
205
|
+
let chartTitle;
|
206
|
+
|
207
|
+
switch(timeGrouping) {
|
208
|
+
case 'hourly':
|
209
|
+
chartColor = 'rgba(75, 192, 192, 0.7)';
|
210
|
+
chartTitle = 'Hourly Record Creation';
|
211
|
+
break;
|
212
|
+
case 'weekly':
|
213
|
+
chartColor = 'rgba(153, 102, 255, 0.7)';
|
214
|
+
chartTitle = 'Weekly Record Creation';
|
215
|
+
break;
|
216
|
+
default:
|
217
|
+
chartColor = 'rgba(54, 162, 235, 0.7)';
|
218
|
+
chartTitle = 'Daily Record Creation';
|
219
|
+
}
|
220
|
+
|
221
|
+
const ctx = document.getElementById('timestampChart').getContext('2d');
|
222
|
+
new Chart(ctx, {
|
223
|
+
type: 'bar',
|
224
|
+
data: {
|
225
|
+
labels: labels,
|
226
|
+
datasets: [{
|
227
|
+
label: 'Records Created',
|
228
|
+
data: values,
|
229
|
+
backgroundColor: chartColor,
|
230
|
+
borderColor: chartColor.replace('0.7', '1.0'),
|
231
|
+
borderWidth: 1
|
232
|
+
}]
|
233
|
+
},
|
234
|
+
options: {
|
235
|
+
responsive: true,
|
236
|
+
maintainAspectRatio: false,
|
237
|
+
plugins: {
|
238
|
+
legend: {
|
239
|
+
display: false
|
240
|
+
},
|
241
|
+
title: {
|
242
|
+
display: true,
|
243
|
+
text: chartTitle,
|
244
|
+
font: {
|
245
|
+
size: 16
|
246
|
+
}
|
247
|
+
}
|
248
|
+
},
|
249
|
+
scales: {
|
250
|
+
y: {
|
251
|
+
beginAtZero: true,
|
252
|
+
title: {
|
253
|
+
display: true,
|
254
|
+
text: 'Number of Records'
|
255
|
+
},
|
256
|
+
ticks: {
|
257
|
+
precision: 0
|
258
|
+
}
|
259
|
+
},
|
260
|
+
x: {
|
261
|
+
title: {
|
262
|
+
display: true,
|
263
|
+
text: timeGrouping.charAt(0).toUpperCase() + timeGrouping.slice(1)
|
264
|
+
}
|
265
|
+
}
|
266
|
+
}
|
267
|
+
}
|
268
|
+
});
|
269
|
+
});
|
270
|
+
</script>
|
271
|
+
<% end %>
|