dbviewer 0.9.2 → 0.9.4.pre.alpha.1

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: 73a576f4a8f66beb4c9320015567379b28baf8f11d857b4e5b69ff7372ec0977
4
- data.tar.gz: f408948d8b866d8a223035555d31b9b5131f7d3c21ae4d2fe3864dfbd5670d75
3
+ metadata.gz: f79e82d83d69572cd7fa4331831e7fa7cc385f7d0655fec2ab9cee57bf5dcdcd
4
+ data.tar.gz: 8a34270cfb5f9ede4b77d9b531a67cb1c6b3a022923b359080b4e171e725dadb
5
5
  SHA512:
6
- metadata.gz: c973bd58d1b0e0005bcf0591d72d70d58ba917c2c4e39f1678a77011f4832e25e53d22d0153fbded40612e0d8e1bc43ef75271e64fc20f18489920e4299a8fa2
7
- data.tar.gz: a464fd32e361fa3e5eab3917f72b9b1e08e98f0887e5cc396a685a0624ff71c7a74998887ec38f0ed71432535546b354c1086618269c48781806b17ea73f92fd
6
+ metadata.gz: edfcfc4a7eb88659393cd59cac7af960339a686010aa57d6da0ccfcf000d9ad33ee18ebed955294ca741119bca0f1d63f2579682fab48f70e9ca6ec93bce936f
7
+ data.tar.gz: b1e37a36cee23d2e04284c957e3f865afa24da128579b811f25cede3f2afcda9230abfaab332b48a7525ab190da5f53a270a7d0acc594de22324c9c624019f72
data/README.md CHANGED
@@ -14,6 +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
18
  - **SQL Queries** - Safe SQL query execution with validation
18
19
  - **Multiple Database Connections** - Support for multiple database sources
19
20
  - **PII Data Masking** - Configurable masking for sensitive data
@@ -197,7 +198,7 @@ DBViewer includes comprehensive security features to protect your database:
197
198
 
198
199
  ### Core Security
199
200
 
200
- - **Read-only Mode**: Only SELECT queries are allowed; all data modification operations are blocked
201
+ - **Data Manipulation**: Create and read operations are supported through the UI with proper validation
201
202
  - **SQL Validation**: Prevents potentially harmful operations with comprehensive validation
202
203
  - **Query Limits**: Automatic LIMIT clause added to prevent excessive data retrieval
203
204
  - **Pattern Detection**: Detection of SQL injection patterns and suspicious constructs
@@ -24,7 +24,6 @@ document.addEventListener("DOMContentLoaded", function () {
24
24
  }
25
25
 
26
26
  const toggleBtn = document.querySelector(".dbviewer-sidebar-toggle");
27
- const closeBtn = document.querySelector(".dbviewer-sidebar-close");
28
27
  const sidebar = document.querySelector(".dbviewer-sidebar");
29
28
  const overlay = document.createElement("div");
30
29
 
@@ -61,10 +60,6 @@ document.addEventListener("DOMContentLoaded", function () {
61
60
  }
62
61
  });
63
62
 
64
- closeBtn.addEventListener("click", function () {
65
- hideSidebar();
66
- });
67
-
68
63
  overlay.addEventListener("click", function () {
69
64
  hideSidebar();
70
65
  });
@@ -0,0 +1,178 @@
1
+ // Record Creation Modal
2
+ document.addEventListener("DOMContentLoaded", function () {
3
+ const addRecordButton = document.getElementById("floatingAddRecordBtn");
4
+ if (!addRecordButton) return;
5
+
6
+ const tableName = document.getElementById("table_name").value;
7
+ const newRecordModal = document.getElementById("newRecordModal");
8
+
9
+ addRecordButton.addEventListener("click", async function () {
10
+ const modal = new bootstrap.Modal(newRecordModal);
11
+ const modalBody = newRecordModal.querySelector(".modal-content");
12
+
13
+ // Show loading state
14
+ modalBody.innerHTML = `
15
+ <div class="modal-body text-center py-5">
16
+ <div class="spinner-border text-primary" role="status">
17
+ <span class="visually-hidden">Loading...</span>
18
+ </div>
19
+ <p class="mt-3">Loading form...</p>
20
+ </div>
21
+ `;
22
+ modal.show();
23
+
24
+ try {
25
+ const response = await fetch(`/dbviewer/tables/${tableName}/new_record`);
26
+ const html = await response.text();
27
+ modalBody.innerHTML = html;
28
+ initializeFormElements();
29
+ } catch (error) {
30
+ modalBody.innerHTML = `
31
+ <div class="modal-header">
32
+ <h5 class="modal-title">Error</h5>
33
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
34
+ </div>
35
+ <div class="modal-body">
36
+ <div class="alert alert-danger">
37
+ Failed to load form: ${error.message}
38
+ </div>
39
+ </div>
40
+ <div class="modal-footer">
41
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
42
+ </div>
43
+ `;
44
+ }
45
+ });
46
+
47
+ newRecordModal.addEventListener("submit", handleNewRecordSubmit);
48
+ });
49
+
50
+ // Initialize Select2 dropdowns and other form elements
51
+ function initializeFormElements() {
52
+ if (typeof $.fn.select2 !== "undefined") {
53
+ // Use jquery select2 for now, maybe we can change the dependency later
54
+ $(".select2-dropdown").select2({
55
+ dropdownParent: $("#newRecordModal"),
56
+ theme: "bootstrap-5",
57
+ width: "100%",
58
+ });
59
+ }
60
+ }
61
+
62
+ async function handleNewRecordSubmit(event) {
63
+ event.preventDefault();
64
+
65
+ const form = event.target;
66
+ const submitButton = document.getElementById("createRecordButton");
67
+ const originalText = submitButton.innerHTML;
68
+ const formData = new FormData(form);
69
+
70
+ disableSubmitButton(submitButton);
71
+
72
+ try {
73
+ const response = await fetch(form.action, {
74
+ method: "POST",
75
+ body: formData,
76
+ headers: {
77
+ "X-Requested-With": "XMLHttpRequest",
78
+ },
79
+ });
80
+
81
+ const result = await response.json();
82
+
83
+ if (!response.ok) {
84
+ handleErrors(result, form);
85
+ return;
86
+ }
87
+
88
+ handleSuccess(result);
89
+ } catch (error) {
90
+ displayGenericError(form, error.message);
91
+ } finally {
92
+ enableSubmitButton(submitButton, originalText);
93
+ }
94
+ }
95
+
96
+ function disableSubmitButton(button) {
97
+ button.disabled = true;
98
+ button.innerHTML =
99
+ '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> Creating...';
100
+ }
101
+
102
+ function enableSubmitButton(button, originalText) {
103
+ button.disabled = false;
104
+ button.innerHTML = originalText;
105
+ }
106
+
107
+ function handleSuccess() {
108
+ const modal = bootstrap.Modal.getInstance(newRecordModal);
109
+ modal.hide();
110
+
111
+ Toastify({
112
+ text: `<span class="toast-icon"><i class="bi bi-clipboard-check"></i></span> Record created successfully!`,
113
+ className: "toast-factory-bot",
114
+ duration: 3000,
115
+ gravity: "bottom",
116
+ position: "right",
117
+ escapeMarkup: false,
118
+ style: {
119
+ animation: "slideInRight 0.3s ease-out, slideOutRight 0.3s ease-out 2.7s",
120
+ },
121
+ }).showToast();
122
+
123
+ setTimeout(() => {
124
+ window.location.reload();
125
+ }, 1000);
126
+ }
127
+
128
+ function handleErrors(result, form) {
129
+ if (result.errors) {
130
+ showToastErrors(result.messages);
131
+ showFieldErrors(result.errors, form);
132
+ }
133
+ initializeFormElements();
134
+ }
135
+
136
+ function showToastErrors(messages) {
137
+ const combinedMessages = Object.values(messages)
138
+ .flat()
139
+ .map((msg) => `<div>${msg}</div>`)
140
+ .join("");
141
+
142
+ Toastify({
143
+ text: `<span class="toast-icon"><i class="bi bi-exclamation-triangle"></i></span> ${combinedMessages}`,
144
+ className: "toast-factory-bot toast-error",
145
+ duration: 5000,
146
+ gravity: "bottom",
147
+ position: "right",
148
+ escapeMarkup: false,
149
+ style: {
150
+ animation: "slideInRight 0.3s ease-out, slideOutRight 0.3s ease-out 4.7s",
151
+ background: "#dc3545",
152
+ },
153
+ }).showToast();
154
+ }
155
+
156
+ function showFieldErrors(errors, form) {
157
+ form.querySelectorAll(".invalid-feedback").forEach((el) => el.remove());
158
+ Object.entries(errors).forEach(([field, messages]) => {
159
+ const input = document.getElementById(`record_${field}`);
160
+ if (!input) return;
161
+
162
+ let errorDiv = input.parentElement.querySelector(".invalid-feedback");
163
+ if (!errorDiv) {
164
+ errorDiv = document.createElement("div");
165
+ errorDiv.className = "invalid-feedback";
166
+ input.parentElement.appendChild(errorDiv);
167
+ }
168
+ errorDiv.innerHTML = messages.join("<br>");
169
+ input.classList.add("is-invalid");
170
+ });
171
+ }
172
+
173
+ function displayGenericError(form, message) {
174
+ const errorDiv = document.createElement("div");
175
+ errorDiv.className = "alert alert-danger";
176
+ errorDiv.textContent = `Error: ${message}`;
177
+ form.prepend(errorDiv);
178
+ }
@@ -125,16 +125,23 @@ tbody tr td.action-column {
125
125
  /* ========== ACTION COLUMN STYLING ========== */
126
126
  /* Action column styling */
127
127
  .action-column {
128
- width: 100px; /* Increased from 60px to accommodate two buttons */
129
- min-width: 100px; /* Ensure minimum width */
128
+ width: 100px; /* Default for desktop */
129
+ min-width: 100px;
130
130
  white-space: nowrap;
131
131
  position: sticky;
132
132
  left: 0;
133
- z-index: 30; /* Higher z-index to ensure it stays on top */
134
- background-color: var(--bs-body-bg, #fff); /* Use body background color */
133
+ z-index: 30;
134
+ background-color: var(--bs-body-bg, #fff);
135
135
  box-shadow: 2px 0 6px rgba(0, 0, 0, 0.04);
136
136
  }
137
137
 
138
+ @media (max-width: 767.98px) {
139
+ .action-column {
140
+ width: 30px;
141
+ min-width: 30px;
142
+ }
143
+ }
144
+
138
145
  .copy-factory-btn,
139
146
  .view-record-btn {
140
147
  padding: 0.1rem 0.4rem;
@@ -1060,3 +1067,136 @@ span.flatpickr-weekday {
1060
1067
  width: 0.875rem;
1061
1068
  height: 0.875rem;
1062
1069
  }
1070
+
1071
+ /* ========== FLOATING ADD RECORD BUTTON STYLES ========== */
1072
+ .floating-add-record {
1073
+ position: fixed;
1074
+ bottom: 100px;
1075
+ right: 30px;
1076
+ z-index: 1050;
1077
+ }
1078
+
1079
+ .floating-add-record .btn {
1080
+ width: 60px;
1081
+ height: 60px;
1082
+ border-radius: 50%;
1083
+ font-size: 1.5rem;
1084
+ display: flex;
1085
+ align-items: center;
1086
+ justify-content: center;
1087
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
1088
+ transition: all 0.3s ease;
1089
+ }
1090
+
1091
+ .floating-add-record .btn:hover {
1092
+ transform: translateY(-3px);
1093
+ box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
1094
+ }
1095
+
1096
+ .floating-add-record .btn:active {
1097
+ transform: translateY(-1px);
1098
+ box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15);
1099
+ }
1100
+
1101
+ /* Dark mode styles */
1102
+ [data-bs-theme="dark"] .floating-add-record .btn {
1103
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
1104
+ }
1105
+
1106
+ /* Style for form select2 dropdowns */
1107
+ .select2-container--bootstrap-5 .select2-selection {
1108
+ min-height: 38px;
1109
+ }
1110
+
1111
+ /* ============================= */
1112
+ /* DARK MODE SUPPORT FOR SELECT2 */
1113
+ /* ============================= */
1114
+
1115
+ [data-bs-theme="dark"] .select2-container--bootstrap-5 .select2-selection {
1116
+ background-color: var(--bs-dark);
1117
+ color: var(--bs-light);
1118
+ border-color: var(--bs-border-color-translucent);
1119
+ }
1120
+
1121
+ [data-bs-theme="dark"]
1122
+ .select2-container--bootstrap-5
1123
+ .select2-selection--single
1124
+ .select2-selection__rendered {
1125
+ color: var(--bs-light);
1126
+ }
1127
+
1128
+ [data-bs-theme="dark"]
1129
+ .select2-container--bootstrap-5
1130
+ .select2-selection--multiple
1131
+ .select2-selection__choice {
1132
+ background-color: var(--bs-secondary-bg);
1133
+ color: var(--bs-light);
1134
+ border-color: var(--bs-border-color-translucent);
1135
+ }
1136
+
1137
+ [data-bs-theme="dark"]
1138
+ .select2-container--bootstrap-5
1139
+ .select2-selection__placeholder {
1140
+ color: var(--bs-secondary-color);
1141
+ }
1142
+
1143
+ [data-bs-theme="dark"]
1144
+ .select2-container--bootstrap-5
1145
+ .select2-selection__arrow
1146
+ b {
1147
+ border-color: var(--bs-light) transparent transparent transparent;
1148
+ }
1149
+
1150
+ [data-bs-theme="dark"]
1151
+ .select2-container--bootstrap-5.select2-container--open
1152
+ .select2-selection {
1153
+ border-color: var(--bs-primary);
1154
+ box-shadow: 0 0 0 0.2rem rgba(var(--bs-primary-rgb), 0.25);
1155
+ }
1156
+
1157
+ [data-bs-theme="dark"] .select2-dropdown {
1158
+ background-color: var(--bs-dark);
1159
+ border-color: var(--bs-border-color-translucent);
1160
+ color: var(--bs-light);
1161
+ box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.5);
1162
+ }
1163
+
1164
+ [data-bs-theme="dark"] .select2-results__option {
1165
+ background-color: var(--bs-dark);
1166
+ color: var(--bs-light);
1167
+ }
1168
+
1169
+ [data-bs-theme="dark"] .select2-results__option--highlighted {
1170
+ background-color: var(--bs-primary);
1171
+ color: #fff;
1172
+ }
1173
+
1174
+ [data-bs-theme="dark"]
1175
+ .select2-container--bootstrap-5
1176
+ .select2-dropdown
1177
+ .select2-search
1178
+ .select2-search__field {
1179
+ background-color: var(--bs-dark);
1180
+ color: var(--bs-light);
1181
+ border: 1px solid var(--bs-border-color-translucent);
1182
+ }
1183
+
1184
+ [data-bs-theme="dark"]
1185
+ .select2-container--bootstrap-5
1186
+ .select2-search__field::placeholder {
1187
+ color: var(--bs-secondary-color);
1188
+ }
1189
+
1190
+ /* Ensure select2 and form fields use Bootstrap default font size */
1191
+ .select2-container--bootstrap-5 .select2-selection,
1192
+ .select2-container--bootstrap-5 .select2-results__option,
1193
+ .select2-container--bootstrap-5 .select2-search__field,
1194
+ .form-control {
1195
+ font-size: 1rem !important; /* Bootstrap default */
1196
+ line-height: 1.5; /* Also aligns vertically */
1197
+ }
1198
+
1199
+ /* Optional: improve vertical alignment of Select2 with input fields */
1200
+ .select2-container--bootstrap-5 .select2-selection {
1201
+ padding: 0.375rem 0.75rem;
1202
+ }
@@ -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 ]
6
+ before_action :validate_table, only: [ :show, :query, :export_csv, :new_record, :create_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
 
@@ -30,6 +30,25 @@ module Dbviewer
30
30
  @metadata = datatable_data[:metadata]
31
31
  end
32
32
 
33
+ def new_record
34
+ @table_columns = filter_accessible_columns(@table_name, database_manager.table_columns(@table_name))
35
+ @metadata = database_manager.table_metadata(@table_name)
36
+ @foreign_key_options = load_foreign_key_options(@metadata)
37
+
38
+ render layout: false
39
+ end
40
+
41
+ def create_record
42
+ model_class = database_manager.get_model_for(@table_name)
43
+ record = model_class.new(record_params)
44
+
45
+ if record.save
46
+ render json: { message: "Record created successfully" }
47
+ else
48
+ render json: { errors: record.errors, messages: record.errors.full_messages }, status: :unprocessable_entity
49
+ end
50
+ end
51
+
33
52
  def query
34
53
  all_columns = fetch_table_columns(@table_name)
35
54
  @columns = filter_accessible_columns(@table_name, all_columns)
@@ -45,8 +64,6 @@ module Dbviewer
45
64
  end
46
65
 
47
66
  @records = execute_query(@query)
48
-
49
- render :query
50
67
  end
51
68
 
52
69
  def export_csv
@@ -56,7 +73,6 @@ module Dbviewer
56
73
  return
57
74
  end
58
75
 
59
- include_headers = params[:include_headers] != "0"
60
76
  query_params = Dbviewer::Datatable::QueryParams.new(
61
77
  page: @current_page,
62
78
  per_page: (params[:limit] || 10000).to_i,
@@ -64,10 +80,9 @@ module Dbviewer
64
80
  direction: @order_direction,
65
81
  column_filters: @column_filters.reject { |_, v| v.blank? }
66
82
  )
67
- csv_data = export_table_to_csv(@table_name, query_params, include_headers)
68
83
 
69
- timestamp = Time.now.strftime("%Y%m%d%H%M%S")
70
- filename = "#{@table_name}_#{timestamp}.csv"
84
+ csv_data = export_table_to_csv(@table_name, query_params, params[:include_headers] != "0")
85
+ filename = "#{@table_name}_#{Time.now.strftime('%Y%m%d%H%M%S')}.csv"
71
86
 
72
87
  send_data csv_data,
73
88
  type: "text/csv; charset=utf-8; header=present",
@@ -76,6 +91,16 @@ module Dbviewer
76
91
 
77
92
  private
78
93
 
94
+ def record_params
95
+ accessible_columns = filter_accessible_columns(@table_name, database_manager.table_columns(@table_name))
96
+
97
+ permitted_fields = accessible_columns
98
+ .reject { |col| %w[id created_at updated_at].include?(col[:name]) }
99
+ .map { |col| col[:name].to_sym }
100
+
101
+ params.require(:record).permit(*permitted_fields)
102
+ end
103
+
79
104
  def set_table_name
80
105
  @table_name = params[:id]
81
106
  end
@@ -86,78 +111,82 @@ module Dbviewer
86
111
 
87
112
  def set_query_filters
88
113
  @current_page = [ 1, params[:page].to_i ].max
89
- @per_page = params[:per_page] ? params[:per_page].to_i : Dbviewer.configuration.default_per_page
90
- @per_page = Dbviewer.configuration.default_per_page unless Dbviewer.configuration.per_page_options.include?(@per_page)
114
+ @per_page = Dbviewer.configuration.per_page_options.include?(params[:per_page].to_i) ? params[:per_page].to_i : Dbviewer.configuration.default_per_page
91
115
  @order_by = params[:order_by].presence || determine_default_order_column
92
- @order_direction = params[:order_direction].upcase if params[:order_direction].present?
93
- @order_direction = "DESC" unless %w[ASC DESC].include?(@order_direction)
116
+ @order_direction = %w[ASC DESC].include?(params[:order_direction].to_s.upcase) ? params[:order_direction].upcase : "DESC"
94
117
  @column_filters = params[:column_filters].presence ? params[:column_filters].to_enum.to_h : {}
95
118
  end
96
119
 
97
120
  def set_global_filters
98
- # Store creation filter datetimes in session to persist between table navigation
99
- if params[:creation_filter_start].present?
100
- session[:creation_filter_start] = params[:creation_filter_start]
101
- end
102
-
103
- if params[:creation_filter_end].present?
104
- session[:creation_filter_end] = params[:creation_filter_end]
105
- end
121
+ session[:creation_filter_start] = params[:creation_filter_start] if params[:creation_filter_start].present?
122
+ session[:creation_filter_end] = params[:creation_filter_end] if params[:creation_filter_end].present?
106
123
 
107
- # Clear filters if explicitly requested
108
124
  if params[:clear_creation_filter] == "true"
109
125
  session.delete(:creation_filter_start)
110
126
  session.delete(:creation_filter_end)
111
127
  end
112
128
 
113
- # Set instance variables for view access
114
129
  @creation_filter_start = session[:creation_filter_start]
115
130
  @creation_filter_end = session[:creation_filter_end]
116
-
117
- # Initialize column_filters if not present
118
131
  @column_filters ||= {}
119
132
 
120
- # Apply creation filters to column_filters if the table has a created_at column
121
133
  if has_timestamp_column?(@table_name) && (@creation_filter_start.present? || @creation_filter_end.present?)
122
- # Clear any existing created_at filters
123
134
  %w[created_at created_at_operator created_at_end].each { |key| @column_filters.delete(key) }
124
135
 
125
- case
126
- when @creation_filter_start.present? && @creation_filter_end.present?
127
- @column_filters.merge!({
128
- "created_at" => @creation_filter_start,
129
- "created_at_end" => @creation_filter_end
130
- })
131
- when @creation_filter_start.present?
132
- @column_filters.merge!({
133
- "created_at" => @creation_filter_start,
134
- "created_at_operator" => "gte"
135
- })
136
- when @creation_filter_end.present?
137
- @column_filters.merge!({
138
- "created_at" => @creation_filter_end,
139
- "created_at_operator" => "lte"
140
- })
136
+ if @creation_filter_start.present? && @creation_filter_end.present?
137
+ @column_filters.merge!("created_at" => @creation_filter_start, "created_at_end" => @creation_filter_end)
138
+ elsif @creation_filter_start.present?
139
+ @column_filters.merge!("created_at" => @creation_filter_start, "created_at_operator" => "gte")
140
+ elsif @creation_filter_end.present?
141
+ @column_filters.merge!("created_at" => @creation_filter_end, "created_at_operator" => "lte")
141
142
  end
142
143
  end
143
144
  end
144
145
 
145
- # Determine the default order column using configurable ordering logic
146
146
  def determine_default_order_column
147
- # Get the table columns to check what's available
148
147
  columns = @columns || fetch_table_columns(@table_name)
149
148
  column_names = columns.map { |col| col[:name] }
150
149
 
151
- # Try the configured default order column first
152
- default_column = Dbviewer.configuration.default_order_column
153
- return default_column if default_column && column_names.include?(default_column)
150
+ Dbviewer.configuration.default_order_column.presence_in(column_names) ||
151
+ database_manager.primary_key(@table_name) ||
152
+ columns.first&.dig(:name)
153
+ end
154
154
 
155
- # Fall back to primary key
156
- primary_key = database_manager.primary_key(@table_name)
157
- return primary_key if primary_key.present?
155
+ def find_display_column(columns)
156
+ column_names = columns.map { |c| c[:name] }
157
+ # Common display columns to check for (TODO: next we can add this to configuration for better dev control)
158
+ # If none found, fallback to first non-id column or id column
159
+ # This ensures we have a sensible default for display purposes
160
+ %w[name title label display_name username email description].find { |name| column_names.include?(name) } ||
161
+ columns.find { |c| [ :string, :text ].include?(c[:type]) && c[:name] != "id" }&.[](:name) ||
162
+ columns.find { |c| c[:name] != "id" }&.[](:name) || "id"
163
+ end
164
+
165
+ def load_foreign_key_options(metadata)
166
+ options = {}
167
+ metadata[:foreign_keys].each do |fk|
168
+ foreign_table = fk[:to_table]
169
+ foreign_key_column = fk[:primary_key] || "id"
170
+ next unless access_control.table_accessible?(foreign_table)
158
171
 
159
- # Final fallback to first column
160
- columns.first ? columns.first[:name] : nil
172
+ begin
173
+ foreign_columns = database_manager.table_columns(foreign_table)
174
+ display_column = find_display_column(foreign_columns)
175
+
176
+ foreign_model = database_manager.get_model_for(foreign_table)
177
+ records = foreign_model
178
+ .select([ foreign_key_column, display_column ].uniq)
179
+ .limit(100)
180
+ .order(display_column)
181
+ .map { |r| [ r[display_column].to_s, r[foreign_key_column] ] }
182
+
183
+ options[fk[:column]] = records
184
+ rescue => e
185
+ Rails.logger.error("Error fetching foreign key options for #{foreign_table}: #{e.message}")
186
+ options[fk[:column]] = []
187
+ end
188
+ end
189
+ options
161
190
  end
162
191
  end
163
192
  end
@@ -6,6 +6,7 @@ module Dbviewer
6
6
 
7
7
  include DatatableUiHelper
8
8
  include DatatableUiFilterHelper
9
+ include DatatableUiFormHelper
9
10
  include DatatableUiPaginationHelper
10
11
  include DatatableUiSortingHelper
11
12
  include DatatableUiTableHelper
@@ -0,0 +1,18 @@
1
+ module Dbviewer
2
+ module DatatableUiFormHelper
3
+ def determine_field_type(column_type)
4
+ case column_type
5
+ when :boolean
6
+ :check_box
7
+ when :text
8
+ :text_area
9
+ when :date
10
+ :date_field
11
+ when :datetime, :timestamp
12
+ :datetime_local_field
13
+ else
14
+ :text_field
15
+ end
16
+ end
17
+ end
18
+ end
@@ -106,7 +106,7 @@ module Dbviewer
106
106
  end
107
107
 
108
108
  content_tag(:td, class: "text-center action-column") do
109
- content_tag(:div, class: "d-flex gap-1 justify-content-center") do
109
+ content_tag(:div, class: "d-flex flex-column flex-md-row gap-1 justify-content-center") do
110
110
  # View Record button (existing)
111
111
  view_button = button_tag(
112
112
  type: "button",
@@ -15,7 +15,7 @@
15
15
  <div class="row h-100">
16
16
  <div class="col-md-12 p-0">
17
17
  <div class="card h-100">
18
- <div class="card-header d-flex justify-content-between align-items-center">
18
+ <div class="card-header d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-2">
19
19
  <h5 class="mb-0">
20
20
  <i class="bi bi-diagram-3"></i> Entity Relationship Diagram
21
21
  </h5>
@@ -28,7 +28,7 @@
28
28
  <i class="bi bi-zoom-out"></i>
29
29
  </button>
30
30
  <button id="resetView" class="btn btn-sm btn-outline-secondary me-1">
31
- <i class="bi bi-arrow-counterclockwise"></i> Reset
31
+ <i class="bi bi-arrow-counterclockwise"></i>
32
32
  </button>
33
33
  <div class="dropdown">
34
34
  <button class="btn btn-sm btn-outline-primary dropdown-toggle" type="button" id="downloadButton" data-bs-toggle="dropdown" aria-expanded="false">
@@ -240,27 +240,6 @@
240
240
  </div>
241
241
  <% end %>
242
242
 
243
- <!-- Table Access Chart -->
244
- <% if @stats[:tables_queried].present? %>
245
- <div class="row mb-4">
246
- <div class="col-md-12">
247
- <div class="card <%= 'border-info' if @filtered_stats %>">
248
- <div class="card-header <%= @filtered_stats ? 'bg-info-subtle' : '' %>">
249
- <div class="d-flex justify-content-between align-items-center">
250
- <h5 class="card-title mb-0">Table Access Frequency</h5>
251
- <% if @filtered_stats %>
252
- <span class="badge bg-info text-dark">Filtered</span>
253
- <% end %>
254
- </div>
255
- </div>
256
- <div class="card-body">
257
- <canvas id="tablesChart" width="400" height="150"></canvas>
258
- </div>
259
- </div>
260
- </div>
261
- </div>
262
- <% end %>
263
-
264
243
  <!-- Filters -->
265
244
  <div class="card mb-4">
266
245
  <div class="card-header">
@@ -0,0 +1,70 @@
1
+ <% if @errors.present? %>
2
+ <div class="alert alert-danger">
3
+ <ul class="mb-0">
4
+ <% @errors.full_messages.each do |message| %>
5
+ <li><%= message %></li>
6
+ <% end %>
7
+ </ul>
8
+ </div>
9
+ <% end %>
10
+
11
+ <div class="modal-header">
12
+ <h5 class="modal-title" id="newRecordModalLabel">
13
+ <i class="bi bi-plus-circle me-1"></i> Create New <%= @table_name.humanize.titleize %> Record
14
+ </h5>
15
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
16
+ </div>
17
+
18
+ <div class="modal-body">
19
+ <%= form_with url: create_record_table_path(@table_name), method: :post, id: "newRecordForm" do |form| %>
20
+ <% @table_columns.each do |column| %>
21
+ <%
22
+ column_name = column[:name]
23
+ is_primary_key = @metadata[:primary_key] == column_name
24
+ skip_column = is_primary_key && %w[id].include?(column_name.downcase)
25
+
26
+ # Skip auto-increment primary keys and timestamps
27
+ next if skip_column || %w[created_at updated_at].include?(column_name)
28
+ %>
29
+
30
+ <div class="mb-3">
31
+ <%
32
+ # Handle different field types
33
+ field_type = determine_field_type(column[:type])
34
+ foreign_key = @metadata[:foreign_keys].find { |fk| fk[:column] == column_name }
35
+ field_id = "record_#{column_name}"
36
+ required = !column[:null]
37
+ %>
38
+
39
+ <%= form.label "record[#{column_name}]", column_name.humanize, class: "form-label" %>
40
+
41
+ <% if foreign_key && @foreign_key_options[column_name].present? %>
42
+ <%= form.select "record[#{column_name}]",
43
+ options_for_select(@foreign_key_options[column_name]),
44
+ { include_blank: column[:null] ? "-- Select --" : false },
45
+ { class: "form-select form-control select2-dropdown" }
46
+ %>
47
+ <% elsif field_type == :check_box %>
48
+ <div class="form-check">
49
+ <%= form.check_box "record[#{column_name}]", class: "form-check-input", id: field_id %>
50
+ </div>
51
+ <% elsif field_type == :text_area %>
52
+ <%= form.text_area "record[#{column_name}]", class: "form-control", id: field_id, rows: 3, required: required %>
53
+ <% else %>
54
+ <%= form.send(field_type, "record[#{column_name}]", class: "form-control", id: field_id, required: required) %>
55
+ <% end %>
56
+
57
+ <% if column[:default].present? && column[:default] != "NULL" %>
58
+ <div class="form-text text-muted">Default: <%= column[:default] %></div>
59
+ <% end %>
60
+ </div>
61
+ <% end %>
62
+ <% end %>
63
+ </div>
64
+
65
+ <div class="modal-footer">
66
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
67
+ <button type="submit" form="newRecordForm" class="btn btn-primary" id="createRecordButton">
68
+ <i class="bi bi-plus-lg me-1"></i>Create Record
69
+ </button>
70
+ </div>
@@ -22,7 +22,7 @@
22
22
  <div class="d-flex justify-content-between align-items-center mb-4">
23
23
  <h1>Query: <%= @table_name %></h1>
24
24
  <div>
25
- <%= link_to table_path(@table_name), class: "btn btn-outline-primary" do %>
25
+ <%= link_to table_path(@table_name), class: "btn btn-outline-primary d-none d-md-inline-flex" do %>
26
26
  <i class="bi bi-arrow-left me-1"></i> Back to Table
27
27
  <% end %>
28
28
  </div>
@@ -41,8 +41,8 @@
41
41
  <%= form.hidden_field :query, id: "query-input", value: @query.to_s %>
42
42
  </div>
43
43
 
44
- <div class="d-flex justify-content-between align-items-start">
45
- <div class="form-text">
44
+ <div class="d-md-flex justify-content-between align-items-start">
45
+ <div class="form-text mb-3 mb-md-0">
46
46
  <strong>Examples:</strong><br>
47
47
  <div class="example-queries">
48
48
  <code class="example-query btn btn-sm btn-outline-secondary mb-1">SELECT * FROM <%= @table_name %> LIMIT 100</code>
@@ -5,8 +5,15 @@
5
5
  <% content_for :head do %>
6
6
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
7
7
  <script src="https://cdn.jsdelivr.net/npm/svg-pan-zoom@3.6.1/dist/svg-pan-zoom.min.js"></script>
8
+ <!-- jQuery for Select2 -->
9
+ <script src="https://cdn.jsdelivr.net/npm/jquery@3.7.0/dist/jquery.min.js"></script>
10
+ <!-- Select2 for searchable dropdowns -->
11
+ <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
12
+ <link href="https://cdn.jsdelivr.net/npm/select2-bootstrap-5-theme@1.3.0/dist/select2-bootstrap-5-theme.min.css" rel="stylesheet" />
13
+ <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
8
14
  <%= stylesheet_link_tag "dbviewer/table", "data-turbo-track": "reload" %>
9
15
  <%= javascript_include_tag "dbviewer/table", "data-turbo-track": "reload" %>
16
+ <%= javascript_include_tag "dbviewer/record_creation", "data-turbo-track": "reload" %>
10
17
  <% end %>
11
18
 
12
19
  <% content_for :sidebar_active do %>active<% end %>
@@ -18,18 +25,22 @@
18
25
  </div>
19
26
  <div class="d-flex flex-wrap gap-2">
20
27
  <button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#tableStructureModal">
21
- <i class="bi bi-table me-1"></i> Table Structure
28
+ <i class="bi bi-table me-1"></i>
29
+ <span class="d-none d-sm-inline">Table Structure</span>
22
30
  </button>
23
31
  <button type="button" class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#miniErdModal">
24
- <i class="bi bi-diagram-3 me-1"></i> View Relationships
32
+ <i class="bi bi-diagram-3 me-1"></i>
33
+ <span class="d-none d-sm-inline">View Relationships</span>
25
34
  </button>
26
35
  <% if Dbviewer.configuration.enable_data_export %>
27
36
  <button type="button" class="btn btn-success" data-bs-toggle="modal" data-bs-target="#csvExportModal">
28
- <i class="bi bi-file-earmark-spreadsheet me-1"></i> Export CSV
37
+ <i class="bi bi-file-earmark-spreadsheet me-1"></i>
38
+ <span class="d-none d-sm-inline">Export CSV</span>
29
39
  </button>
30
40
  <% end %>
31
41
  <%= link_to query_table_path(@table_name), class: "btn btn-primary" do %>
32
- <i class="bi bi-code-square me-1"></i> Run SQL Query
42
+ <i class="bi bi-code-square me-1"></i>
43
+ <span class="d-none d-sm-inline">Run SQL Query</span>
33
44
  <% end %>
34
45
  </div>
35
46
  </div>
@@ -99,25 +110,25 @@
99
110
  <div class="dbviewer-card card mb-4" id="table-section">
100
111
  <div class="card-header d-flex justify-content-between align-items-center">
101
112
  <h5 class="mb-0">
102
- <select id="per-page-select" class="form-select form-select-sm" onchange="window.location.href='<%= table_path(@table_name) %>?<%= per_page_url_params(@table_name) %>'">
113
+ <select id="per-page-select" class="form-select form-select-sm pe-4" onchange="window.location.href='<%= table_path(@table_name) %>?<%= per_page_url_params(@table_name) %>'">
103
114
  <% Dbviewer.configuration.per_page_options.each do |option| %>
104
115
  <option value="<%= option %>" <%= 'selected' if @per_page == option %>><%= option %></option>
105
116
  <% end %>
106
117
  </select>
107
118
  </h5>
108
- <div class="d-flex align-items-center table-actions">
119
+ <div class="d-none d-md-flex align-items-center table-actions">
109
120
  <% if @order_by.present? %>
110
121
  <span class="badge bg-primary me-2" title="Sort order">
111
122
  <i class="bi bi-sort-<%= @order_direction == "ASC" ? "up" : "down" %> me-1"></i>
112
123
  <%= @order_by %> (<%= @order_direction == "ASC" ? "ascending" : "descending" %>)
113
124
  </span>
114
125
  <% end %>
115
- <span class="badge bg-secondary">Total: <%= @total_count %> records</span>
116
- <% active_filters = @column_filters.reject { |_, v| v.blank? }.size %>
126
+ <span class="d-none d-md-block badge bg-secondary">Total: <%= @total_count %> records</span>
127
+ <% active_filters = @column_filters.reject { |k, v| v.blank? || k.to_s.ends_with?('_operator') }.size %>
117
128
  <% if active_filters > 0 %>
118
129
  <span class="badge bg-info ms-2" title="Active filters"><i class="bi bi-funnel-fill me-1"></i><%= active_filters %></span>
119
130
  <% end %>
120
- <button type="button" class="btn btn-outline-secondary btn-sm ms-2" id="fullscreen-toggle" title="Toggle fullscreen">
131
+ <button type="button" class="d-none d-md-block btn btn-outline-secondary btn-sm ms-2" id="fullscreen-toggle" title="Toggle fullscreen">
121
132
  <i class="bi bi-fullscreen" id="fullscreen-icon"></i>
122
133
  </button>
123
134
  </div>
@@ -348,5 +359,28 @@
348
359
  </div>
349
360
  </div>
350
361
  <% end %>
362
+
363
+ <!-- Floating Add Record Button -->
364
+ <%# TODO: move this to helpers so that we can have centralized creation check %>
365
+ <% if @table_name != 'schema_migrations'%>
366
+ <div class="floating-add-record d-none d-lg-block">
367
+ <button id="floatingAddRecordBtn"
368
+ class="btn btn-success btn-lg shadow-lg rounded-circle"
369
+ type="button"
370
+ title="Add New Record">
371
+ <i class="bi bi-plus-lg"></i>
372
+ </button>
373
+ </div>
374
+ <% end %>
375
+
376
+ <!-- New Record Modal -->
377
+ <div id="newRecordModal" class="modal fade" tabindex="-1" aria-labelledby="newRecordModalLabel" aria-hidden="true">
378
+ <div class="modal-dialog modal-lg">
379
+ <div class="modal-content">
380
+ <!-- Content will be loaded via AJAX -->
381
+ </div>
382
+ </div>
383
+ </div>
384
+
351
385
  <input type="hidden" id="mini_erd_table_path" name="mini_erd_table_path" value="<%= dbviewer.mini_erd_api_table_path(@table_name, format: :json) %>">
352
386
  <input type="hidden" id="table_name" name="table_name" value="<%= @table_name %>">
data/config/routes.rb CHANGED
@@ -4,6 +4,8 @@ Dbviewer::Engine.routes.draw do
4
4
  get "query"
5
5
  post "query"
6
6
  get "export_csv"
7
+ get "new_record"
8
+ post "create_record"
7
9
  end
8
10
  end
9
11
 
@@ -2,12 +2,13 @@ module Dbviewer
2
2
  module Database
3
3
  # DynamicModelFactory creates and manages ActiveRecord models for database tables
4
4
  class DynamicModelFactory
5
- # Initialize with a connection and cache manager
5
+ # Initialize with a connection, cache manager, and metadata manager
6
6
  # @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
7
- # @param cache_manager [Dbviewer::Database::CacheManager] Cache manager instance
8
- def initialize(connection, cache_manager)
7
+ # @param cache_manager [Dbviewer::Cache::Base] Cache manager instance
8
+ def initialize(connection, cache_manager, metadata_manager)
9
9
  @connection = connection
10
10
  @cache_manager = cache_manager
11
+ @metadata_manager = metadata_manager
11
12
  end
12
13
 
13
14
  # Get or create an ActiveRecord model for a table
@@ -16,7 +17,7 @@ module Dbviewer
16
17
  def get_model_for(table_name)
17
18
  # Cache models for shorter time since they might need refreshing more frequently
18
19
  @cache_manager.fetch("model-#{table_name}", expires_in: 300) do
19
- create_model_for(table_name)
20
+ find_or_create_model_for(table_name)
20
21
  end
21
22
  end
22
23
 
@@ -25,14 +26,15 @@ module Dbviewer
25
26
  # Create a new ActiveRecord model for a table
26
27
  # @param table_name [String] Name of the table
27
28
  # @return [Class] ActiveRecord model class for the table
28
- def create_model_for(table_name)
29
+ def find_or_create_model_for(table_name)
29
30
  class_name = table_name.classify
30
31
 
31
32
  # Check if we can reuse an existing constant
32
33
  existing_model = handle_existing_constant(class_name, table_name)
33
34
  return existing_model if existing_model
34
35
 
35
- model = create_active_record_model(class_name, table_name)
36
+ table_metadata = @metadata_manager.table_metadata(table_name)
37
+ model = create_active_record_model(class_name, table_name, table_metadata)
36
38
  model.establish_connection(@connection.instance_variable_get(:@config))
37
39
  model
38
40
  end
@@ -64,7 +66,7 @@ module Dbviewer
64
66
  # @param class_name [String] The constant name for the model
65
67
  # @param table_name [String] The table name this model should represent
66
68
  # @return [Class] New ActiveRecord model class
67
- def create_active_record_model(class_name, table_name)
69
+ def create_active_record_model(class_name, table_name, table_metadata)
68
70
  Dbviewer.const_set(class_name, Class.new(ActiveRecord::Base) do
69
71
  self.table_name = table_name
70
72
 
@@ -76,25 +78,17 @@ module Dbviewer
76
78
  self.primary_key = "id"
77
79
  end
78
80
 
79
- # Disable STI
80
- self.inheritance_column = :_type_disabled
81
-
82
- # Disable timestamps for better compatibility
83
- self.record_timestamps = false
84
-
85
- # Make the model read-only to prevent accidental data modifications
86
- def readonly?
87
- true
88
- end
89
-
90
- # Disable write operations at the class level
91
- class << self
92
- def delete_all(*)
93
- raise ActiveRecord::ReadOnlyRecord, "#{name} is a read-only model"
94
- end
95
-
96
- def update_all(*)
97
- raise ActiveRecord::ReadOnlyRecord, "#{name} is a read-only model"
81
+ # Add validations from indexes
82
+ table_metadata[:indexes].each do |index|
83
+ next unless index[:unique] && index[:columns].present?
84
+
85
+ columns = index[:columns]
86
+ column = columns.first
87
+ if columns.size == 1
88
+ validates column, uniqueness: true
89
+ elsif columns.size > 1
90
+ # Validate composite uniqueness using `scope`
91
+ validates column, uniqueness: { scope: columns[1..].map(&:to_sym) }
98
92
  end
99
93
  end
100
94
  end)
@@ -12,7 +12,7 @@ module Dbviewer
12
12
  ensure_connection
13
13
  @cache_manager = ::Dbviewer::Cache::InMemory.new(configuration.cache_expiry)
14
14
  @table_metadata_manager = ::Dbviewer::Database::MetadataManager.new(@connection, @cache_manager)
15
- @dynamic_model_factory = ::Dbviewer::Database::DynamicModelFactory.new(@connection, @cache_manager)
15
+ @dynamic_model_factory = ::Dbviewer::Database::DynamicModelFactory.new(@connection, @cache_manager, @table_metadata_manager)
16
16
  @query_executor = ::Dbviewer::Query::Executor.new(@connection, configuration)
17
17
  @table_query_operations = ::Dbviewer::Datatable::QueryOperations.new(
18
18
  @connection,
@@ -0,0 +1,28 @@
1
+ module Dbviewer
2
+ # Helper class for configuring PII masking rules
3
+ class PiiConfigurator
4
+ def initialize(configuration)
5
+ @configuration = configuration
6
+ end
7
+
8
+ # Define a PII masking rule
9
+ # @param column_spec [String] Table and column in format "table.column"
10
+ # @param with [Symbol, Proc] Masking rule - either built-in symbol or custom proc
11
+ def mask(column_spec, with:)
12
+ @configuration.pii_rules[column_spec] = with
13
+ end
14
+
15
+ # Define a custom masking function
16
+ # @param name [Symbol] Name of the custom mask
17
+ # @param block [Proc] The masking function
18
+ def custom_mask(name, block)
19
+ @configuration.custom_pii_masks[name] = block
20
+ end
21
+
22
+ # Enable or disable PII masking globally
23
+ # @param enabled [Boolean] Whether to enable PII masking
24
+ def enabled=(enabled)
25
+ @configuration.enable_pii_masking = enabled
26
+ end
27
+ end
28
+ end
@@ -61,7 +61,7 @@ module Dbviewer
61
61
  # @param event [ActiveSupport::Notifications::Event] The notification event
62
62
  # @return [Boolean] True if the query should be skipped
63
63
  def self.should_skip_internal_query?(event)
64
- event.payload[:name].include?("Dbviewer::") ||
64
+ event.payload[:name]&.include?("Dbviewer::") ||
65
65
  # SQLite specific check for size queries
66
66
  event.payload[:sql].include?("PRAGMA") ||
67
67
  # PostgreSQL specific check for size queries
@@ -1,3 +1,3 @@
1
1
  module Dbviewer
2
- VERSION = "0.9.2"
2
+ VERSION = "0.9.4-alpha.1"
3
3
  end
data/lib/dbviewer.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "dbviewer/version"
2
2
  require "dbviewer/configuration"
3
3
  require "dbviewer/engine"
4
+ require "dbviewer/pii_configuration"
5
+
4
6
  require "dbviewer/validator/sql"
5
7
  require "dbviewer/security/sql_parser"
6
8
  require "dbviewer/security/access_control"
@@ -32,33 +34,6 @@ require "propshaft"
32
34
  module Dbviewer
33
35
  # Main module for the database viewer
34
36
 
35
- # Helper class for configuring PII masking rules
36
- class PiiConfigurator
37
- def initialize(configuration)
38
- @configuration = configuration
39
- end
40
-
41
- # Define a PII masking rule
42
- # @param column_spec [String] Table and column in format "table.column"
43
- # @param with [Symbol, Proc] Masking rule - either built-in symbol or custom proc
44
- def mask(column_spec, with:)
45
- @configuration.pii_rules[column_spec] = with
46
- end
47
-
48
- # Define a custom masking function
49
- # @param name [Symbol] Name of the custom mask
50
- # @param block [Proc] The masking function
51
- def custom_mask(name, block)
52
- @configuration.custom_pii_masks[name] = block
53
- end
54
-
55
- # Enable or disable PII masking globally
56
- # @param enabled [Boolean] Whether to enable PII masking
57
- def enabled=(enabled)
58
- @configuration.enable_pii_masking = enabled
59
- end
60
- end
61
-
62
37
  class << self
63
38
  # Module accessor for configuration
64
39
  attr_writer :configuration
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.2
4
+ version: 0.9.4.pre.alpha.1
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-06-28 00:00:00.000000000 Z
11
+ date: 2025-07-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -86,6 +86,7 @@ files:
86
86
  - app/assets/javascripts/dbviewer/home.js
87
87
  - app/assets/javascripts/dbviewer/layout.js
88
88
  - app/assets/javascripts/dbviewer/query.js
89
+ - app/assets/javascripts/dbviewer/record_creation.js
89
90
  - app/assets/javascripts/dbviewer/sidebar.js
90
91
  - app/assets/javascripts/dbviewer/table.js
91
92
  - app/assets/javascripts/dbviewer/utility.js
@@ -119,6 +120,7 @@ files:
119
120
  - app/helpers/dbviewer/application_helper.rb
120
121
  - app/helpers/dbviewer/database_helper.rb
121
122
  - app/helpers/dbviewer/datatable_ui_filter_helper.rb
123
+ - app/helpers/dbviewer/datatable_ui_form_helper.rb
122
124
  - app/helpers/dbviewer/datatable_ui_helper.rb
123
125
  - app/helpers/dbviewer/datatable_ui_pagination_helper.rb
124
126
  - app/helpers/dbviewer/datatable_ui_sorting_helper.rb
@@ -136,6 +138,7 @@ files:
136
138
  - app/views/dbviewer/shared/_tables_sidebar.html.erb
137
139
  - app/views/dbviewer/tables/_table_structure.html.erb
138
140
  - app/views/dbviewer/tables/index.html.erb
141
+ - app/views/dbviewer/tables/new_record.html.erb
139
142
  - app/views/dbviewer/tables/query.html.erb
140
143
  - app/views/dbviewer/tables/show.html.erb
141
144
  - app/views/layouts/dbviewer/application.html.erb
@@ -152,6 +155,7 @@ files:
152
155
  - lib/dbviewer/datatable/query_operations.rb
153
156
  - lib/dbviewer/datatable/query_params.rb
154
157
  - lib/dbviewer/engine.rb
158
+ - lib/dbviewer/pii_configuration.rb
155
159
  - lib/dbviewer/query/analyzer.rb
156
160
  - lib/dbviewer/query/executor.rb
157
161
  - lib/dbviewer/query/logger.rb
@@ -190,9 +194,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
190
194
  version: '0'
191
195
  required_rubygems_version: !ruby/object:Gem::Requirement
192
196
  requirements:
193
- - - ">="
197
+ - - ">"
194
198
  - !ruby/object:Gem::Version
195
- version: '0'
199
+ version: 1.3.1
196
200
  requirements: []
197
201
  rubygems_version: 3.4.10
198
202
  signing_key: