dbviewer 0.6.7 → 0.7.0

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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +553 -0
  3. data/app/assets/javascripts/dbviewer/home.js +287 -0
  4. data/app/assets/javascripts/dbviewer/layout.js +194 -0
  5. data/app/assets/javascripts/dbviewer/query.js +277 -0
  6. data/app/assets/javascripts/dbviewer/table.js +1563 -0
  7. data/app/assets/stylesheets/dbviewer/application.css +1460 -21
  8. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +181 -0
  9. data/app/assets/stylesheets/dbviewer/home.css +229 -0
  10. data/app/assets/stylesheets/dbviewer/logs.css +64 -0
  11. data/app/assets/stylesheets/dbviewer/query.css +171 -0
  12. data/app/assets/stylesheets/dbviewer/table.css +1144 -0
  13. data/app/views/dbviewer/connections/index.html.erb +0 -30
  14. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +14 -713
  15. data/app/views/dbviewer/home/index.html.erb +9 -499
  16. data/app/views/dbviewer/logs/index.html.erb +5 -220
  17. data/app/views/dbviewer/tables/index.html.erb +0 -65
  18. data/app/views/dbviewer/tables/query.html.erb +129 -565
  19. data/app/views/dbviewer/tables/show.html.erb +4 -2429
  20. data/app/views/layouts/dbviewer/application.html.erb +13 -1544
  21. data/lib/dbviewer/version.rb +1 -1
  22. metadata +12 -7
  23. data/app/assets/javascripts/dbviewer/connections.js +0 -70
  24. data/app/assets/stylesheets/dbviewer/dbviewer.css +0 -0
  25. data/app/assets/stylesheets/dbviewer/enhanced.css +0 -0
  26. data/app/views/dbviewer/connections/new.html.erb +0 -79
  27. data/app/views/dbviewer/tables/mini_erd.html.erb +0 -517
@@ -0,0 +1,287 @@
1
+ document.addEventListener("DOMContentLoaded", function () {
2
+ // Helper function to format numbers with commas
3
+ function numberWithDelimiter(number) {
4
+ return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
5
+ }
6
+
7
+ // Helper function to format file sizes
8
+ function numberToHumanSize(bytes) {
9
+ if (bytes === null || bytes === undefined) return "N/A";
10
+ if (bytes === 0) return "0 Bytes";
11
+
12
+ const k = 1024;
13
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
14
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
15
+
16
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
17
+ }
18
+
19
+ // Function to update analytics cards
20
+ function updateTablesCount(data) {
21
+ document.getElementById("tables-loading").classList.add("d-none");
22
+ document.getElementById("tables-count").classList.remove("d-none");
23
+ document.getElementById("tables-count").textContent =
24
+ data.total_tables || 0;
25
+ }
26
+
27
+ function updateRelationshipsCount(data) {
28
+ document.getElementById("relationships-loading").classList.add("d-none");
29
+ document.getElementById("relationships-count").classList.remove("d-none");
30
+ document.getElementById("relationships-count").textContent =
31
+ data.total_relationships || 0;
32
+ }
33
+
34
+ function updateDatabaseSize(data) {
35
+ document.getElementById("size-loading").classList.add("d-none");
36
+ document.getElementById("size-count").classList.remove("d-none");
37
+ document.getElementById("size-count").textContent = numberToHumanSize(
38
+ data.schema_size
39
+ );
40
+ }
41
+
42
+ function updateRecordsData(recordsData) {
43
+ // Update records count
44
+ document.getElementById("records-loading").classList.add("d-none");
45
+ document.getElementById("records-count").classList.remove("d-none");
46
+ document.getElementById("records-count").textContent = numberWithDelimiter(
47
+ recordsData.total_records || 0
48
+ );
49
+
50
+ // Update largest tables
51
+ updateLargestTables(recordsData);
52
+ }
53
+
54
+ // Function to update largest tables
55
+ function updateLargestTables(data) {
56
+ const container = document.getElementById("largest-tables-container");
57
+
58
+ if (data.largest_tables && data.largest_tables.length > 0) {
59
+ const tableHtml = `
60
+ <div class="table-responsive">
61
+ <table class="table table-sm table-hover">
62
+ <thead>
63
+ <tr>
64
+ <th>Table Name</th>
65
+ <th class="text-end">Records</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ ${data.largest_tables
70
+ .map(
71
+ (table) => `
72
+ <tr>
73
+ <td>
74
+ <a href="${
75
+ window.location.origin
76
+ }${window.location.pathname.replace(/\/$/, "")}/tables/${
77
+ table.name
78
+ }">
79
+ ${table.name}
80
+ </a>
81
+ </td>
82
+ <td class="text-end">${numberWithDelimiter(
83
+ table.record_count
84
+ )}</td>
85
+ </tr>
86
+ `
87
+ )
88
+ .join("")}
89
+ </tbody>
90
+ </table>
91
+ </div>
92
+ `;
93
+ container.innerHTML = tableHtml;
94
+ } else {
95
+ container.innerHTML = `
96
+ <div class="text-center my-4 empty-data-message">
97
+ <p>No table data available</p>
98
+ </div>
99
+ `;
100
+ }
101
+ }
102
+
103
+ // Function to update recent queries
104
+ function updateRecentQueries(data) {
105
+ const container = document.getElementById("recent-queries-container");
106
+ const linkContainer = document.getElementById("queries-view-all-link");
107
+
108
+ if (data.enabled) {
109
+ // Show "View All Logs" link if query logging is enabled
110
+ linkContainer.innerHTML = `
111
+ <a href="${window.location.origin}${window.location.pathname.replace(
112
+ /\/$/,
113
+ ""
114
+ )}/logs" class="btn btn-sm btn-primary">View All Logs</a>
115
+ `;
116
+ linkContainer.classList.remove("d-none");
117
+
118
+ if (data.queries && data.queries.length > 0) {
119
+ const tableHtml = `
120
+ <div class="table-responsive">
121
+ <table class="table table-sm table-hover mb-0">
122
+ <thead>
123
+ <tr>
124
+ <th>Query</th>
125
+ <th class="text-end" style="width: 120px">Duration</th>
126
+ <th class="text-end" style="width: 180px">Time</th>
127
+ </tr>
128
+ </thead>
129
+ <tbody>
130
+ ${data.queries
131
+ .map((query) => {
132
+ const duration = query.duration_ms;
133
+ const durationClass =
134
+ duration > 100 ? "query-duration-slow" : "query-duration";
135
+ const timestamp = new Date(query.timestamp);
136
+ const timeString = timestamp.toLocaleTimeString();
137
+
138
+ return `
139
+ <tr>
140
+ <td class="text-truncate" style="max-width: 500px;">
141
+ <code class="sql-query-code">${query.sql}</code>
142
+ </td>
143
+ <td class="text-end">
144
+ <span class="${durationClass}">
145
+ ${duration} ms
146
+ </span>
147
+ </td>
148
+ <td class="text-end query-timestamp">
149
+ <small>${timeString}</small>
150
+ </td>
151
+ </tr>
152
+ `;
153
+ })
154
+ .join("")}
155
+ </tbody>
156
+ </table>
157
+ </div>
158
+ `;
159
+ container.innerHTML = tableHtml;
160
+ } else {
161
+ container.innerHTML = `
162
+ <div class="text-center my-4 empty-data-message">
163
+ <p>No queries recorded yet</p>
164
+ </div>
165
+ `;
166
+ }
167
+ } else {
168
+ container.innerHTML = `
169
+ <div class="text-center my-4 empty-data-message">
170
+ <p>Query logging is disabled</p>
171
+ <small class="text-muted">Enable it in the configuration to see SQL queries here</small>
172
+ </div>
173
+ `;
174
+ }
175
+ }
176
+
177
+ // Function to show error state
178
+ function showError(containerId, message) {
179
+ const container = document.getElementById(containerId);
180
+ container.innerHTML = `
181
+ <div class="text-center my-4 text-danger">
182
+ <i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
183
+ <p>Error loading data</p>
184
+ <small>${message}</small>
185
+ </div>
186
+ `;
187
+ }
188
+
189
+ // Load tables count data
190
+ fetch(document.getElementById("api_tables_path").value, {
191
+ headers: {
192
+ Accept: "application/json",
193
+ "X-Requested-With": "XMLHttpRequest",
194
+ },
195
+ })
196
+ .then((response) => {
197
+ if (!response.ok) {
198
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
199
+ }
200
+ return response.json();
201
+ })
202
+ .then((data) => {
203
+ updateTablesCount(data);
204
+ })
205
+ .catch((error) => {
206
+ console.error("Error loading tables count:", error);
207
+ const loading = document.getElementById("tables-loading");
208
+ const count = document.getElementById("tables-count");
209
+ loading.classList.add("d-none");
210
+ count.classList.remove("d-none");
211
+ count.innerHTML = '<span class="text-danger">Error</span>';
212
+ });
213
+
214
+ // Load database size data
215
+ fetch(document.getElementById("size_api_database_path").value, {
216
+ headers: {
217
+ Accept: "application/json",
218
+ "X-Requested-With": "XMLHttpRequest",
219
+ },
220
+ })
221
+ .then((response) => {
222
+ if (!response.ok) {
223
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
224
+ }
225
+ return response.json();
226
+ })
227
+ .then((data) => {
228
+ updateDatabaseSize(data);
229
+ })
230
+ .catch((error) => {
231
+ console.error("Error loading database size:", error);
232
+ const loading = document.getElementById("size-loading");
233
+ const count = document.getElementById("size-count");
234
+ loading.classList.add("d-none");
235
+ count.classList.remove("d-none");
236
+ count.innerHTML = '<span class="text-danger">Error</span>';
237
+ });
238
+
239
+ // Load records data separately
240
+ fetch(document.getElementById("records_api_tables_path").value, {
241
+ headers: {
242
+ Accept: "application/json",
243
+ "X-Requested-With": "XMLHttpRequest",
244
+ },
245
+ })
246
+ .then((response) => {
247
+ if (!response.ok) {
248
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
249
+ }
250
+ return response.json();
251
+ })
252
+ .then((recordsData) => {
253
+ updateRecordsData(recordsData);
254
+ })
255
+ .catch((error) => {
256
+ console.error("Error loading records data:", error);
257
+ // Update records-related UI with error state
258
+ const recordsLoading = document.getElementById("records-loading");
259
+ const recordsCount = document.getElementById("records-count");
260
+ recordsLoading.classList.add("d-none");
261
+ recordsCount.classList.remove("d-none");
262
+ recordsCount.innerHTML = '<span class="text-danger">Error</span>';
263
+
264
+ showError("largest-tables-container", error.message);
265
+ });
266
+
267
+ // Load recent queries data
268
+ fetch(document.getElementById("recent_api_queries_path").value, {
269
+ headers: {
270
+ Accept: "application/json",
271
+ "X-Requested-With": "XMLHttpRequest",
272
+ },
273
+ })
274
+ .then((response) => {
275
+ if (!response.ok) {
276
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
277
+ }
278
+ return response.json();
279
+ })
280
+ .then((data) => {
281
+ updateRecentQueries(data);
282
+ })
283
+ .catch((error) => {
284
+ console.error("Error loading recent queries:", error);
285
+ showError("recent-queries-container", error.message);
286
+ });
287
+ });
@@ -0,0 +1,194 @@
1
+ document.addEventListener("DOMContentLoaded", function () {
2
+ // Theme toggle functionality
3
+ const themeToggleBtn = document.querySelector(".theme-toggle");
4
+ const htmlElement = document.documentElement;
5
+
6
+ // Check for saved theme preference or respect OS preference
7
+ const prefersDarkMode = window.matchMedia(
8
+ "(prefers-color-scheme: dark)"
9
+ ).matches;
10
+ const savedTheme = localStorage.getItem("dbviewerTheme");
11
+
12
+ // Set initial theme
13
+ if (savedTheme) {
14
+ htmlElement.setAttribute("data-bs-theme", savedTheme);
15
+ } else if (prefersDarkMode) {
16
+ htmlElement.setAttribute("data-bs-theme", "dark");
17
+ localStorage.setItem("dbviewerTheme", "dark");
18
+ }
19
+
20
+ // Toggle theme when button is clicked
21
+ if (themeToggleBtn) {
22
+ themeToggleBtn.addEventListener("click", function () {
23
+ const currentTheme = htmlElement.getAttribute("data-bs-theme");
24
+ const newTheme = currentTheme === "dark" ? "light" : "dark";
25
+
26
+ // Update theme
27
+ htmlElement.setAttribute("data-bs-theme", newTheme);
28
+ localStorage.setItem("dbviewerTheme", newTheme);
29
+
30
+ // Dispatch event for other components to respond to theme change (Monaco editor)
31
+ const themeChangeEvent = new CustomEvent("dbviewerThemeChanged", {
32
+ detail: { theme: newTheme },
33
+ });
34
+ document.dispatchEvent(themeChangeEvent);
35
+ });
36
+ }
37
+
38
+ // Check if styles are loaded properly
39
+ const styleCheck = getComputedStyle(
40
+ document.documentElement
41
+ ).getPropertyValue("--dbviewer-styles-loaded");
42
+ if (!styleCheck) {
43
+ console.log(
44
+ "DBViewer: Using fallback inline styles (asset pipeline may not be available)"
45
+ );
46
+ } else {
47
+ console.log("DBViewer: External CSS loaded successfully");
48
+ }
49
+
50
+ const toggleBtn = document.querySelector(".dbviewer-sidebar-toggle");
51
+ const closeBtn = document.querySelector(".dbviewer-sidebar-close");
52
+ const sidebar = document.querySelector(".dbviewer-sidebar");
53
+ const overlay = document.createElement("div");
54
+
55
+ // Create and configure overlay for mobile
56
+ overlay.className = "dbviewer-sidebar-overlay";
57
+ document.body.appendChild(overlay);
58
+
59
+ function showSidebar() {
60
+ sidebar.classList.add("active");
61
+ document.body.classList.add("dbviewer-sidebar-open");
62
+ setTimeout(() => {
63
+ overlay.classList.add("active");
64
+ }, 50);
65
+ }
66
+
67
+ function hideSidebar() {
68
+ sidebar.classList.remove("active");
69
+ overlay.classList.remove("active");
70
+ setTimeout(() => {
71
+ document.body.classList.remove("dbviewer-sidebar-open");
72
+ }, 300);
73
+ }
74
+
75
+ if (toggleBtn) {
76
+ toggleBtn.addEventListener("click", function () {
77
+ if (sidebar.classList.contains("active")) {
78
+ hideSidebar();
79
+ } else {
80
+ showSidebar();
81
+ // Focus the search input when sidebar becomes visible
82
+ setTimeout(() => {
83
+ const searchInput = document.getElementById("tableSearch");
84
+ if (searchInput) searchInput.focus();
85
+ }, 300); // Small delay to allow for animation
86
+ }
87
+ });
88
+ }
89
+
90
+ if (closeBtn) {
91
+ closeBtn.addEventListener("click", function () {
92
+ hideSidebar();
93
+ });
94
+ }
95
+
96
+ overlay.addEventListener("click", function () {
97
+ hideSidebar();
98
+ });
99
+
100
+ // Close sidebar on window resize (from mobile to desktop)
101
+ let resizeTimer;
102
+ window.addEventListener("resize", function () {
103
+ clearTimeout(resizeTimer);
104
+ resizeTimer = setTimeout(function () {
105
+ if (window.innerWidth >= 992 && sidebar.classList.contains("active")) {
106
+ overlay.classList.remove("active");
107
+ }
108
+ }, 250);
109
+ });
110
+
111
+ // Offcanvas enhancement for theme synchronization
112
+ const offcanvasElement = document.getElementById("navbarOffcanvas");
113
+ if (offcanvasElement) {
114
+ // Get all theme toggles
115
+ const allThemeToggles = document.querySelectorAll(".theme-toggle");
116
+
117
+ // Handle theme change from any toggle button
118
+ allThemeToggles.forEach((toggleBtn) => {
119
+ toggleBtn.addEventListener("click", function () {
120
+ const currentTheme =
121
+ document.documentElement.getAttribute("data-bs-theme") || "light";
122
+
123
+ // Update all theme toggle buttons to maintain consistency
124
+ allThemeToggles.forEach((btn) => {
125
+ // Update icon in all theme toggle buttons
126
+ if (currentTheme === "dark") {
127
+ btn.querySelector("span").innerHTML =
128
+ '<i class="bi bi-sun-fill"></i>';
129
+ btn.setAttribute("aria-label", "Switch to light mode");
130
+ } else {
131
+ btn.querySelector("span").innerHTML =
132
+ '<i class="bi bi-moon-fill"></i>';
133
+ btn.setAttribute("aria-label", "Switch to dark mode");
134
+ }
135
+ });
136
+ });
137
+ });
138
+
139
+ // Function to sync offcanvas colors with current theme
140
+ function syncOffcanvasWithTheme() {
141
+ const currentTheme =
142
+ document.documentElement.getAttribute("data-bs-theme") || "light";
143
+ if (currentTheme === "dark") {
144
+ offcanvasElement
145
+ .querySelector(".offcanvas-header")
146
+ .classList.remove("bg-light-subtle");
147
+ offcanvasElement
148
+ .querySelector(".offcanvas-header")
149
+ .classList.add("bg-dark-subtle");
150
+ } else {
151
+ offcanvasElement
152
+ .querySelector(".offcanvas-header")
153
+ .classList.remove("bg-dark-subtle");
154
+ offcanvasElement
155
+ .querySelector(".offcanvas-header")
156
+ .classList.add("bg-light-subtle");
157
+ }
158
+ }
159
+
160
+ // Sync on page load
161
+ document.addEventListener("DOMContentLoaded", syncOffcanvasWithTheme);
162
+
163
+ // Listen for theme changes
164
+ document.addEventListener("dbviewerThemeChanged", syncOffcanvasWithTheme);
165
+
166
+ // Handle link click in offcanvas (auto-close on mobile)
167
+ const offcanvasLinks = offcanvasElement.querySelectorAll(
168
+ ".nav-link:not(.dropdown-toggle)"
169
+ );
170
+ offcanvasLinks.forEach((link) => {
171
+ link.addEventListener("click", function () {
172
+ if (window.innerWidth < 992) {
173
+ bootstrap.Offcanvas.getInstance(offcanvasElement).hide();
174
+ }
175
+ });
176
+ });
177
+
178
+ // Fix offcanvas backdrop on desktop
179
+ window.addEventListener("resize", function () {
180
+ if (window.innerWidth >= 992) {
181
+ const offcanvasInstance =
182
+ bootstrap.Offcanvas.getInstance(offcanvasElement);
183
+ if (offcanvasInstance) {
184
+ offcanvasInstance.hide();
185
+ }
186
+ // Also remove any backdrop
187
+ const backdrop = document.querySelector(".offcanvas-backdrop");
188
+ if (backdrop) {
189
+ backdrop.remove();
190
+ }
191
+ }
192
+ });
193
+ }
194
+ });