dbviewer 0.6.6 → 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/README.md +12 -36
- 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 +22 -221
- 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
@@ -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
|
+
});
|