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 +4 -4
- data/README.md +2 -1
- data/app/assets/javascripts/dbviewer/record_deletion.js +171 -0
- data/app/controllers/dbviewer/tables_controller.rb +23 -1
- data/app/helpers/dbviewer/datatable_ui_table_helper.rb +21 -2
- data/app/views/dbviewer/tables/show.html.erb +46 -0
- data/config/routes.rb +1 -0
- data/lib/dbviewer/configuration.rb +4 -0
- data/lib/dbviewer/version.rb +1 -1
- data/lib/generators/dbviewer/templates/initializer.rb +3 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bab71132b89e3a2b246802692149a2d662bc42bcfa54b9db4d71ab9593dc2c2a
|
4
|
+
data.tar.gz: 8d2160e56a9872ad9e1dfa274e5ccbdcb16c8ff1b65c134be00b806d91841a55
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
#
|
141
|
-
|
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
@@ -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"
|
data/lib/dbviewer/version.rb
CHANGED
@@ -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.
|
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-
|
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
|