dbwatcher 1.0.0 → 1.1.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 +4 -4
- data/README.md +81 -210
- data/app/assets/config/dbwatcher_manifest.js +15 -0
- data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
- data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
- data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
- data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
- data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
- data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
- data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
- data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
- data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
- data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
- data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
- data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
- data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
- data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
- data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
- data/app/assets/stylesheets/dbwatcher/application.css +423 -0
- data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
- data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
- data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
- data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
- data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
- data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
- data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
- data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
- data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
- data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
- data/app/controllers/dbwatcher/base_controller.rb +8 -2
- data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
- data/app/helpers/dbwatcher/component_helper.rb +29 -0
- data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
- data/app/helpers/dbwatcher/session_helper.rb +3 -2
- data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
- data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
- data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
- data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
- data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
- data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
- data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
- data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
- data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
- data/app/views/dbwatcher/sessions/index.html.erb +14 -10
- data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
- data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
- data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
- data/app/views/dbwatcher/sessions/show.html.erb +3 -346
- data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
- data/app/views/layouts/dbwatcher/application.html.erb +125 -247
- data/bin/compile_scss +49 -0
- data/config/routes.rb +26 -0
- data/lib/dbwatcher/configuration.rb +102 -8
- data/lib/dbwatcher/engine.rb +17 -7
- data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
- data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
- data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
- data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
- data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
- data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
- data/lib/dbwatcher/services/base_service.rb +64 -0
- data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
- data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
- data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
- data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +603 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +280 -0
- data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
- data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
- data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
- data/lib/dbwatcher/services/diagram_data.rb +65 -0
- data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
- data/lib/dbwatcher/services/diagram_generator.rb +154 -0
- data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
- data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
- data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
- data/lib/dbwatcher/services/diagram_system.rb +69 -0
- data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
- data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
- data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +140 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +48 -0
- data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
- data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
- data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +118 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
- data/lib/dbwatcher/storage/api/session_api.rb +47 -0
- data/lib/dbwatcher/storage/base_storage.rb +7 -0
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +58 -1
- metadata +94 -2
@@ -0,0 +1,96 @@
|
|
1
|
+
<!-- Table List Navigation -->
|
2
|
+
<div class="bg-white border-b border-gray-200 p-4">
|
3
|
+
<h3 class="text-sm font-medium text-gray-900 mb-3">Tables with Changes</h3>
|
4
|
+
<div class="flex flex-wrap gap-2">
|
5
|
+
<template x-for="[tableName, summary] in Object.entries(tablesData)" :key="tableName">
|
6
|
+
<button
|
7
|
+
@click="scrollToTable(tableName)"
|
8
|
+
class="px-3 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded-md transition-colors">
|
9
|
+
<span x-text="tableName"></span>
|
10
|
+
<span class="ml-1 text-gray-500" x-text="`(${Object.values(summary.changes || {}).reduce((sum, count) => sum + count, 0)})`"></span>
|
11
|
+
</button>
|
12
|
+
</template>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<!-- Table Details -->
|
17
|
+
<template x-for="[tableName, summary] in Object.entries(tablesData)" :key="tableName">
|
18
|
+
<div :id="`table-${tableName}`" class="border-b border-gray-300" :data-table-name="tableName">
|
19
|
+
<!-- Table Header -->
|
20
|
+
<div class="bg-gray-50 px-4 py-3 border-b border-gray-200">
|
21
|
+
<div class="flex items-center justify-between">
|
22
|
+
<div class="flex items-center space-x-3">
|
23
|
+
<h4 class="text-sm font-medium text-gray-900" x-text="tableName"></h4>
|
24
|
+
<div class="flex space-x-2">
|
25
|
+
<template x-for="[operation, count] in Object.entries(summary.changes || {})" :key="operation">
|
26
|
+
<span :class="getOperationClass(operation)" class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium">
|
27
|
+
<span x-text="`${operation}: ${count}`"></span>
|
28
|
+
</span>
|
29
|
+
</template>
|
30
|
+
</div>
|
31
|
+
</div>
|
32
|
+
<button
|
33
|
+
@click="toggleColumnSelector(tableName)"
|
34
|
+
class="text-sm text-blue-600 hover:text-blue-800">
|
35
|
+
Configure Columns
|
36
|
+
</button>
|
37
|
+
</div>
|
38
|
+
</div>
|
39
|
+
|
40
|
+
<!-- Column Selector -->
|
41
|
+
<div x-show="showColumnSelector === tableName" x-collapse class="bg-gray-50 px-4 py-3 border-b border-gray-200">
|
42
|
+
<div class="flex items-center justify-between mb-2">
|
43
|
+
<span class="text-sm font-medium text-gray-700">Column Visibility</span>
|
44
|
+
<div class="space-x-2">
|
45
|
+
<button @click="selectAllColumns(tableName)" class="text-xs text-blue-600 hover:text-blue-800">All</button>
|
46
|
+
<button @click="selectNoneColumns(tableName)" class="text-xs text-blue-600 hover:text-blue-800">None</button>
|
47
|
+
</div>
|
48
|
+
</div>
|
49
|
+
<div class="grid grid-cols-4 gap-2">
|
50
|
+
<template x-for="[column, visible] in Object.entries(tableColumns[tableName] || {})" :key="column">
|
51
|
+
<label class="flex items-center space-x-2">
|
52
|
+
<input
|
53
|
+
type="checkbox"
|
54
|
+
:checked="visible"
|
55
|
+
@change="tableColumns[tableName][column] = $event.target.checked"
|
56
|
+
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
|
57
|
+
<span class="text-xs text-gray-700" x-text="column"></span>
|
58
|
+
</label>
|
59
|
+
</template>
|
60
|
+
</div>
|
61
|
+
</div>
|
62
|
+
|
63
|
+
<!-- Table Content -->
|
64
|
+
<div class="overflow-auto">
|
65
|
+
<table class="compact-table w-full">
|
66
|
+
<thead>
|
67
|
+
<tr class="sticky top-0 bg-gray-100 z-10">
|
68
|
+
<th class="text-center w-16 sticky left-0 bg-gray-100 z-20 border-r border-gray-300">
|
69
|
+
<span class="text-xs">Op</span>
|
70
|
+
</th>
|
71
|
+
<th class="text-left w-24 sticky bg-gray-100 z-20 border-r border-gray-300">Time</th>
|
72
|
+
<template x-for="[column, visible] in Object.entries(tableColumns[tableName] || {})" :key="column">
|
73
|
+
<th x-show="visible" class="text-left min-w-32 px-2" x-text="column"></th>
|
74
|
+
</template>
|
75
|
+
</tr>
|
76
|
+
</thead>
|
77
|
+
<tbody>
|
78
|
+
<template x-for="(change, index) in summary.changes || []" :key="index">
|
79
|
+
<tr class="hover:bg-gray-50">
|
80
|
+
<td class="text-center sticky left-0 bg-white z-10">
|
81
|
+
<span :class="getOperationClass(change.operation)" class="inline-block w-2 h-2 rounded-full"></span>
|
82
|
+
</td>
|
83
|
+
<td class="sticky bg-white z-10" x-text="formatTimestamp(change.timestamp)"></td>
|
84
|
+
<template x-for="[column, visible] in Object.entries(tableColumns[tableName] || {})" :key="column">
|
85
|
+
<td x-show="visible" class="px-2">
|
86
|
+
<span x-text="change.data?.[column] || '--'"></span>
|
87
|
+
<span x-show="hasColumnChanges(change, column)" class="ml-1 text-xs text-orange-600">*</span>
|
88
|
+
</td>
|
89
|
+
</template>
|
90
|
+
</tr>
|
91
|
+
</template>
|
92
|
+
</tbody>
|
93
|
+
</table>
|
94
|
+
</div>
|
95
|
+
</div>
|
96
|
+
</template>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="h-full flex flex-col">
|
2
|
+
<%= render partial: 'session_header', locals: { session: @session } %>
|
3
|
+
|
4
|
+
<!-- Tab Bar with URL-based navigation -->
|
5
|
+
<div class="tab-bar">
|
6
|
+
<%= link_to changes_session_path(@session.id), class: "tab-item" do %>
|
7
|
+
Changes
|
8
|
+
<% end %>
|
9
|
+
<%= link_to summary_session_path(@session.id), class: "tab-item" do %>
|
10
|
+
Summary
|
11
|
+
<% end %>
|
12
|
+
<%= link_to diagrams_session_path(@session.id), class: "tab-item active" do %>
|
13
|
+
Diagrams
|
14
|
+
<% end %>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<!-- Diagrams Content -->
|
18
|
+
<div class="flex-1 overflow-auto">
|
19
|
+
<%= render partial: 'diagrams_tab' %>
|
20
|
+
</div>
|
21
|
+
</div>
|
@@ -59,8 +59,8 @@
|
|
59
59
|
<table class="compact-table w-full">
|
60
60
|
<thead>
|
61
61
|
<tr>
|
62
|
-
<th class="text-left">Session ID</th>
|
63
|
-
<th class="text-left">Name</th>
|
62
|
+
<th class="text-left" style="min-width:180px; max-width:260px; width:18%">Session ID</th>
|
63
|
+
<th class="text-left" style="min-width:160px; max-width:260px; width:22%">Name</th>
|
64
64
|
<th class="text-center">Status</th>
|
65
65
|
<th class="text-center">Changes</th>
|
66
66
|
<th class="text-right">Started</th>
|
@@ -71,13 +71,16 @@
|
|
71
71
|
<tbody>
|
72
72
|
<% @sessions.each do |session| %>
|
73
73
|
<tr class="hover:bg-blue-50">
|
74
|
-
<td class="font-mono text-xs">
|
75
|
-
|
74
|
+
<td class="font-mono text-xs" style="min-width:180px; max-width:260px; width:18%">
|
75
|
+
<span class="inline-block whitespace-nowrap overflow-x-auto" style="max-width:260px;">
|
76
|
+
<%= safe_value(session, :id) %>
|
77
|
+
</span>
|
76
78
|
</td>
|
77
|
-
<td
|
79
|
+
<td style="min-width:160px; max-width:260px; width:22%" title="<%= safe_value(session, :name) %>">
|
78
80
|
<%= link_to display_session_name(safe_value(session, :name)),
|
79
81
|
session_path(safe_value(session, :id)),
|
80
|
-
class: "text-navy-dark hover:text-blue-medium"
|
82
|
+
class: "text-navy-dark hover:text-blue-medium whitespace-normal break-words inline-block",
|
83
|
+
style: "max-width:260px; overflow-x:auto; display:inline-block;" %>
|
81
84
|
</td>
|
82
85
|
<td class="text-center">
|
83
86
|
<%= render 'dbwatcher/shared/badge',
|
@@ -85,14 +88,15 @@
|
|
85
88
|
badge_class: (session_active?(session) ? 'badge-success' : 'badge-primary') %>
|
86
89
|
</td>
|
87
90
|
<td class="text-center">
|
91
|
+
<% change_count = session_change_count(session) %>
|
88
92
|
<%= render 'dbwatcher/shared/badge',
|
89
|
-
content:
|
90
|
-
badge_class: 'bg-gray-600 text-white' %>
|
93
|
+
content: change_count > 99 ? "#{change_count}" : change_count,
|
94
|
+
badge_class: 'bg-gray-600 text-white whitespace-nowrap' %>
|
91
95
|
</td>
|
92
|
-
<td class="text-right text-xs">
|
96
|
+
<td class="text-right text-xs whitespace-nowrap">
|
93
97
|
<%= format_timestamp(safe_value(session, :started_at)) %>
|
94
98
|
</td>
|
95
|
-
<td class="text-right text-xs">
|
99
|
+
<td class="text-right text-xs whitespace-nowrap">
|
96
100
|
<% if session_active?(session) %>
|
97
101
|
<span class="text-blue-600">Active</span>
|
98
102
|
<% else %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<div class="h-full flex flex-col" x-data="sessionNavigation('<%= @session.id %>')">
|
2
|
+
<%= render 'dbwatcher/sessions/shared/session_header', session: @session %>
|
3
|
+
<%= render 'dbwatcher/sessions/shared/navigation' %>
|
4
|
+
|
5
|
+
<div class="flex-1 overflow-auto">
|
6
|
+
<%= yield %>
|
7
|
+
</div>
|
8
|
+
</div>
|
@@ -0,0 +1,35 @@
|
|
1
|
+
<nav class="bg-white border-b border-gray-200" x-data="sessionNavigation('<%= @session.id %>')">
|
2
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
3
|
+
<div class="flex justify-between h-16">
|
4
|
+
<div class="flex">
|
5
|
+
<div class="flex-shrink-0 flex items-center">
|
6
|
+
<h1 class="text-xl font-semibold text-gray-900">
|
7
|
+
Session <%= @session.id %>
|
8
|
+
</h1>
|
9
|
+
</div>
|
10
|
+
<div class="ml-6 flex space-x-8">
|
11
|
+
<a href="<%= changes_session_path(@session.id) %>"
|
12
|
+
@click.prevent="$store.session.setView('changes')"
|
13
|
+
:class="$store.session.currentView === 'changes' ? 'border-blue-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
14
|
+
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium transition-colors">
|
15
|
+
Changes
|
16
|
+
</a>
|
17
|
+
|
18
|
+
<a href="<%= summary_session_path(@session.id) %>"
|
19
|
+
@click.prevent="$store.session.setView('summary')"
|
20
|
+
:class="$store.session.currentView === 'summary' ? 'border-blue-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
21
|
+
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium transition-colors">
|
22
|
+
Summary
|
23
|
+
</a>
|
24
|
+
|
25
|
+
<a href="<%= diagrams_session_path(@session.id) %>"
|
26
|
+
@click.prevent="$store.session.setView('diagrams')"
|
27
|
+
:class="$store.session.currentView === 'diagrams' ? 'border-blue-500 text-gray-900' : 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'"
|
28
|
+
class="inline-flex items-center px-1 pt-1 border-b-2 text-sm font-medium transition-colors">
|
29
|
+
Diagrams
|
30
|
+
</a>
|
31
|
+
</div>
|
32
|
+
</div>
|
33
|
+
</div>
|
34
|
+
</div>
|
35
|
+
</nav>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<div class="bg-white border-b border-gray-200 px-4 py-3">
|
2
|
+
<div class="flex items-center justify-between">
|
3
|
+
<div class="flex items-center space-x-4">
|
4
|
+
<h1 class="text-xl font-semibold text-gray-900">
|
5
|
+
Session <%= display_session_id(session.id) %>
|
6
|
+
</h1>
|
7
|
+
<div class="text-sm text-gray-500">
|
8
|
+
<% if session.started_at %>
|
9
|
+
<% started_time = session.started_at.is_a?(String) ? session.started_at : session.started_at.strftime('%Y-%m-%d %H:%M:%S') %>
|
10
|
+
<span>Started: <%= started_time %></span>
|
11
|
+
<% else %>
|
12
|
+
<span>Started: Unknown</span>
|
13
|
+
<% end %>
|
14
|
+
<% if session.ended_at %>
|
15
|
+
<% ended_time = session.ended_at.is_a?(String) ? session.ended_at : session.ended_at.strftime('%Y-%m-%d %H:%M:%S') %>
|
16
|
+
<span class="ml-4">Ended: <%= ended_time %></span>
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
</div>
|
20
|
+
|
21
|
+
<div class="flex items-center space-x-3">
|
22
|
+
<%= link_to 'All Sessions', sessions_path, class: 'text-sm text-blue-600 hover:text-blue-800' %>
|
23
|
+
</div>
|
24
|
+
</div>
|
25
|
+
</div>
|
@@ -1,347 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
<
|
4
|
-
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
5
|
-
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
|
6
|
-
</svg>
|
7
|
-
<h1 class="text-sm font-medium truncate"><%= @session.name %></h1>
|
8
|
-
<span class="ml-auto text-xs text-blue-light">
|
9
|
-
<%= Time.parse(@session.started_at.to_s).strftime("%H:%M:%S") rescue @session.started_at %> -
|
10
|
-
<%= Time.parse(@session.ended_at.to_s).strftime("%H:%M:%S") rescue @session.ended_at %>
|
11
|
-
</span>
|
12
|
-
</div>
|
13
|
-
|
14
|
-
<!-- Tab Bar -->
|
15
|
-
<div class="tab-bar">
|
16
|
-
<div class="tab-item" :class="{ active: activeTab === 'changes' }" @click="activeTab = 'changes'">Changes</div>
|
17
|
-
<div class="tab-item" :class="{ active: activeTab === 'summary' }" @click="activeTab = 'summary'">Summary</div>
|
18
|
-
</div>
|
19
|
-
|
20
|
-
<!-- Compact Toolbar -->
|
21
|
-
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1 flex items-center gap-3">
|
22
|
-
|
23
|
-
<div class="ml-auto flex items-center gap-2">
|
24
|
-
<button @click="exportData()"
|
25
|
-
class="compact-button bg-white border border-gray-300 hover:bg-gray-50">
|
26
|
-
<svg class="w-3 h-3 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
27
|
-
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
28
|
-
</svg>
|
29
|
-
Export
|
30
|
-
</button>
|
31
|
-
</div>
|
32
|
-
</div>
|
33
|
-
|
34
|
-
|
35
|
-
<!-- Content -->
|
36
|
-
<div class="flex-1 overflow-auto">
|
37
|
-
<!-- Summary Tab -->
|
38
|
-
<div x-show="activeTab === 'summary'" class="p-4">
|
39
|
-
<div class="grid grid-cols-3 gap-3 mb-4">
|
40
|
-
<% @tables_summary.each do |table_name, summary| %>
|
41
|
-
<div class="border border-gray-300 rounded p-3 hover:shadow-md cursor-pointer bg-white"
|
42
|
-
@click="activeTab = 'changes'; scrollToTable('<%= table_name %>')">
|
43
|
-
<h4 class="text-sm font-medium text-navy-dark mb-2"><%= table_name %></h4>
|
44
|
-
<div class="space-y-1 text-xs">
|
45
|
-
<div class="flex justify-between">
|
46
|
-
<span class="badge badge-insert">INSERT</span>
|
47
|
-
<span><%= summary[:operations]['INSERT'] %></span>
|
48
|
-
</div>
|
49
|
-
<div class="flex justify-between">
|
50
|
-
<span class="badge badge-update">UPDATE</span>
|
51
|
-
<span><%= summary[:operations]['UPDATE'] %></span>
|
52
|
-
</div>
|
53
|
-
<div class="flex justify-between">
|
54
|
-
<span class="badge badge-delete">DELETE</span>
|
55
|
-
<span><%= summary[:operations]['DELETE'] %></span>
|
56
|
-
</div>
|
57
|
-
</div>
|
58
|
-
</div>
|
59
|
-
<% end %>
|
60
|
-
</div>
|
61
|
-
</div>
|
62
|
-
|
63
|
-
<!-- Changes Tab -->
|
64
|
-
<div x-show="activeTab === 'changes'" class="h-full">
|
65
|
-
<% @tables_summary.each do |table_name, summary| %>
|
66
|
-
<div class="border-b border-gray-300" x-data="{ expanded: true }">
|
67
|
-
<!-- Table Header with Column Controls -->
|
68
|
-
<div class="bg-gray-100 px-3 py-2 flex items-center cursor-pointer"
|
69
|
-
@click="expanded = !expanded"
|
70
|
-
:id="`table-${table_name}`">
|
71
|
-
<svg class="w-3 h-3 mr-2 transition-transform"
|
72
|
-
:class="{ 'rotate-90': expanded }"
|
73
|
-
fill="currentColor" viewBox="0 0 20 20">
|
74
|
-
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
75
|
-
</svg>
|
76
|
-
<h3 class="text-sm font-medium text-gray-900 flex-1"><%= table_name %></h3>
|
77
|
-
<div class="flex gap-2 mr-4">
|
78
|
-
<% summary[:operations].each do |op, count| %>
|
79
|
-
<% next if count == 0 %>
|
80
|
-
<span class="badge badge-<%= op.downcase %>"><%= count %></span>
|
81
|
-
<% end %>
|
82
|
-
</div>
|
83
|
-
<!-- Column Visibility Button -->
|
84
|
-
<button @click.stop="toggleColumnSelector('<%= table_name %>')"
|
85
|
-
class="text-xs bg-white border border-gray-300 px-2 py-1 rounded hover:bg-gray-50 relative">
|
86
|
-
Columns
|
87
|
-
</button>
|
88
|
-
</div>
|
89
|
-
|
90
|
-
<!-- Column Selector Dropdown -->
|
91
|
-
<div x-show="showColumnSelector === '<%= table_name %>'"
|
92
|
-
x-transition
|
93
|
-
@click.away="showColumnSelector = null"
|
94
|
-
class="absolute z-50 bg-white border border-gray-300 rounded shadow-lg p-3 max-h-64 overflow-auto"
|
95
|
-
style="right: 1rem; margin-top: -2px;">
|
96
|
-
<div class="text-xs font-medium mb-2">Select Visible Columns:</div>
|
97
|
-
<div class="space-y-1 min-w-48">
|
98
|
-
<% if summary[:sample_record] %>
|
99
|
-
<% summary[:sample_record].keys.each do |column| %>
|
100
|
-
<label class="flex items-center text-xs hover:bg-gray-50 p-1 rounded">
|
101
|
-
<input type="checkbox"
|
102
|
-
x-model="tableColumns['<%= table_name %>']['<%= column %>']"
|
103
|
-
class="mr-2">
|
104
|
-
<span class="flex-1"><%= column.to_s.humanize %></span>
|
105
|
-
</label>
|
106
|
-
<% end %>
|
107
|
-
<% end %>
|
108
|
-
</div>
|
109
|
-
<div class="mt-2 pt-2 border-t border-gray-200 flex gap-1">
|
110
|
-
<button @click="selectAllColumns('<%= table_name %>')"
|
111
|
-
class="text-xs bg-blue-600 text-white px-2 py-1 rounded hover:bg-blue-700">All</button>
|
112
|
-
<button @click="selectNoneColumns('<%= table_name %>')"
|
113
|
-
class="text-xs bg-gray-600 text-white px-2 py-1 rounded hover:bg-gray-700">None</button>
|
114
|
-
</div>
|
115
|
-
</div>
|
116
|
-
|
117
|
-
<!-- Table Content with Horizontal Scroll -->
|
118
|
-
<div x-show="expanded" x-collapse class="overflow-auto">
|
119
|
-
<div class="min-w-full">
|
120
|
-
<table class="compact-table w-full">
|
121
|
-
<thead>
|
122
|
-
<tr class="sticky top-0 bg-gray-100 z-10">
|
123
|
-
<th class="text-center w-16 sticky left-0 bg-gray-100 z-20 border-r border-gray-300">
|
124
|
-
<span class="text-xs">Op</span>
|
125
|
-
</th>
|
126
|
-
<th class="text-left w-24 sticky bg-gray-100 z-20 border-r border-gray-300">Time</th>
|
127
|
-
<% if summary[:sample_record] %>
|
128
|
-
<% summary[:sample_record].keys.each do |column| %>
|
129
|
-
<th class="text-left min-w-32 px-2"
|
130
|
-
x-show="tableColumns['<%= table_name %>']['<%= column %>']">
|
131
|
-
<%= column.to_s.humanize %>
|
132
|
-
</th>
|
133
|
-
<% end %>
|
134
|
-
<% end %>
|
135
|
-
</tr>
|
136
|
-
</thead>
|
137
|
-
<tbody>
|
138
|
-
<% summary[:changes].each_with_index do |change, index| %>
|
139
|
-
<% row_id = "#{table_name}_row_#{index}" %>
|
140
|
-
<% operation = change['operation'] || change[:operation] %>
|
141
|
-
<% timestamp = change['timestamp'] || change[:timestamp] %>
|
142
|
-
<% snapshot = change['record_snapshot'] || change[:record_snapshot] || {} %>
|
143
|
-
<% column_changes = change['changes'] || change[:changes] %>
|
144
|
-
|
145
|
-
<tbody x-data="{ expanded: false }">
|
146
|
-
<tr class="hover:bg-blue-50">
|
147
|
-
<td class="text-center sticky left-0 bg-white z-10 border-r border-gray-200 w-16">
|
148
|
-
<div class="flex items-center justify-center gap-1">
|
149
|
-
<!-- Expand/Collapse Button -->
|
150
|
-
<button @click="expanded = !expanded"
|
151
|
-
class="text-gray-400 hover:text-gray-600 transition-colors p-1 rounded hover:bg-gray-100">
|
152
|
-
<svg class="w-3 h-3 transition-transform"
|
153
|
-
:class="{ 'rotate-90': expanded }"
|
154
|
-
fill="currentColor" viewBox="0 0 20 20">
|
155
|
-
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
156
|
-
</svg>
|
157
|
-
</button>
|
158
|
-
<!-- Operation Badge -->
|
159
|
-
<% if operation %>
|
160
|
-
<span class="badge badge-<%= operation.downcase %>">
|
161
|
-
<%= operation[0] %>
|
162
|
-
</span>
|
163
|
-
<% else %>
|
164
|
-
<span class="badge">?</span>
|
165
|
-
<% end %>
|
166
|
-
</div>
|
167
|
-
</td>
|
168
|
-
<td class="text-xs text-gray-600 sticky bg-white z-10 border-r border-gray-200 w-24">
|
169
|
-
<% if timestamp %>
|
170
|
-
<%= Time.parse(timestamp.to_s).strftime("%H:%M:%S") rescue timestamp %>
|
171
|
-
<% else %>
|
172
|
-
<span class="text-gray-400">--</span>
|
173
|
-
<% end %>
|
174
|
-
</td>
|
175
|
-
|
176
|
-
<% if snapshot %>
|
177
|
-
<% snapshot.each do |key, value| %>
|
178
|
-
<td x-show="tableColumns['<%= table_name %>']['<%= key %>']"
|
179
|
-
class="relative px-2"
|
180
|
-
@click.stop="">
|
181
|
-
<% changed_column = column_changes&.find { |c| (c['column'] || c[:column]) == key.to_s } %>
|
182
|
-
<div class="min-w-32 max-w-48 truncate">
|
183
|
-
<% if operation == 'UPDATE' && changed_column %>
|
184
|
-
<div class="text-xs">
|
185
|
-
<div class="text-red-600 line-through">
|
186
|
-
<%= truncate_cell_value(changed_column['old_value'] || changed_column[:old_value]) %>
|
187
|
-
</div>
|
188
|
-
<div class="text-green-600 font-medium">
|
189
|
-
<%= truncate_cell_value(changed_column['new_value'] || changed_column[:new_value]) %>
|
190
|
-
</div>
|
191
|
-
</div>
|
192
|
-
<% elsif operation == 'INSERT' %>
|
193
|
-
<span class="text-green-600 font-medium">
|
194
|
-
<%= truncate_cell_value(value) %>
|
195
|
-
</span>
|
196
|
-
<% elsif operation == 'DELETE' %>
|
197
|
-
<span class="text-red-600 line-through">
|
198
|
-
<%= truncate_cell_value(value) %>
|
199
|
-
</span>
|
200
|
-
<% else %>
|
201
|
-
<span class="text-gray-700">
|
202
|
-
<%= truncate_cell_value(value) %>
|
203
|
-
</span>
|
204
|
-
<% end %>
|
205
|
-
</div>
|
206
|
-
</td>
|
207
|
-
<% end %>
|
208
|
-
<% end %>
|
209
|
-
</tr>
|
210
|
-
|
211
|
-
<!-- Expanded Row Content -->
|
212
|
-
<tr x-show="expanded"
|
213
|
-
x-collapse
|
214
|
-
class="bg-gray-50 border-t border-gray-200">
|
215
|
-
<td colspan="2" class="sticky left-0 bg-gray-50 z-10 border-r border-gray-200 p-3 w-40">
|
216
|
-
<div class="text-xs font-medium text-gray-700 mb-2">Record Details</div>
|
217
|
-
<div class="text-xs text-gray-600">
|
218
|
-
Operation: <span class="badge badge-<%= operation&.downcase %>"><%= operation %></span>
|
219
|
-
<% if timestamp %>
|
220
|
-
<br>Time: <%= Time.parse(timestamp.to_s).strftime("%H:%M:%S.%L") rescue timestamp %>
|
221
|
-
<% end %>
|
222
|
-
</div>
|
223
|
-
</td>
|
224
|
-
<% if snapshot %>
|
225
|
-
<% snapshot.each do |key, value| %>
|
226
|
-
<td x-show="tableColumns['<%= table_name %>']['<%= key %>']"
|
227
|
-
class="p-3 border-r border-gray-200 align-top">
|
228
|
-
<% changed_column = column_changes&.find { |c| (c['column'] || c[:column]) == key.to_s } %>
|
229
|
-
<div class="text-xs font-medium text-gray-600 mb-1"><%= key.to_s.humanize %></div>
|
230
|
-
<div class="max-w-md">
|
231
|
-
<% if operation == 'UPDATE' && changed_column %>
|
232
|
-
<div class="space-y-2">
|
233
|
-
<div class="text-red-600 bg-red-50 p-2 rounded">
|
234
|
-
<div class="text-xs font-medium mb-1">Old Value:</div>
|
235
|
-
<pre class="text-xs whitespace-pre-wrap"><%= format_cell_value(changed_column['old_value'] || changed_column[:old_value]) %></pre>
|
236
|
-
</div>
|
237
|
-
<div class="text-green-600 bg-green-50 p-2 rounded">
|
238
|
-
<div class="text-xs font-medium mb-1">New Value:</div>
|
239
|
-
<pre class="text-xs whitespace-pre-wrap"><%= format_cell_value(changed_column['new_value'] || changed_column[:new_value]) %></pre>
|
240
|
-
</div>
|
241
|
-
</div>
|
242
|
-
<% elsif operation == 'INSERT' %>
|
243
|
-
<div class="text-green-600 bg-green-50 p-2 rounded">
|
244
|
-
<pre class="text-xs whitespace-pre-wrap"><%= format_cell_value(value) %></pre>
|
245
|
-
</div>
|
246
|
-
<% elsif operation == 'DELETE' %>
|
247
|
-
<div class="text-red-600 bg-red-50 p-2 rounded">
|
248
|
-
<pre class="text-xs whitespace-pre-wrap"><%= format_cell_value(value) %></pre>
|
249
|
-
</div>
|
250
|
-
<% else %>
|
251
|
-
<div class="text-gray-700 bg-gray-100 p-2 rounded">
|
252
|
-
<pre class="text-xs whitespace-pre-wrap"><%= format_cell_value(value) %></pre>
|
253
|
-
</div>
|
254
|
-
<% end %>
|
255
|
-
</div>
|
256
|
-
</td>
|
257
|
-
<% end %>
|
258
|
-
<% end %>
|
259
|
-
</tr>
|
260
|
-
</tbody>
|
261
|
-
<% end %>
|
262
|
-
</tbody>
|
263
|
-
</table>
|
264
|
-
</div>
|
265
|
-
</div>
|
266
|
-
</div>
|
267
|
-
<% end %>
|
268
|
-
</div>
|
269
|
-
</div>
|
1
|
+
<!-- This view is only used for redirects now -->
|
2
|
+
<div>
|
3
|
+
<p>Redirecting to changes...</p>
|
270
4
|
</div>
|
271
|
-
|
272
|
-
<script>
|
273
|
-
function sessionView() {
|
274
|
-
return {
|
275
|
-
activeTab: 'changes',
|
276
|
-
showColumnSelector: null,
|
277
|
-
tableColumns: {},
|
278
|
-
|
279
|
-
init() {
|
280
|
-
// Initialize column visibility for each table
|
281
|
-
<% @tables_summary.each do |table_name, summary| %>
|
282
|
-
this.tableColumns['<%= table_name %>'] = {};
|
283
|
-
<% if summary[:sample_record] %>
|
284
|
-
<% summary[:sample_record].keys.each do |column| %>
|
285
|
-
this.tableColumns['<%= table_name %>']['<%= column %>'] = true;
|
286
|
-
<% end %>
|
287
|
-
<% end %>
|
288
|
-
<% end %>
|
289
|
-
},
|
290
|
-
|
291
|
-
scrollToTable(tableName) {
|
292
|
-
this.activeTab = 'changes';
|
293
|
-
setTimeout(() => {
|
294
|
-
const element = document.getElementById(`table-${tableName}`);
|
295
|
-
if (element) {
|
296
|
-
element.scrollIntoView({ behavior: 'smooth' });
|
297
|
-
}
|
298
|
-
}, 100);
|
299
|
-
},
|
300
|
-
|
301
|
-
toggleColumnSelector(tableName) {
|
302
|
-
this.showColumnSelector = this.showColumnSelector === tableName ? null : tableName;
|
303
|
-
},
|
304
|
-
|
305
|
-
selectAllColumns(tableName) {
|
306
|
-
Object.keys(this.tableColumns[tableName]).forEach(column => {
|
307
|
-
this.tableColumns[tableName][column] = true;
|
308
|
-
});
|
309
|
-
},
|
310
|
-
|
311
|
-
selectNoneColumns(tableName) {
|
312
|
-
Object.keys(this.tableColumns[tableName]).forEach(column => {
|
313
|
-
this.tableColumns[tableName][column] = false;
|
314
|
-
});
|
315
|
-
},
|
316
|
-
|
317
|
-
exportData() {
|
318
|
-
const data = [];
|
319
|
-
<% @tables_summary.each do |table_name, summary| %>
|
320
|
-
<% summary[:changes].each do |change| %>
|
321
|
-
data.push({
|
322
|
-
table: '<%= table_name %>',
|
323
|
-
operation: '<%= change['operation'] || change[:operation] || 'UNKNOWN' %>',
|
324
|
-
timestamp: '<%= change['timestamp'] || change[:timestamp] %>',
|
325
|
-
record_id: '<%= change['record_id'] || change[:record_id] %>',
|
326
|
-
changes: '<%= change['changes'].to_json if change['changes'] %>'
|
327
|
-
});
|
328
|
-
<% end %>
|
329
|
-
<% end %>
|
330
|
-
|
331
|
-
const csv = [
|
332
|
-
['Table', 'Operation', 'Timestamp', 'Record ID', 'Changes'],
|
333
|
-
...data.map(row => [row.table, row.operation, row.timestamp, row.record_id, row.changes])
|
334
|
-
].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
|
335
|
-
|
336
|
-
const blob = new Blob([csv], { type: 'text/csv' });
|
337
|
-
const url = URL.createObjectURL(blob);
|
338
|
-
const a = document.createElement('a');
|
339
|
-
a.href = url;
|
340
|
-
a.download = `session-<%= @session.id %>-changes.csv`;
|
341
|
-
a.click();
|
342
|
-
URL.revokeObjectURL(url);
|
343
|
-
}
|
344
|
-
}
|
345
|
-
}
|
346
|
-
</script>
|
347
|
-
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="h-full flex flex-col">
|
2
|
+
<%= render partial: 'session_header', locals: { session: @session } %>
|
3
|
+
|
4
|
+
<!-- Tab Bar with URL-based navigation -->
|
5
|
+
<div class="tab-bar">
|
6
|
+
<%= link_to changes_session_path(@session.id), class: "tab-item" do %>
|
7
|
+
Changes
|
8
|
+
<% end %>
|
9
|
+
<%= link_to summary_session_path(@session.id), class: "tab-item active" do %>
|
10
|
+
Summary
|
11
|
+
<% end %>
|
12
|
+
<%= link_to diagrams_session_path(@session.id), class: "tab-item" do %>
|
13
|
+
Diagrams
|
14
|
+
<% end %>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<!-- Summary Content -->
|
18
|
+
<div class="flex-1 overflow-auto">
|
19
|
+
<%= render partial: 'summary_tab', locals: { tables_summary: @tables_summary } %>
|
20
|
+
</div>
|
21
|
+
</div>
|