dbviewer 0.9.4.pre.alpha.1 → 0.9.4.pre.alpha.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f79e82d83d69572cd7fa4331831e7fa7cc385f7d0655fec2ab9cee57bf5dcdcd
4
- data.tar.gz: 8a34270cfb5f9ede4b77d9b531a67cb1c6b3a022923b359080b4e171e725dadb
3
+ metadata.gz: bab71132b89e3a2b246802692149a2d662bc42bcfa54b9db4d71ab9593dc2c2a
4
+ data.tar.gz: 8d2160e56a9872ad9e1dfa274e5ccbdcb16c8ff1b65c134be00b806d91841a55
5
5
  SHA512:
6
- metadata.gz: edfcfc4a7eb88659393cd59cac7af960339a686010aa57d6da0ccfcf000d9ad33ee18ebed955294ca741119bca0f1d63f2579682fab48f70e9ca6ec93bce936f
7
- data.tar.gz: b1e37a36cee23d2e04284c957e3f865afa24da128579b811f25cede3f2afcda9230abfaab332b48a7525ab190da5f53a270a7d0acc594de22324c9c624019f72
6
+ metadata.gz: c00de395e8f283afd2f6c905ae1e448438296ecfdc2167606d2216d3c39a492d27d2c60334d35234e5d460472a66730a6893fe79b81ce1f972b48b67d767b1b6
7
+ data.tar.gz: 8f9a53eb14f23c5aa2bb006458dcda9de39d031aa49907ff0a017f76d96950d101f14785292a7e0a062aac91915face67bbe34f28bfffc1fdf126076375127a8
data/README.md CHANGED
@@ -14,7 +14,7 @@ It's designed for development, debugging, and database analysis, offering a clea
14
14
  - **Detailed Schema Information** - Column details, indexes, and constraints
15
15
  - **Entity Relationship Diagram (ERD)** - Interactive database schema visualization
16
16
  - **Data Browsing** - Paginated record viewing with search and filtering
17
- - **Data Creation** - Create new database records directly from the interface
17
+ - **Data Management** - Create and delete database records directly from the interface
18
18
  - **SQL Queries** - Safe SQL query execution with validation
19
19
  - **Multiple Database Connections** - Support for multiple database sources
20
20
  - **PII Data Masking** - Configurable masking for sensitive data
@@ -99,6 +99,7 @@ Dbviewer.configure do |config|
99
99
  config.cache_expiry = 300 # Cache expiration in seconds
100
100
  config.max_records = 10000 # Maximum records to return in any query
101
101
  config.enable_data_export = false # Whether to allow data exporting
102
+ config.enable_record_deletion = true # Whether to allow record deletion
102
103
  config.query_timeout = 30 # SQL query timeout in seconds
103
104
 
104
105
  # Query logging options
@@ -0,0 +1,171 @@
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const tableName = document.getElementById("table_name")?.value;
3
+ const deleteConfirmModal = document.getElementById("deleteConfirmModal");
4
+ const recordDeleteForm = document.getElementById("recordDeleteForm");
5
+
6
+ // === TOASTIFY UTIL ===
7
+ const showToast = (message, type = "success") => {
8
+ const icons = {
9
+ success: "clipboard-check",
10
+ info: "info-circle",
11
+ danger: "exclamation-triangle",
12
+ warning: "exclamation-diamond",
13
+ };
14
+
15
+ Toastify({
16
+ text: `
17
+ <span class="toast-icon">
18
+ <i class="bi bi-${icons[type] || "info-circle"}"></i>
19
+ </span>
20
+ ${message}
21
+ `,
22
+ className: `toast-factory-bot toast-${type}`,
23
+ duration: 3000,
24
+ gravity: "bottom",
25
+ position: "right",
26
+ escapeMarkup: false,
27
+ style: {
28
+ animation:
29
+ "slideInRight 0.3s ease-out, slideOutRight 0.3s ease-out 2.7s",
30
+ background: type === "danger" ? "#dc3545" : undefined,
31
+ },
32
+ }).showToast();
33
+ };
34
+
35
+ // === SETUP DELETE MODAL ===
36
+ const setupDeleteConfirmModal = (recordData, pkName, pkValue) => {
37
+ if (!recordData || !pkValue)
38
+ return console.error("Missing record data for delete confirmation");
39
+
40
+ const infoDiv = document.getElementById("deleteRecordInfo");
41
+ const idInput = document.getElementById("deleteRecordId");
42
+
43
+ idInput.value = pkValue;
44
+ recordDeleteForm.action = `${
45
+ window.location.pathname
46
+ }/records/${encodeURIComponent(pkValue)}`;
47
+ infoDiv.innerHTML = `<div><strong>${pkName}:</strong> ${pkValue}</div>`;
48
+
49
+ const importantFields = [
50
+ "name",
51
+ "title",
52
+ "email",
53
+ "username",
54
+ "code",
55
+ "reference",
56
+ ];
57
+ let count = 0;
58
+
59
+ for (const key of Object.keys(recordData)) {
60
+ if (count >= 3) break;
61
+ const lowerKey = key.toLowerCase();
62
+ if (
63
+ key !== pkName &&
64
+ importantFields.some((f) => lowerKey.includes(f)) &&
65
+ recordData[key]
66
+ ) {
67
+ const fieldDiv = document.createElement("div");
68
+ fieldDiv.className = "mt-1";
69
+ fieldDiv.innerHTML = `<strong>${key}:</strong> ${recordData[key]}`;
70
+ infoDiv.appendChild(fieldDiv);
71
+ count++;
72
+ }
73
+ }
74
+ };
75
+
76
+ // === DELETE FROM DETAIL MODAL ===
77
+ const detailDeleteBtn = document.getElementById("recordDetailDeleteBtn");
78
+ if (detailDeleteBtn) {
79
+ detailDeleteBtn.addEventListener("click", () => {
80
+ const rows = document.querySelectorAll("#recordDetailTableBody tr");
81
+ const recordData = {};
82
+
83
+ rows.forEach((row) => {
84
+ const [keyCell, valueCell] = row.querySelectorAll("td");
85
+ if (keyCell && valueCell) {
86
+ recordData[keyCell.textContent.trim()] = valueCell.textContent.trim();
87
+ }
88
+ });
89
+
90
+ const pkName =
91
+ Object.keys(recordData).find((k) => k.toLowerCase() === "id") ||
92
+ Object.keys(recordData)[0];
93
+ const pkValue = recordData[pkName];
94
+
95
+ setupDeleteConfirmModal(recordData, pkName, pkValue);
96
+
97
+ bootstrap.Modal.getInstance(
98
+ document.getElementById("recordDetailModal")
99
+ )?.hide();
100
+ setTimeout(() => new bootstrap.Modal(deleteConfirmModal).show(), 500);
101
+ });
102
+ }
103
+
104
+ // === DELETE FROM TABLE ROW ===
105
+ document.querySelectorAll(".delete-record-btn").forEach((button) => {
106
+ button.addEventListener("click", () => {
107
+ const recordData = JSON.parse(button.dataset.recordData || "{}");
108
+ const pkName =
109
+ Object.keys(recordData).find((k) => k.toLowerCase() === "id") ||
110
+ Object.keys(recordData)[0];
111
+ const pkValue = recordData[pkName];
112
+ setupDeleteConfirmModal(recordData, pkName, pkValue);
113
+ });
114
+ });
115
+
116
+ // === FORM SUBMIT (DELETE) ===
117
+ if (recordDeleteForm) {
118
+ recordDeleteForm.addEventListener("submit", async (e) => {
119
+ e.preventDefault();
120
+
121
+ const form = e.target;
122
+ const submitButton = document.querySelector(
123
+ "#recordDeleteForm button[type='submit']"
124
+ );
125
+ const originalText = submitButton.innerHTML;
126
+ const csrfToken = document.querySelector(
127
+ 'meta[name="csrf-token"]'
128
+ )?.content;
129
+
130
+ disableSubmitButton(submitButton);
131
+
132
+ try {
133
+ const response = await fetch(form.action, {
134
+ method: "DELETE",
135
+ headers: {
136
+ "Content-Type": "application/json",
137
+ "X-CSRF-Token": csrfToken,
138
+ },
139
+ credentials: "same-origin",
140
+ });
141
+
142
+ const result = await response.json();
143
+
144
+ if (!response.ok) {
145
+ throw new Error(result?.error || "Failed to delete record");
146
+ }
147
+
148
+ showToast(result.message || "Record deleted successfully", "success");
149
+
150
+ const modal = bootstrap.Modal.getInstance(deleteConfirmModal);
151
+ modal.hide();
152
+
153
+ setTimeout(() => window.location.reload(), 1000);
154
+ } catch (error) {
155
+ console.error("Delete Error:", error);
156
+ showToast(error.message || "Unexpected error occurred", "danger");
157
+ } finally {
158
+ enableSubmitButton(submitButton, originalText);
159
+ }
160
+ });
161
+ }
162
+
163
+ // === BUTTON HELPERS ===
164
+ function disableSubmitButton(button) {
165
+ button.disabled = true;
166
+ }
167
+
168
+ function enableSubmitButton(button) {
169
+ button.disabled = false;
170
+ }
171
+ });
@@ -3,7 +3,7 @@ module Dbviewer
3
3
  include Dbviewer::AccessControlValidation
4
4
 
5
5
  before_action :set_table_name, except: [ :index ]
6
- before_action :validate_table, only: [ :show, :query, :export_csv, :new_record, :create_record ]
6
+ before_action :validate_table, only: [ :show, :query, :export_csv, :new_record, :create_record, :destroy_record ]
7
7
  before_action :set_query_filters, only: [ :show, :export_csv ]
8
8
  before_action :set_global_filters, only: [ :show, :export_csv ]
9
9
 
@@ -89,6 +89,28 @@ module Dbviewer
89
89
  disposition: "attachment; filename=#{filename}"
90
90
  end
91
91
 
92
+ def destroy_record
93
+ model_class = database_manager.get_model_for(@table_name)
94
+ primary_key = database_manager.primary_key(@table_name) || "id"
95
+ record = model_class.find_by(primary_key => params[:record_id])
96
+
97
+ if record.nil?
98
+ render json: { error: "Record not found" }, status: :not_found
99
+ return
100
+ end
101
+
102
+ begin
103
+ if record.destroy
104
+ render json: { message: "Record deleted successfully" }, status: :ok
105
+ else
106
+ render json: { errors: record.errors.full_messages, message: "Failed to delete record" }, status: :unprocessable_entity
107
+ end
108
+ rescue => e
109
+ Rails.logger.error("Error deleting record from #{@table_name}: #{e.message}")
110
+ render json: { error: "Failed to delete record: #{e.message}" }, status: :internal_server_error
111
+ end
112
+ end
113
+
92
114
  private
93
115
 
94
116
  def record_params
@@ -137,8 +137,27 @@ module Dbviewer
137
137
  content_tag(:i, "", class: "bi bi-clipboard")
138
138
  end
139
139
 
140
- # Concatenate both buttons
141
- view_button + copy_factory_button
140
+ # Delete Record button (only if enabled in configuration)
141
+ delete_button = if Dbviewer.configuration.enable_record_deletion
142
+ button_tag(
143
+ type: "button",
144
+ class: "btn btn-sm btn-outline-danger delete-record-btn",
145
+ title: "Delete Record",
146
+ data: {
147
+ bs_toggle: "modal",
148
+ bs_target: "#deleteConfirmModal",
149
+ record_data: data_attributes.to_json,
150
+ table_name: table_name
151
+ }
152
+ ) do
153
+ content_tag(:i, "", class: "bi bi-trash")
154
+ end
155
+ else
156
+ "".html_safe
157
+ end
158
+
159
+ # Concatenate all buttons
160
+ view_button + copy_factory_button + delete_button
142
161
  end
143
162
  end
144
163
  end
@@ -14,6 +14,7 @@
14
14
  <%= stylesheet_link_tag "dbviewer/table", "data-turbo-track": "reload" %>
15
15
  <%= javascript_include_tag "dbviewer/table", "data-turbo-track": "reload" %>
16
16
  <%= javascript_include_tag "dbviewer/record_creation", "data-turbo-track": "reload" %>
17
+ <%= javascript_include_tag "dbviewer/record_deletion", "data-turbo-track": "reload" %>
17
18
  <% end %>
18
19
 
19
20
  <% content_for :sidebar_active do %>active<% end %>
@@ -192,6 +193,11 @@
192
193
  </div>
193
194
  <div class="modal-footer">
194
195
  <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
196
+ <% if Dbviewer.configuration.enable_record_deletion %>
197
+ <button type="button" class="btn btn-danger" id="recordDetailDeleteBtn" data-record-id="">
198
+ <i class="bi bi-trash me-1"></i>Delete Record
199
+ </button>
200
+ <% end %>
195
201
  </div>
196
202
  </div>
197
203
  </div>
@@ -382,5 +388,45 @@
382
388
  </div>
383
389
  </div>
384
390
 
391
+ <% if Dbviewer.configuration.enable_record_deletion %>
392
+ <!-- Delete Confirmation Modal -->
393
+ <div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true">
394
+ <div class="modal-dialog">
395
+ <div class="modal-content">
396
+ <div class="modal-header bg-danger text-white">
397
+ <h5 class="modal-title" id="deleteConfirmModalLabel">
398
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>Confirm Deletion
399
+ </h5>
400
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
401
+ </div>
402
+ <div class="modal-body">
403
+ <p>Are you sure you want to delete this record from <strong><%= @table_name %></strong>?</p>
404
+ <div class="alert alert-warning">
405
+ <i class="bi bi-exclamation-triangle-fill me-2"></i>
406
+ <strong>Warning:</strong> This action cannot be undone.
407
+ </div>
408
+
409
+ <!-- Primary key info will be inserted here -->
410
+ <div id="deleteRecordInfo" class="mt-3 mb-2 p-2 border-start border-4 border-danger ps-3">
411
+ <!-- Record info will be displayed here -->
412
+ </div>
413
+ </div>
414
+ <div class="modal-footer">
415
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
416
+ <%= form_with(url: "#", method: :delete, id: "recordDeleteForm") do |form| %>
417
+ <input type="hidden" id="deleteRecordId" name="record_id" value="">
418
+ <button type="submit" class="btn btn-danger">
419
+ <i class="bi bi-trash me-1"></i>Delete Record
420
+ </button>
421
+ <% end %>
422
+ </div>
423
+ </div>
424
+ </div>
425
+ </div>
426
+ <% end %>
427
+
428
+ <!-- Toast container for notifications -->
429
+ <div id="toast-container" class="toast-container position-fixed bottom-0 end-0 p-3"></div>
430
+
385
431
  <input type="hidden" id="mini_erd_table_path" name="mini_erd_table_path" value="<%= dbviewer.mini_erd_api_table_path(@table_name, format: :json) %>">
386
432
  <input type="hidden" id="table_name" name="table_name" value="<%= @table_name %>">
data/config/routes.rb CHANGED
@@ -6,6 +6,7 @@ Dbviewer::Engine.routes.draw do
6
6
  get "export_csv"
7
7
  get "new_record"
8
8
  post "create_record"
9
+ delete "records/:record_id", to: "tables#destroy_record", as: :destroy_record
9
10
  end
10
11
  end
11
12
 
@@ -117,6 +117,9 @@ module Dbviewer
117
117
  # Maximum number of security events to keep in memory
118
118
  attr_accessor :max_security_events
119
119
 
120
+ # Enable or disable record deletion functionality
121
+ attr_accessor :enable_record_deletion
122
+
120
123
  def initialize
121
124
  @per_page_options = [ 10, 20, 50, 100 ]
122
125
  @default_per_page = 20
@@ -124,6 +127,7 @@ module Dbviewer
124
127
  @max_query_length = 10000
125
128
  @cache_expiry = 300
126
129
  @enable_data_export = false
130
+ @enable_record_deletion = true
127
131
  @query_timeout = 30
128
132
  @query_logging_mode = :memory
129
133
  @query_log_path = "log/dbviewer.log"
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.9.4-alpha.1"
2
+ VERSION = "0.9.4-alpha.2"
3
3
  end
@@ -13,6 +13,9 @@ Dbviewer.configure do |config|
13
13
  config.query_log_path = "log/dbviewer.log" # Path for query log file when in :file mode
14
14
  config.max_memory_queries = 1000 # Maximum number of queries to store in memory
15
15
 
16
+ # Data Management options
17
+ config.enable_record_deletion = true # Whether to allow record deletion functionality
18
+
16
19
  # Authentication options (Basic Auth)
17
20
  # config.admin_credentials = {
18
21
  # username: "admin",
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbviewer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.4.pre.alpha.1
4
+ version: 0.9.4.pre.alpha.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wailan Tirajoh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-05 00:00:00.000000000 Z
11
+ date: 2025-07-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -87,6 +87,7 @@ files:
87
87
  - app/assets/javascripts/dbviewer/layout.js
88
88
  - app/assets/javascripts/dbviewer/query.js
89
89
  - app/assets/javascripts/dbviewer/record_creation.js
90
+ - app/assets/javascripts/dbviewer/record_deletion.js
90
91
  - app/assets/javascripts/dbviewer/sidebar.js
91
92
  - app/assets/javascripts/dbviewer/table.js
92
93
  - app/assets/javascripts/dbviewer/utility.js