dbviewer 0.7.10 โ†’ 0.8.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +60 -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/helpers/dbviewer/datatable_ui_table_helper.rb +10 -9
  31. data/app/helpers/dbviewer/formatting_helper.rb +6 -1
  32. data/app/views/dbviewer/entity_relationship_diagrams/index.html.erb +1 -1
  33. data/app/views/dbviewer/tables/query.html.erb +24 -8
  34. data/app/views/dbviewer/tables/show.html.erb +3 -3
  35. data/app/views/layouts/dbviewer/application.html.erb +12 -3
  36. data/config/routes.rb +2 -2
  37. data/lib/dbviewer/configuration.rb +21 -0
  38. data/lib/dbviewer/data_privacy/pii_masker.rb +125 -0
  39. data/lib/dbviewer/database/manager.rb +2 -2
  40. data/lib/dbviewer/datatable/query_operations.rb +1 -17
  41. data/lib/dbviewer/engine.rb +29 -0
  42. data/lib/dbviewer/version.rb +1 -1
  43. data/lib/dbviewer.rb +45 -0
  44. data/lib/generators/dbviewer/install_generator.rb +6 -0
  45. data/lib/generators/dbviewer/templates/pii_configuration_example.rb +99 -0
  46. metadata +17 -10
  47. data/app/controllers/concerns/dbviewer/connection_management.rb +0 -88
  48. data/app/controllers/concerns/dbviewer/data_export.rb +0 -32
  49. data/app/controllers/concerns/dbviewer/database_information.rb +0 -62
  50. data/app/controllers/concerns/dbviewer/datatable_support.rb +0 -47
  51. data/app/controllers/concerns/dbviewer/pagination_concern.rb +0 -34
  52. data/app/controllers/concerns/dbviewer/query_operations.rb +0 -28
  53. data/app/controllers/concerns/dbviewer/relationship_management.rb +0 -173
  54. data/app/controllers/concerns/dbviewer/table_operations.rb +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8875d81748ca0f9c40b3d13c8f2ed9ac47ab2aba58c68a3da51f7ce403ee56c
4
- data.tar.gz: b670e95150a1843357373c5c892ee3c08e78243a0b0453731212177a077f12a6
3
+ metadata.gz: 988493e4778410960da76a39e233df19e74a8155393a2b7cbe39a5f073c5d905
4
+ data.tar.gz: 5fbc3a34833d8d7758b786623bf3a5767699f80bba68ba953415e791d63fe86f
5
5
  SHA512:
6
- metadata.gz: ab5ff0acfabc15d711f61aa3e8811c40af5655c70b033eacb63f1ba2d508f08cf6c151f5da2a46ffd21e04cb4b170c14c03d87eea7c9d4b789232796d1087c88
7
- data.tar.gz: cb1e8197c523567d1dc29e96b98a1690f4056dff85ccf4cf4adcca5ab816cb0da6baf1adf84e9875059b400931e344923f08c579057531a829f1f1cd126d25bc
6
+ metadata.gz: 06c52e89a4342edcc7edfb15f91ef5c53bc528ddff3b56bb3f7a0a9d9967b2c75ecf41e8850bdab13a535e93dbdb9036bf545aac4e4b55aeb0a05b9fec0b1b2e
7
+ data.tar.gz: a18bd6e86c8dcd70dc851e2800d13cf150fe5a08ec1c4c606bd6d714e4dd62a3548b1bfc7e9510c8f8836fb01a783a5a6be32b5d6344bd0cd040c3da430e92b6
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  # ๐Ÿ‘๏ธ DBViewer
4
4
 
5
+ > **The fastest way to visualize and explore your database**
6
+
5
7
  DBViewer is a powerful Rails engine that provides a comprehensive interface to view and explore database tables, records, and schema.
6
8
  It's designed for development, debugging, and database analysis, offering a clean and intuitive way to interact with your application's database.
7
9
 
@@ -16,6 +18,7 @@ It's designed for development, debugging, and database analysis, offering a clea
16
18
  - **Data Browsing**
17
19
  - **SQL Queries**
18
20
  - **Multiple Database Connections**
21
+ - **PII Data Masking** - Protect sensitive data with configurable masking rules
19
22
  - **Enhanced UI Features**
20
23
 
21
24
  ## ๐Ÿงช Demo Application
@@ -257,6 +260,63 @@ end
257
260
 
258
261
  When disabled, all DBViewer routes return 404 responses, making it appear as if the tool was never installed. This is the recommended approach for production systems where database admin tools should not be accessible.
259
262
 
263
+ ## ๐Ÿ” PII Data Masking
264
+
265
+ DBViewer includes built-in support for masking Personally Identifiable Information (PII) to protect sensitive data while allowing developers to browse database contents.
266
+
267
+ ### Quick Setup
268
+
269
+ Configure PII masking in your Rails initializer (e.g., `config/initializers/dbviewer.rb`):
270
+
271
+ ```ruby
272
+ # Enable PII masking (enabled by default)
273
+ Dbviewer.configure do |config|
274
+ config.enable_pii_masking = true
275
+ end
276
+
277
+ # Define masking rules
278
+ Dbviewer.configure_pii do |pii|
279
+ # Built-in masking types
280
+ pii.mask 'users.email', with: :email # john@example.com โ†’ jo***@example.com
281
+ pii.mask 'users.phone', with: :phone # +1234567890 โ†’ +1***90
282
+ pii.mask 'users.ssn', with: :ssn # 123456789 โ†’ ***-**-6789
283
+ pii.mask 'payments.card_number', with: :credit_card # 1234567890123456 โ†’ ****-****-****-3456
284
+ pii.mask 'users.api_key', with: :full_redact # any_value โ†’ ***REDACTED***
285
+
286
+ # Custom masking with lambda
287
+ pii.mask 'users.salary', with: ->(value) { value ? '$***,***' : value }
288
+
289
+ # Define reusable custom masks
290
+ pii.custom_mask :ip_mask, ->(value) {
291
+ return value if value.nil?
292
+ parts = value.split('.')
293
+ "#{parts[0]}.#{parts[1]}.***.***.***"
294
+ }
295
+ pii.mask 'logs.ip_address', with: :ip_mask
296
+ end
297
+ ```
298
+
299
+ ### Built-in Masking Types
300
+
301
+ - **`:email`** - Masks email addresses while preserving domain
302
+ - **`:phone`** - Masks phone numbers keeping first and last digits
303
+ - **`:ssn`** - Masks Social Security Numbers showing only last 4 digits
304
+ - **`:credit_card`** - Masks credit card numbers showing only last 4 digits
305
+ - **`:full_redact`** - Completely redacts the value
306
+ - **`:partial`** - Partial masking (default behavior)
307
+
308
+ ### Generate Example Configuration
309
+
310
+ Use the generator to create an example PII configuration:
311
+
312
+ ```bash
313
+ rails generate dbviewer:install
314
+ ```
315
+
316
+ This creates `config/initializers/dbviewer_pii_example.rb` with comprehensive examples.
317
+
318
+ For detailed PII masking documentation, see [PII_MASKING.md](docs/PII_MASKING.md).
319
+
260
320
  ## ๐Ÿ“ Security Note
261
321
 
262
322
  โš ๏ธ **Warning**: This engine provides direct access to your database contents, which contains sensitive information. Always protect it with HTTP Basic Authentication by configuring strong credentials as shown above.
@@ -0,0 +1 @@
1
+ data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>๐Ÿ‘๏ธ</text></svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
2
+ <circle fill="#0d6efd" cx="8" cy="8" r="8"/>
3
+ <text x="8" y="11" font-family="sans-serif" font-size="8" text-anchor="middle" fill="white">๐Ÿ‘๏ธ</text>
4
+ </svg>
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
2
+ <circle fill="#0d6efd" cx="16" cy="16" r="16"/>
3
+ <text x="16" y="22" font-family="sans-serif" font-size="16" text-anchor="middle" fill="white">๐Ÿ‘๏ธ</text>
4
+ </svg>
@@ -0,0 +1,10 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg width="100px" height="100px" viewBox="0 0 100 100" version="1.1"
3
+ xmlns="http://www.w3.org/2000/svg">
4
+ <title>DB Viewer Favicon</title>
5
+ <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
6
+ <circle fill="#0d6efd" cx="50" cy="50" r="50" />
7
+ <text x="50" y="70" font-family="sans-serif" font-size="60" text-anchor="middle"
8
+ fill="white">๐Ÿ‘๏ธ</text>
9
+ </g>
10
+ </svg>
@@ -1,30 +1,30 @@
1
1
  document.addEventListener("DOMContentLoaded", function () {
2
+ // Validate that required utility scripts have loaded
3
+ if (!window.DBViewer || !DBViewer.Utility || !DBViewer.ErrorHandler) {
4
+ console.error(
5
+ "Required DBViewer scripts not loaded. Please check utility.js and error_handler.js."
6
+ );
7
+ return;
8
+ }
9
+
10
+ // Destructure the needed functions for easier access
11
+ const { debounce, ThemeManager } = DBViewer.Utility;
12
+ const { displayError } = DBViewer.ErrorHandler;
2
13
  // Check if mermaid is loaded first
3
14
  if (typeof mermaid === "undefined") {
4
15
  console.error("Mermaid library not loaded!");
5
- showError(
6
- "Mermaid library not loaded",
16
+ displayError(
17
+ "erd-container",
18
+ "Mermaid Library Not Loaded",
7
19
  "The diagram library could not be loaded. Please check your internet connection and try again."
8
20
  );
9
21
  return;
10
22
  }
11
23
 
12
- // Helper function to debounce rapid function calls
13
- function debounce(func, wait) {
14
- let timeout;
15
- return function (...args) {
16
- clearTimeout(timeout);
17
- timeout = setTimeout(() => func.apply(this, args), wait);
18
- };
19
- }
20
-
21
24
  // Initialize mermaid with theme detection like mini ERD
22
25
  mermaid.initialize({
23
26
  startOnLoad: true,
24
- theme:
25
- document.documentElement.getAttribute("data-bs-theme") === "dark"
26
- ? "dark"
27
- : "default",
27
+ theme: ThemeManager.getCurrentTheme() === "dark" ? "dark" : "default",
28
28
  securityLevel: "loose",
29
29
  er: {
30
30
  diagramPadding: 20,
@@ -38,7 +38,7 @@ document.addEventListener("DOMContentLoaded", function () {
38
38
  },
39
39
  });
40
40
 
41
- // Function to show error messages
41
+ // Function to show error messages - using our custom error handler with specific UI adjustments
42
42
  function showError(title, message, details = "") {
43
43
  const errorContainer = document.getElementById("erd-error");
44
44
  const errorMessage = document.getElementById("erd-error-message");
@@ -656,34 +656,30 @@ document.addEventListener("DOMContentLoaded", function () {
656
656
 
657
657
  // Add theme observer to update diagram when theme changes
658
658
  function setupThemeObserver() {
659
- const observer = new MutationObserver((mutations) => {
660
- mutations.forEach((mutation) => {
661
- if (mutation.attributeName === "data-bs-theme") {
662
- const newTheme =
663
- document.documentElement.getAttribute("data-bs-theme");
664
- mermaid.initialize({
665
- theme: newTheme === "dark" ? "dark" : "default",
666
- // Keep other settings
667
- securityLevel: "loose",
668
- er: {
669
- diagramPadding: 20,
670
- layoutDirection: "TB",
671
- minEntityWidth: 100,
672
- minEntityHeight: 75,
673
- entityPadding: 15,
674
- stroke: "gray",
675
- fill: "honeydew",
676
- fontSize: 20,
677
- },
678
- });
679
- // Trigger redraw if diagram is already displayed
680
- if (diagramReady) {
681
- updateDiagramWithFullData();
682
- }
683
- }
659
+ // Listen for our custom theme change event
660
+ document.addEventListener("dbviewerThemeChanged", (event) => {
661
+ const newTheme = event.detail.theme;
662
+ mermaid.initialize({
663
+ theme: newTheme === "dark" ? "dark" : "default",
664
+ // Keep other settings
665
+ securityLevel: "loose",
666
+ er: {
667
+ diagramPadding: 20,
668
+ layoutDirection: "TB",
669
+ minEntityWidth: 100,
670
+ minEntityHeight: 75,
671
+ entityPadding: 15,
672
+ stroke: "gray",
673
+ fill: "honeydew",
674
+ fontSize: 20,
675
+ },
684
676
  });
677
+
678
+ // Trigger redraw if diagram is already displayed
679
+ if (diagramReady) {
680
+ updateDiagramWithFullData();
681
+ }
685
682
  });
686
- observer.observe(document.documentElement, { attributes: true });
687
683
  }
688
684
 
689
685
  setupThemeObserver();
@@ -0,0 +1,58 @@
1
+ /**
2
+ * DBViewer Error Handler
3
+ * Provides consistent error handling across the application
4
+ */
5
+
6
+ // Create a global namespace for DBViewer if it doesn't exist yet
7
+ window.DBViewer = window.DBViewer || {};
8
+
9
+ /**
10
+ * Display an error message in a container
11
+ * @param {string} containerId - The ID of the container to show the error in
12
+ * @param {string} title - Error title
13
+ * @param {string} message - Error message
14
+ * @param {string} details - Optional details about the error
15
+ */
16
+ function displayError(containerId, title, message, details = "") {
17
+ const container = document.getElementById(containerId);
18
+ if (!container) {
19
+ console.error(`Error container ${containerId} not found`);
20
+ return;
21
+ }
22
+
23
+ let detailsHtml = "";
24
+ if (details) {
25
+ detailsHtml = `<small class="text-muted">${details}</small>`;
26
+ }
27
+
28
+ container.innerHTML = `
29
+ <div class="text-center my-4 text-danger">
30
+ <i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
31
+ <p class="mb-1">${title}</p>
32
+ <div>${message}</div>
33
+ ${detailsHtml}
34
+ </div>
35
+ `;
36
+ }
37
+
38
+ /**
39
+ * Handle API fetch errors with consistent error handling and logging
40
+ * @param {string} endpoint - The API endpoint being accessed
41
+ * @param {Error} error - The error that occurred
42
+ * @param {Function} errorCallback - Callback function to handle UI updates on error
43
+ */
44
+ async function handleApiError(endpoint, error, errorCallback) {
45
+ console.error(`Error fetching from ${endpoint}:`, error);
46
+
47
+ if (typeof errorCallback === "function") {
48
+ errorCallback(error);
49
+ }
50
+
51
+ // You could add additional error tracking here, like sending to a monitoring service
52
+ }
53
+
54
+ // Expose error handling functions globally
55
+ DBViewer.ErrorHandler = {
56
+ displayError,
57
+ handleApiError,
58
+ };
@@ -1,21 +1,16 @@
1
+ // Use DBViewer namespace for utility functions and error handling
1
2
  document.addEventListener("DOMContentLoaded", async 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];
3
+ // Validate that required utility scripts have loaded
4
+ if (!window.DBViewer || !DBViewer.Utility || !DBViewer.ErrorHandler) {
5
+ console.error(
6
+ "Required DBViewer scripts not loaded. Please check utility.js and error_handler.js."
7
+ );
8
+ return;
17
9
  }
18
10
 
11
+ // Destructure the needed functions for easier access
12
+ const { numberWithDelimiter, numberToHumanSize } = DBViewer.Utility;
13
+ const { displayError, handleApiError } = DBViewer.ErrorHandler;
19
14
  // Function to update analytics cards
20
15
  function updateTablesCount(data) {
21
16
  document.getElementById("tables-loading").classList.add("d-none");
@@ -167,17 +162,7 @@ document.addEventListener("DOMContentLoaded", async function () {
167
162
  }
168
163
  }
169
164
 
170
- // Function to show error state
171
- function showError(containerId, message) {
172
- const container = document.getElementById(containerId);
173
- container.innerHTML = `
174
- <div class="text-center my-4 text-danger">
175
- <i class="bi bi-exclamation-triangle fs-2 d-block mb-2"></i>
176
- <p>Error loading data</p>
177
- <small>${message}</small>
178
- </div>
179
- `;
180
- }
165
+ // Using error handler from imported module
181
166
 
182
167
  async function fetchTableCount() {
183
168
  try {
@@ -196,12 +181,13 @@ document.addEventListener("DOMContentLoaded", async function () {
196
181
  const data = await response.json();
197
182
  updateTablesCount(data);
198
183
  } catch (error) {
199
- console.error("Error loading tables count:", error);
200
- const loading = document.getElementById("tables-loading");
201
- const count = document.getElementById("tables-count");
202
- loading.classList.add("d-none");
203
- count.classList.remove("d-none");
204
- count.innerHTML = '<span class="text-danger">Error</span>';
184
+ await handleApiError("tables count", error, () => {
185
+ const loading = document.getElementById("tables-loading");
186
+ const count = document.getElementById("tables-count");
187
+ loading.classList.add("d-none");
188
+ count.classList.remove("d-none");
189
+ count.innerHTML = '<span class="text-danger">Error</span>';
190
+ });
205
191
  }
206
192
  }
207
193
 
@@ -274,8 +260,13 @@ document.addEventListener("DOMContentLoaded", async function () {
274
260
  const data = await response.json();
275
261
  updateRecentQueries(data);
276
262
  } catch (error) {
277
- console.error("Error loading recent queries:", error);
278
- showError("recent-queries-container", error.message);
263
+ await handleApiError("recent queries", error, () => {
264
+ displayError(
265
+ "recent-queries-container",
266
+ "Error Loading Queries",
267
+ error.message
268
+ );
269
+ });
279
270
  }
280
271
  }
281
272
  // Load database size data (Loading records first to prevent race condition on dbviewer model constant creation)
@@ -1,52 +1,28 @@
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 ThemeManager from the global namespace
11
+ const { ThemeManager } = DBViewer.Utility;
12
+
2
13
  // Theme toggle functionality
3
14
  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
- }
15
+
16
+ // Initialize theme based on saved preference or OS preference
17
+ ThemeManager.initialize();
19
18
 
20
19
  // Toggle theme when button is clicked
21
20
  if (themeToggleBtn) {
22
21
  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);
22
+ ThemeManager.toggleTheme();
35
23
  });
36
24
  }
37
25
 
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
26
  const toggleBtn = document.querySelector(".dbviewer-sidebar-toggle");
51
27
  const closeBtn = document.querySelector(".dbviewer-sidebar-close");
52
28
  const sidebar = document.querySelector(".dbviewer-sidebar");
@@ -72,26 +48,22 @@ document.addEventListener("DOMContentLoaded", function () {
72
48
  }, 300);
73
49
  }
74
50
 
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 () {
51
+ toggleBtn.addEventListener("click", function () {
52
+ if (sidebar.classList.contains("active")) {
92
53
  hideSidebar();
93
- });
94
- }
54
+ } else {
55
+ showSidebar();
56
+ // Focus the search input when sidebar becomes visible
57
+ setTimeout(() => {
58
+ const searchInput = document.getElementById("tableSearch");
59
+ if (searchInput) searchInput.focus();
60
+ }, 300); // Small delay to allow for animation
61
+ }
62
+ });
63
+
64
+ closeBtn.addEventListener("click", function () {
65
+ hideSidebar();
66
+ });
95
67
 
96
68
  overlay.addEventListener("click", function () {
97
69
  hideSidebar();
@@ -110,85 +82,84 @@ document.addEventListener("DOMContentLoaded", function () {
110
82
 
111
83
  // Offcanvas enhancement for theme synchronization
112
84
  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
85
 
139
- // Function to sync offcanvas colors with current theme
140
- function syncOffcanvasWithTheme() {
86
+ // Get all theme toggles
87
+ const allThemeToggles = document.querySelectorAll(".theme-toggle");
88
+
89
+ // Handle theme change from any toggle button
90
+ allThemeToggles.forEach((toggleBtn) => {
91
+ toggleBtn.addEventListener("click", function () {
141
92
  const currentTheme =
142
93
  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
94
 
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();
95
+ // Update all theme toggle buttons to maintain consistency
96
+ allThemeToggles.forEach((btn) => {
97
+ // Update icon in all theme toggle buttons
98
+ if (currentTheme === "dark") {
99
+ btn.querySelector("span").innerHTML =
100
+ '<i class="bi bi-sun-fill"></i>';
101
+ btn.setAttribute("aria-label", "Switch to light mode");
102
+ } else {
103
+ btn.querySelector("span").innerHTML =
104
+ '<i class="bi bi-moon-fill"></i>';
105
+ btn.setAttribute("aria-label", "Switch to dark mode");
174
106
  }
175
107
  });
176
108
  });
109
+ });
177
110
 
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
- }
111
+ // Function to sync offcanvas colors with current theme
112
+ function syncOffcanvasWithTheme() {
113
+ const currentTheme =
114
+ document.documentElement.getAttribute("data-bs-theme") || "light";
115
+ if (currentTheme === "dark") {
116
+ offcanvasElement
117
+ .querySelector(".offcanvas-header")
118
+ .classList.remove("bg-light-subtle");
119
+ offcanvasElement
120
+ .querySelector(".offcanvas-header")
121
+ .classList.add("bg-dark-subtle");
122
+ } else {
123
+ offcanvasElement
124
+ .querySelector(".offcanvas-header")
125
+ .classList.remove("bg-dark-subtle");
126
+ offcanvasElement
127
+ .querySelector(".offcanvas-header")
128
+ .classList.add("bg-light-subtle");
129
+ }
130
+ }
131
+
132
+ // Sync on page load
133
+ document.addEventListener("DOMContentLoaded", syncOffcanvasWithTheme);
134
+
135
+ // Listen for theme changes
136
+ document.addEventListener("dbviewerThemeChanged", syncOffcanvasWithTheme);
137
+
138
+ // Handle link click in offcanvas (auto-close on mobile)
139
+ const offcanvasLinks = offcanvasElement.querySelectorAll(
140
+ ".nav-link:not(.dropdown-toggle)"
141
+ );
142
+ offcanvasLinks.forEach((link) => {
143
+ link.addEventListener("click", function () {
144
+ if (window.innerWidth < 992) {
145
+ bootstrap.Offcanvas.getInstance(offcanvasElement).hide();
191
146
  }
192
147
  });
193
- }
148
+ });
149
+
150
+ // Fix offcanvas backdrop on desktop
151
+ window.addEventListener("resize", function () {
152
+ if (window.innerWidth >= 992) {
153
+ const offcanvasInstance =
154
+ bootstrap.Offcanvas.getInstance(offcanvasElement);
155
+ if (offcanvasInstance) {
156
+ offcanvasInstance.hide();
157
+ }
158
+ // Also remove any backdrop
159
+ const backdrop = document.querySelector(".offcanvas-backdrop");
160
+ if (backdrop) {
161
+ backdrop.remove();
162
+ }
163
+ }
164
+ });
194
165
  });