dbviewer 0.7.10 → 0.7.11

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -0
  3. data/app/assets/images/dbviewer/emoji-favicon.txt +1 -0
  4. data/app/assets/images/dbviewer/favicon.ico +4 -0
  5. data/app/assets/images/dbviewer/favicon.png +4 -0
  6. data/app/assets/images/dbviewer/favicon.svg +10 -0
  7. data/app/assets/javascripts/dbviewer/entity_relationship_diagram.js +38 -42
  8. data/app/assets/javascripts/dbviewer/error_handler.js +58 -0
  9. data/app/assets/javascripts/dbviewer/home.js +25 -34
  10. data/app/assets/javascripts/dbviewer/layout.js +100 -129
  11. data/app/assets/javascripts/dbviewer/query.js +309 -246
  12. data/app/assets/javascripts/dbviewer/sidebar.js +170 -183
  13. data/app/assets/javascripts/dbviewer/utility.js +124 -0
  14. data/app/assets/stylesheets/dbviewer/application.css +8 -146
  15. data/app/assets/stylesheets/dbviewer/entity_relationship_diagram.css +0 -34
  16. data/app/assets/stylesheets/dbviewer/logs.css +0 -11
  17. data/app/assets/stylesheets/dbviewer/query.css +21 -9
  18. data/app/assets/stylesheets/dbviewer/table.css +49 -131
  19. data/app/controllers/concerns/dbviewer/database_operations/connection_management.rb +90 -0
  20. data/app/controllers/concerns/dbviewer/database_operations/data_export.rb +31 -0
  21. data/app/controllers/concerns/dbviewer/database_operations/database_information.rb +54 -0
  22. data/app/controllers/concerns/dbviewer/database_operations/datatable_operations.rb +37 -0
  23. data/app/controllers/concerns/dbviewer/database_operations/query_operations.rb +37 -0
  24. data/app/controllers/concerns/dbviewer/database_operations/relationship_management.rb +175 -0
  25. data/app/controllers/concerns/dbviewer/database_operations/table_operations.rb +46 -0
  26. data/app/controllers/concerns/dbviewer/database_operations.rb +4 -9
  27. data/app/controllers/dbviewer/api/tables_controller.rb +12 -0
  28. data/app/controllers/dbviewer/entity_relationship_diagrams_controller.rb +0 -15
  29. data/app/controllers/dbviewer/tables_controller.rb +4 -33
  30. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +1 -1
  31. data/app/views/dbviewer/tables/query.html.erb +21 -6
  32. data/app/views/dbviewer/tables/show.html.erb +2 -2
  33. data/app/views/layouts/dbviewer/application.html.erb +12 -3
  34. data/config/routes.rb +2 -2
  35. data/lib/dbviewer/database/manager.rb +2 -2
  36. data/lib/dbviewer/datatable/query_operations.rb +1 -17
  37. data/lib/dbviewer/engine.rb +29 -0
  38. data/lib/dbviewer/version.rb +1 -1
  39. metadata +15 -10
  40. data/app/controllers/concerns/dbviewer/connection_management.rb +0 -88
  41. data/app/controllers/concerns/dbviewer/data_export.rb +0 -32
  42. data/app/controllers/concerns/dbviewer/database_information.rb +0 -62
  43. data/app/controllers/concerns/dbviewer/datatable_support.rb +0 -47
  44. data/app/controllers/concerns/dbviewer/pagination_concern.rb +0 -34
  45. data/app/controllers/concerns/dbviewer/query_operations.rb +0 -28
  46. data/app/controllers/concerns/dbviewer/relationship_management.rb +0 -173
  47. data/app/controllers/concerns/dbviewer/table_operations.rb +0 -56
@@ -1,4 +1,14 @@
1
1
  document.addEventListener("DOMContentLoaded", function () {
2
+ // Validate that required utility scripts have loaded
3
+ if (!window.DBViewer || !DBViewer.Utility) {
4
+ console.error(
5
+ "Required DBViewer utility scripts not loaded. Please check utility.js."
6
+ );
7
+ return;
8
+ }
9
+
10
+ // Get debounce from the global namespace
11
+ const { debounce } = DBViewer.Utility;
2
12
  const searchInput = document.getElementById("tableSearch");
3
13
  const sidebarContent = document.querySelector(".dbviewer-sidebar-content");
4
14
 
@@ -8,203 +18,180 @@ document.addEventListener("DOMContentLoaded", function () {
8
18
  scrollPosition: "dbviewer_sidebar_scroll_position",
9
19
  };
10
20
 
11
- if (searchInput) {
12
- // Debounce function to limit how often the filter runs
13
- function debounce(func, wait) {
14
- let timeout;
15
- return function () {
16
- const context = this;
17
- const args = arguments;
18
- clearTimeout(timeout);
19
- timeout = setTimeout(function () {
20
- func.apply(context, args);
21
- }, wait);
22
- };
23
- }
21
+ // Filter function
22
+ const filterTables = debounce(function () {
23
+ const query = searchInput.value.toLowerCase();
24
+ const tableItems = document.querySelectorAll(
25
+ "#tablesList .list-group-item-action"
26
+ );
27
+ let visibleCount = 0;
24
28
 
25
- // Filter function
26
- const filterTables = debounce(function () {
27
- const query = searchInput.value.toLowerCase();
28
- const tableItems = document.querySelectorAll(
29
- "#tablesList .list-group-item-action"
30
- );
31
- let visibleCount = 0;
32
-
33
- // Save the current search filter to localStorage
34
- localStorage.setItem(STORAGE_KEYS.searchFilter, searchInput.value);
35
-
36
- tableItems.forEach(function (item) {
37
- // Get the table name from the title attribute for more accurate matching
38
- const tableName = (item.getAttribute("title") || item.textContent)
39
- .trim()
40
- .toLowerCase();
41
-
42
- // Also get the displayed text content for a broader match
43
- const displayedText = item.textContent.trim().toLowerCase();
44
-
45
- if (tableName.includes(query) || displayedText.includes(query)) {
46
- item.classList.remove("d-none");
47
- visibleCount++;
48
- } else {
49
- item.classList.add("d-none");
50
- }
51
- });
52
-
53
- // Update the tables count in the sidebar
54
- const tableCountElement = document.getElementById("table-count");
55
- if (tableCountElement) {
56
- tableCountElement.textContent = visibleCount;
57
- }
29
+ // Save the current search filter to localStorage
30
+ localStorage.setItem(STORAGE_KEYS.searchFilter, searchInput.value);
31
+
32
+ tableItems.forEach(function (item) {
33
+ // Get the table name from the title attribute for more accurate matching
34
+ const tableName = (item.getAttribute("title") || item.textContent)
35
+ .trim()
36
+ .toLowerCase();
58
37
 
59
- // Show/hide no results message
60
- let noResultsEl = document.getElementById("dbviewer-no-filter-results");
61
- if (visibleCount === 0 && query !== "") {
62
- if (!noResultsEl) {
63
- noResultsEl = document.createElement("div");
64
- noResultsEl.id = "dbviewer-no-filter-results";
65
- noResultsEl.className = "list-group-item text-muted text-center py-3";
66
- noResultsEl.innerHTML =
67
- '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
68
- document.getElementById("tablesList").appendChild(noResultsEl);
69
- }
70
- noResultsEl.querySelector(".fw-bold").textContent = query;
71
- noResultsEl.style.display = "block";
72
- } else if (noResultsEl) {
73
- noResultsEl.style.display = "none";
38
+ // Also get the displayed text content for a broader match
39
+ const displayedText = item.textContent.trim().toLowerCase();
40
+
41
+ if (tableName.includes(query) || displayedText.includes(query)) {
42
+ item.classList.remove("d-none");
43
+ visibleCount++;
44
+ } else {
45
+ item.classList.add("d-none");
74
46
  }
75
- }, 150); // Debounce for 150ms
76
-
77
- // Set up clear button first
78
- const clearButton = document.createElement("button");
79
- clearButton.type = "button";
80
- clearButton.className = "btn btn-sm btn-link position-absolute";
81
- clearButton.style.right = "15px";
82
- clearButton.style.top = "50%";
83
- clearButton.style.transform = "translateY(-50%)";
84
- clearButton.style.display = "none";
85
- clearButton.style.color = "#6c757d";
86
- clearButton.style.fontSize = "0.85rem";
87
- clearButton.style.padding = "0.25rem";
88
- clearButton.style.width = "1.5rem";
89
- clearButton.style.textAlign = "center";
90
- clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
91
- clearButton.addEventListener("click", function () {
92
- searchInput.value = "";
93
- // Clear the saved filter from localStorage
94
- localStorage.removeItem(STORAGE_KEYS.searchFilter);
95
- // Call filter directly without debouncing for immediate feedback
96
- filterTables();
97
- this.style.display = "none";
98
47
  });
99
48
 
100
- const filterContainer = document.querySelector(
101
- ".dbviewer-table-filter-container"
102
- );
103
- if (filterContainer) {
104
- filterContainer.style.position = "relative";
105
- filterContainer.appendChild(clearButton);
106
-
107
- searchInput.addEventListener("input", function () {
108
- clearButton.style.display = this.value ? "block" : "none";
109
- });
49
+ // Update the tables count in the sidebar
50
+ const tableCountElement = document.getElementById("table-count");
51
+ if (tableCountElement) {
52
+ tableCountElement.textContent = visibleCount;
110
53
  }
111
54
 
112
- // Restore saved search filter on page load and apply it immediately
113
- const savedFilter = localStorage.getItem(STORAGE_KEYS.searchFilter);
114
- if (savedFilter) {
115
- searchInput.value = savedFilter;
116
- // Show clear button immediately when filter is restored
117
- clearButton.style.display = "block";
118
- // Apply filter immediately without debouncing to prevent blinking
119
- const query = savedFilter.toLowerCase();
120
- const tableItems = document.querySelectorAll(
121
- "#tablesList .list-group-item-action"
122
- );
123
- let visibleCount = 0;
124
-
125
- tableItems.forEach(function (item) {
126
- const tableName = (item.getAttribute("title") || item.textContent)
127
- .trim()
128
- .toLowerCase();
129
- const displayedText = item.textContent.trim().toLowerCase();
130
-
131
- if (tableName.includes(query) || displayedText.includes(query)) {
132
- item.classList.remove("d-none");
133
- visibleCount++;
134
- } else {
135
- item.classList.add("d-none");
136
- }
137
- });
138
-
139
- // Update the tables count immediately
140
- const tableCountElement = document.getElementById("table-count");
141
- if (tableCountElement) {
142
- tableCountElement.textContent = visibleCount;
143
- }
144
-
145
- // Handle no results message immediately
146
- let noResultsEl = document.getElementById("dbviewer-no-filter-results");
147
- if (visibleCount === 0 && query !== "") {
148
- if (!noResultsEl) {
149
- noResultsEl = document.createElement("div");
150
- noResultsEl.id = "dbviewer-no-filter-results";
151
- noResultsEl.className = "list-group-item text-muted text-center py-3";
152
- noResultsEl.innerHTML =
153
- '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
154
- document.getElementById("tablesList").appendChild(noResultsEl);
155
- }
156
- noResultsEl.querySelector(".fw-bold").textContent = query;
157
- noResultsEl.style.display = "block";
158
- } else if (noResultsEl) {
159
- noResultsEl.style.display = "none";
55
+ // Show/hide no results message
56
+ let noResultsEl = document.getElementById("dbviewer-no-filter-results");
57
+ if (visibleCount === 0 && query !== "") {
58
+ if (!noResultsEl) {
59
+ noResultsEl = document.createElement("div");
60
+ noResultsEl.id = "dbviewer-no-filter-results";
61
+ noResultsEl.className = "list-group-item text-muted text-center py-3";
62
+ noResultsEl.innerHTML =
63
+ '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
64
+ document.getElementById("tablesList").appendChild(noResultsEl);
160
65
  }
66
+ noResultsEl.querySelector(".fw-bold").textContent = query;
67
+ noResultsEl.style.display = "block";
68
+ } else if (noResultsEl) {
69
+ noResultsEl.style.display = "none";
161
70
  }
71
+ }, 150); // Debounce for 150ms
72
+
73
+ // Set up clear button first
74
+ const clearButton = document.createElement("button");
75
+ clearButton.type = "button";
76
+ clearButton.className = "btn btn-sm btn-link position-absolute";
77
+ clearButton.style.right = "15px";
78
+ clearButton.style.top = "50%";
79
+ clearButton.style.transform = "translateY(-50%)";
80
+ clearButton.style.display = "none";
81
+ clearButton.style.color = "#6c757d";
82
+ clearButton.style.fontSize = "0.85rem";
83
+ clearButton.style.padding = "0.25rem";
84
+ clearButton.style.width = "1.5rem";
85
+ clearButton.style.textAlign = "center";
86
+ clearButton.innerHTML = '<i class="bi bi-x-circle"></i>';
87
+ clearButton.addEventListener("click", function () {
88
+ searchInput.value = "";
89
+ // Clear the saved filter from localStorage
90
+ localStorage.removeItem(STORAGE_KEYS.searchFilter);
91
+ // Call filter directly without debouncing for immediate feedback
92
+ filterTables();
93
+ this.style.display = "none";
94
+ });
95
+
96
+ const filterContainer = document.querySelector(
97
+ ".dbviewer-table-filter-container"
98
+ );
99
+ if (filterContainer) {
100
+ filterContainer.style.position = "relative";
101
+ filterContainer.appendChild(clearButton);
102
+
103
+ searchInput.addEventListener("input", function () {
104
+ clearButton.style.display = this.value ? "block" : "none";
105
+ });
106
+ }
162
107
 
163
- // Restore saved scroll position on page load
164
- if (sidebarContent) {
165
- const savedScrollPosition = localStorage.getItem(
166
- STORAGE_KEYS.scrollPosition
167
- );
168
- if (savedScrollPosition) {
169
- // Use requestAnimationFrame to ensure DOM is fully rendered
170
- requestAnimationFrame(() => {
171
- sidebarContent.scrollTop = parseInt(savedScrollPosition, 10);
172
- });
173
- }
108
+ // Restore saved search filter on page load and apply it immediately
109
+ const savedFilter = localStorage.getItem(STORAGE_KEYS.searchFilter);
110
+ searchInput.value = savedFilter;
111
+ // Show clear button immediately when filter is restored
112
+ clearButton.style.display = "block";
113
+ // Apply filter immediately without debouncing to prevent blinking
114
+ const query = savedFilter.toLowerCase();
115
+ const tableItems = document.querySelectorAll(
116
+ "#tablesList .list-group-item-action"
117
+ );
118
+ let visibleCount = 0;
119
+
120
+ tableItems.forEach(function (item) {
121
+ const tableName = (item.getAttribute("title") || item.textContent)
122
+ .trim()
123
+ .toLowerCase();
124
+ const displayedText = item.textContent.trim().toLowerCase();
125
+
126
+ if (tableName.includes(query) || displayedText.includes(query)) {
127
+ item.classList.remove("d-none");
128
+ visibleCount++;
129
+ } else {
130
+ item.classList.add("d-none");
131
+ }
132
+ });
174
133
 
175
- // Save scroll position on scroll
176
- const saveScrollPosition = debounce(function () {
177
- localStorage.setItem(
178
- STORAGE_KEYS.scrollPosition,
179
- sidebarContent.scrollTop
180
- );
181
- }, 100);
134
+ // Update the tables count immediately
135
+ const tableCountElement = document.getElementById("table-count");
136
+ if (tableCountElement) {
137
+ tableCountElement.textContent = visibleCount;
138
+ }
182
139
 
183
- sidebarContent.addEventListener("scroll", saveScrollPosition);
140
+ // Handle no results message immediately
141
+ let noResultsEl = document.getElementById("dbviewer-no-filter-results");
142
+ if (visibleCount === 0 && query !== "") {
143
+ if (!noResultsEl) {
144
+ noResultsEl = document.createElement("div");
145
+ noResultsEl.id = "dbviewer-no-filter-results";
146
+ noResultsEl.className = "list-group-item text-muted text-center py-3";
147
+ noResultsEl.innerHTML =
148
+ '<i class="bi bi-search me-1"></i> No tables match "<span class="fw-bold"></span>"';
149
+ document.getElementById("tablesList").appendChild(noResultsEl);
184
150
  }
151
+ noResultsEl.querySelector(".fw-bold").textContent = query;
152
+ noResultsEl.style.display = "block";
153
+ } else if (noResultsEl) {
154
+ noResultsEl.style.display = "none";
155
+ }
185
156
 
186
- // Set up event listeners for the search input
187
- searchInput.addEventListener("input", filterTables);
188
- searchInput.addEventListener("keyup", function (e) {
189
- filterTables();
190
-
191
- // Add keyboard navigation for the filtered list
192
- if (e.key === "Enter" || e.key === "ArrowDown") {
193
- e.preventDefault();
194
- // Focus the first visible table item (not having d-none class)
195
- const firstVisibleItem = document.querySelector(
196
- "#tablesList .list-group-item-action:not(.d-none)"
197
- );
198
- if (firstVisibleItem) {
199
- firstVisibleItem.focus();
200
- // Make sure the item is visible in the scrollable area
201
- firstVisibleItem.scrollIntoView({
202
- behavior: "smooth",
203
- block: "nearest",
204
- });
205
- }
206
- }
157
+ // Restore saved scroll position on page load
158
+ const savedScrollPosition = localStorage.getItem(STORAGE_KEYS.scrollPosition);
159
+ if (savedScrollPosition) {
160
+ // Use requestAnimationFrame to ensure DOM is fully rendered
161
+ requestAnimationFrame(() => {
162
+ sidebarContent.scrollTop = parseInt(savedScrollPosition, 10);
207
163
  });
208
- searchInput.addEventListener("search", filterTables); // For clearing via the "x" in some browsers
209
164
  }
165
+
166
+ // Save scroll position on scroll
167
+ const saveScrollPosition = debounce(function () {
168
+ localStorage.setItem(STORAGE_KEYS.scrollPosition, sidebarContent.scrollTop);
169
+ }, 100);
170
+
171
+ sidebarContent.addEventListener("scroll", saveScrollPosition);
172
+
173
+ // Set up event listeners for the search input
174
+ searchInput.addEventListener("input", filterTables);
175
+ searchInput.addEventListener("keyup", function (e) {
176
+ filterTables();
177
+
178
+ // Add keyboard navigation for the filtered list
179
+ if (e.key === "Enter" || e.key === "ArrowDown") {
180
+ e.preventDefault();
181
+ // Focus the first visible table item (not having d-none class)
182
+ const firstVisibleItem = document.querySelector(
183
+ "#tablesList .list-group-item-action:not(.d-none)"
184
+ );
185
+ if (firstVisibleItem) {
186
+ firstVisibleItem.focus();
187
+ // Make sure the item is visible in the scrollable area
188
+ firstVisibleItem.scrollIntoView({
189
+ behavior: "smooth",
190
+ block: "nearest",
191
+ });
192
+ }
193
+ }
194
+ });
195
+
196
+ searchInput.addEventListener("search", filterTables); // For clearing via the "x" in some browsers
210
197
  });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * DBViewer Utility Functions
3
+ * Shared utilities for DBViewer JavaScript modules
4
+ */
5
+
6
+ // Create a global namespace for DBViewer
7
+ window.DBViewer = window.DBViewer || {};
8
+
9
+ /**
10
+ * Debounces a function call to limit how often it runs
11
+ * @param {Function} func - The function to debounce
12
+ * @param {number} wait - Milliseconds to wait between calls
13
+ * @returns {Function} Debounced function
14
+ */
15
+ function debounce(func, wait) {
16
+ let timeout;
17
+ return function (...args) {
18
+ const context = this;
19
+ clearTimeout(timeout);
20
+ timeout = setTimeout(() => func.apply(context, args), wait);
21
+ };
22
+ }
23
+
24
+ /**
25
+ * Decodes HTML entities in a string
26
+ * @param {string} text - Text with HTML entities
27
+ * @returns {string} Decoded text
28
+ */
29
+ function decodeHTMLEntities(text) {
30
+ const textarea = document.createElement("textarea");
31
+ textarea.innerHTML = text;
32
+ return textarea.value;
33
+ }
34
+
35
+ /**
36
+ * Format a number with thousands separators
37
+ * @param {number} number - Number to format
38
+ * @returns {string} Formatted number with commas
39
+ */
40
+ function numberWithDelimiter(number) {
41
+ return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
42
+ }
43
+
44
+ /**
45
+ * Convert bytes to human-readable file size
46
+ * @param {number} bytes - Size in bytes
47
+ * @returns {string} Human readable size (e.g., "4.2 MB")
48
+ */
49
+ function numberToHumanSize(bytes) {
50
+ if (bytes === null || bytes === undefined) return "N/A";
51
+ if (bytes === 0) return "0 Bytes";
52
+
53
+ const k = 1024;
54
+ const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
55
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
56
+
57
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
58
+ }
59
+
60
+ /**
61
+ * Helper function to manage theme changes across the application
62
+ */
63
+ const ThemeManager = {
64
+ /**
65
+ * Get the current theme
66
+ * @returns {string} 'dark' or 'light'
67
+ */
68
+ getCurrentTheme() {
69
+ return document.documentElement.getAttribute("data-bs-theme") || "light";
70
+ },
71
+
72
+ /**
73
+ * Set the theme and save to local storage
74
+ * @param {string} theme - 'dark' or 'light'
75
+ */
76
+ setTheme(theme) {
77
+ if (theme !== "dark" && theme !== "light") {
78
+ console.error("Invalid theme value:", theme);
79
+ return;
80
+ }
81
+
82
+ document.documentElement.setAttribute("data-bs-theme", theme);
83
+ localStorage.setItem("dbviewerTheme", theme);
84
+
85
+ // Notify all components about theme change
86
+ const themeChangeEvent = new CustomEvent("dbviewerThemeChanged", {
87
+ detail: { theme },
88
+ });
89
+ document.dispatchEvent(themeChangeEvent);
90
+ },
91
+
92
+ /**
93
+ * Toggle between dark and light themes
94
+ */
95
+ toggleTheme() {
96
+ const currentTheme = this.getCurrentTheme();
97
+ this.setTheme(currentTheme === "dark" ? "light" : "dark");
98
+ },
99
+
100
+ /**
101
+ * Initialize theme based on saved preference or OS preference
102
+ */
103
+ initialize() {
104
+ const prefersDarkMode = window.matchMedia(
105
+ "(prefers-color-scheme: dark)"
106
+ ).matches;
107
+ const savedTheme = localStorage.getItem("dbviewerTheme");
108
+
109
+ if (savedTheme) {
110
+ this.setTheme(savedTheme);
111
+ } else if (prefersDarkMode) {
112
+ this.setTheme("dark");
113
+ }
114
+ },
115
+ };
116
+
117
+ // Expose utilities to global namespace
118
+ DBViewer.Utility = {
119
+ debounce,
120
+ decodeHTMLEntities,
121
+ numberWithDelimiter,
122
+ numberToHumanSize,
123
+ ThemeManager,
124
+ };