dbwatcher 0.1.5 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +101 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +20 -0
- data/app/controllers/dbwatcher/queries_controller.rb +24 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +30 -20
- data/app/controllers/dbwatcher/tables_controller.rb +38 -0
- data/app/helpers/dbwatcher/application_helper.rb +103 -0
- data/app/helpers/dbwatcher/component_helper.rb +29 -0
- data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
- data/app/helpers/dbwatcher/formatting_helper.rb +108 -0
- data/app/helpers/dbwatcher/session_helper.rb +28 -0
- data/app/views/dbwatcher/dashboard/index.html.erb +177 -0
- data/app/views/dbwatcher/queries/index.html.erb +240 -0
- 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 +124 -27
- 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 -149
- data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
- data/app/views/dbwatcher/shared/_badge.html.erb +4 -0
- data/app/views/dbwatcher/shared/_data_table.html.erb +20 -0
- data/app/views/dbwatcher/shared/_header.html.erb +7 -0
- data/app/views/dbwatcher/shared/_page_layout.html.erb +20 -0
- data/app/views/dbwatcher/shared/_section_panel.html.erb +9 -0
- data/app/views/dbwatcher/shared/_stats_card.html.erb +11 -0
- data/app/views/dbwatcher/shared/_tab_bar.html.erb +6 -0
- data/app/views/dbwatcher/tables/changes.html.erb +225 -0
- data/app/views/dbwatcher/tables/index.html.erb +123 -0
- data/app/views/dbwatcher/tables/show.html.erb +86 -0
- data/app/views/layouts/dbwatcher/application.html.erb +252 -25
- data/bin/compile_scss +49 -0
- data/config/routes.rb +43 -3
- data/lib/dbwatcher/configuration.rb +103 -1
- data/lib/dbwatcher/engine.rb +28 -13
- data/lib/dbwatcher/logging.rb +72 -0
- 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/dashboard_data_aggregator.rb +121 -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 +564 -0
- data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
- data/lib/dbwatcher/services/diagram_data/dataset.rb +278 -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 +136 -0
- data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +46 -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 +102 -0
- data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
- data/lib/dbwatcher/services/query_filter_processor.rb +114 -0
- data/lib/dbwatcher/services/table_statistics_collector.rb +119 -0
- data/lib/dbwatcher/sql_logger.rb +107 -0
- data/lib/dbwatcher/storage/api/base_api.rb +134 -0
- data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +59 -0
- data/lib/dbwatcher/storage/api/query_api.rb +95 -0
- data/lib/dbwatcher/storage/api/session_api.rb +181 -0
- data/lib/dbwatcher/storage/api/table_api.rb +86 -0
- data/lib/dbwatcher/storage/base_storage.rb +120 -0
- data/lib/dbwatcher/storage/change_processor.rb +65 -0
- data/lib/dbwatcher/storage/concerns/data_normalizer.rb +134 -0
- data/lib/dbwatcher/storage/concerns/error_handler.rb +75 -0
- data/lib/dbwatcher/storage/concerns/timestampable.rb +74 -0
- data/lib/dbwatcher/storage/concerns/validatable.rb +117 -0
- data/lib/dbwatcher/storage/date_helper.rb +21 -0
- data/lib/dbwatcher/storage/errors.rb +86 -0
- data/lib/dbwatcher/storage/file_manager.rb +122 -0
- data/lib/dbwatcher/storage/null_session.rb +39 -0
- data/lib/dbwatcher/storage/query_storage.rb +338 -0
- data/lib/dbwatcher/storage/query_validator.rb +24 -0
- data/lib/dbwatcher/storage/session.rb +58 -0
- data/lib/dbwatcher/storage/session_operations.rb +37 -0
- data/lib/dbwatcher/storage/session_query.rb +71 -0
- data/lib/dbwatcher/storage/session_storage.rb +322 -0
- data/lib/dbwatcher/storage/table_storage.rb +237 -0
- data/lib/dbwatcher/storage.rb +112 -85
- data/lib/dbwatcher/tracker.rb +4 -55
- data/lib/dbwatcher/version.rb +1 -1
- data/lib/dbwatcher.rb +70 -3
- metadata +140 -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>
|
@@ -1,37 +1,134 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
<%# Sessions Index Page %>
|
2
|
+
<div class="h-full flex flex-col">
|
3
|
+
<%= render 'dbwatcher/shared/header', title: 'Tracking Sessions', subtitle: "#{@sessions.count} sessions" %>
|
4
|
+
|
5
|
+
<%= render 'dbwatcher/shared/tab_bar', tabs: [
|
6
|
+
{ name: 'All Sessions', active: true },
|
7
|
+
{ name: 'Active', active: false },
|
8
|
+
{ name: 'Recent', active: false }
|
9
|
+
] %>
|
10
|
+
|
11
|
+
<!-- Toolbar -->
|
12
|
+
<div class="h-8 bg-gray-100 border-b border-gray-300 flex items-center px-4 gap-2">
|
13
|
+
<input type="text" placeholder="Filter sessions..."
|
14
|
+
class="compact-input flex-1 max-w-xs">
|
15
|
+
<select class="compact-select">
|
16
|
+
<option>Last 24 hours</option>
|
17
|
+
<option>Last week</option>
|
18
|
+
<option>All time</option>
|
19
|
+
</select>
|
20
|
+
|
21
|
+
<div class="ml-auto flex items-center gap-2">
|
22
|
+
<button class="compact-button bg-blue-medium text-white hover:bg-blue-700">
|
23
|
+
<svg class="w-3 h-3 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
24
|
+
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/>
|
25
|
+
</svg>
|
26
|
+
Refresh
|
27
|
+
</button>
|
28
|
+
|
29
|
+
<%= button_to clear_sessions_path,
|
30
|
+
method: :delete,
|
31
|
+
class: "compact-button bg-red-600 text-white hover:bg-red-700",
|
32
|
+
data: {
|
33
|
+
confirm: "Are you sure you want to clear all sessions? This action cannot be undone."
|
34
|
+
} do %>
|
35
|
+
<svg class="w-3 h-3 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
36
|
+
<path fill-rule="evenodd" d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/>
|
37
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 012 0v4a1 1 0 11-2 0V7zm5-1a1 1 0 00-1 1v4a1 1 0 102 0V7a1 1 0 00-1-1z"/>
|
38
|
+
</svg>
|
39
|
+
Clear Sessions
|
40
|
+
<% end %>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
|
44
|
+
<!-- Content Area -->
|
45
|
+
<div class="flex-1 overflow-auto p-4">
|
3
46
|
|
4
47
|
<% if @sessions.empty? %>
|
5
|
-
<div class="
|
6
|
-
|
48
|
+
<div class="flex items-center justify-center h-full text-gray-500">
|
49
|
+
<div class="text-center">
|
50
|
+
<svg class="mx-auto h-8 w-8 text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
|
52
|
+
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
|
53
|
+
</svg>
|
54
|
+
<p class="text-xs">No tracking sessions yet</p>
|
55
|
+
<p class="text-xs text-gray-400">Start tracking with <code class="bg-gray-200 px-1 rounded">Dbwatcher.track { ... }</code></p>
|
56
|
+
</div>
|
7
57
|
</div>
|
8
58
|
<% else %>
|
9
|
-
<
|
10
|
-
<
|
59
|
+
<table class="compact-table w-full">
|
60
|
+
<thead>
|
61
|
+
<tr>
|
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
|
+
<th class="text-center">Status</th>
|
65
|
+
<th class="text-center">Changes</th>
|
66
|
+
<th class="text-right">Started</th>
|
67
|
+
<th class="text-right">Duration</th>
|
68
|
+
<th class="text-center">Actions</th>
|
69
|
+
</tr>
|
70
|
+
</thead>
|
71
|
+
<tbody>
|
11
72
|
<% @sessions.each do |session| %>
|
12
|
-
<
|
13
|
-
|
14
|
-
<
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
73
|
+
<tr class="hover:bg-blue-50">
|
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>
|
78
|
+
</td>
|
79
|
+
<td style="min-width:160px; max-width:260px; width:22%" title="<%= safe_value(session, :name) %>">
|
80
|
+
<%= link_to display_session_name(safe_value(session, :name)),
|
81
|
+
session_path(safe_value(session, :id)),
|
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;" %>
|
84
|
+
</td>
|
85
|
+
<td class="text-center">
|
86
|
+
<%= render 'dbwatcher/shared/badge',
|
87
|
+
content: (session_active?(session) ? 'Active' : 'Completed'),
|
88
|
+
badge_class: (session_active?(session) ? 'badge-success' : 'badge-primary') %>
|
89
|
+
</td>
|
90
|
+
<td class="text-center">
|
91
|
+
<% change_count = session_change_count(session) %>
|
92
|
+
<%= render 'dbwatcher/shared/badge',
|
93
|
+
content: change_count > 99 ? "#{change_count}" : change_count,
|
94
|
+
badge_class: 'bg-gray-600 text-white whitespace-nowrap' %>
|
95
|
+
</td>
|
96
|
+
<td class="text-right text-xs whitespace-nowrap">
|
97
|
+
<%= format_timestamp(safe_value(session, :started_at)) %>
|
98
|
+
</td>
|
99
|
+
<td class="text-right text-xs whitespace-nowrap">
|
100
|
+
<% if session_active?(session) %>
|
101
|
+
<span class="text-blue-600">Active</span>
|
102
|
+
<% else %>
|
103
|
+
<%= distance_of_time_in_words(
|
104
|
+
Time.parse(safe_value(session, :started_at)),
|
105
|
+
Time.parse(safe_value(session, :ended_at))
|
106
|
+
) rescue 'N/A' %>
|
107
|
+
<% end %>
|
108
|
+
</td>
|
109
|
+
<td class="text-center">
|
110
|
+
<div class="flex gap-1 justify-end">
|
111
|
+
<%= link_to session_path(safe_value(session, :id)),
|
112
|
+
class: "compact-button bg-navy-dark text-white hover:bg-blue-medium" do %>
|
113
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
114
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
115
|
+
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
116
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
117
|
+
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
28
118
|
</svg>
|
29
|
-
|
119
|
+
<% end %>
|
30
120
|
</div>
|
31
|
-
|
32
|
-
</
|
121
|
+
</td>
|
122
|
+
</tr>
|
33
123
|
<% end %>
|
34
|
-
</
|
35
|
-
</
|
124
|
+
</tbody>
|
125
|
+
</table>
|
36
126
|
<% end %>
|
127
|
+
|
128
|
+
<!-- Status Bar -->
|
129
|
+
<div class="h-6 bg-gray-100 border-t border-gray-300 flex items-center px-4 text-xs text-gray-600">
|
130
|
+
<%= @sessions.count %> sessions total •
|
131
|
+
<%= @sessions.count { |s| session_active?(s) } %> active •
|
132
|
+
Last updated: <%= Time.current.strftime("%H:%M:%S") %>
|
133
|
+
</div>
|
37
134
|
</div>
|
@@ -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,150 +1,4 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
<button @click="showFullRecords = !showFullRecords"
|
5
|
-
class="bg-gray-100 hover:bg-gray-200 px-4 py-2 rounded flex items-center gap-2">
|
6
|
-
<span x-text="showFullRecords ? 'Show Changes Only' : 'Show Full Records'"></span>
|
7
|
-
</button>
|
8
|
-
</div>
|
9
|
-
|
10
|
-
<!-- Summary -->
|
11
|
-
<div class="bg-white shadow rounded-lg p-6">
|
12
|
-
<h3 class="text-lg font-semibold mb-4">Summary</h3>
|
13
|
-
<div class="grid grid-cols-3 gap-4">
|
14
|
-
<template x-for="[key, count] in Object.entries(session.summary)" :key="key">
|
15
|
-
<div class="text-sm">
|
16
|
-
<span x-text="key.split(',')[0]" class="font-medium"></span>
|
17
|
-
<span x-text="key.split(',')[1]" :class="getOperationColor(key.split(',')[1])"></span>:
|
18
|
-
<span x-text="count"></span>
|
19
|
-
</div>
|
20
|
-
</template>
|
21
|
-
</div>
|
22
|
-
</div>
|
23
|
-
|
24
|
-
<!-- Changes by Table -->
|
25
|
-
<div class="bg-white shadow rounded-lg p-6">
|
26
|
-
<h3 class="text-lg font-semibold mb-4">Database Changes</h3>
|
27
|
-
|
28
|
-
<div class="space-y-2">
|
29
|
-
<template x-for="table in groupedChanges" :key="table.name">
|
30
|
-
<div class="border rounded">
|
31
|
-
<button @click="table.expanded = !table.expanded"
|
32
|
-
class="w-full px-4 py-3 flex items-center justify-between hover:bg-gray-50">
|
33
|
-
<div class="flex items-center gap-2">
|
34
|
-
<svg class="w-4 h-4 transition-transform" :class="{'rotate-90': table.expanded}" fill="currentColor" viewBox="0 0 20 20">
|
35
|
-
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
36
|
-
</svg>
|
37
|
-
<span class="font-medium" x-text="table.name"></span>
|
38
|
-
<span class="text-sm text-gray-500" x-text="`(${table.changes.length} changes)`"></span>
|
39
|
-
</div>
|
40
|
-
</button>
|
41
|
-
|
42
|
-
<div x-show="table.expanded" x-collapse class="border-t">
|
43
|
-
<template x-for="record in table.records" :key="record.id">
|
44
|
-
<div class="border-b last:border-0">
|
45
|
-
<button @click="record.expanded = !record.expanded"
|
46
|
-
class="w-full px-6 py-2 flex items-center gap-2 hover:bg-gray-50 text-left">
|
47
|
-
<svg class="w-4 h-4 transition-transform" :class="{'rotate-90': record.expanded}" fill="currentColor" viewBox="0 0 20 20">
|
48
|
-
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
|
49
|
-
</svg>
|
50
|
-
<span x-text="`Record #${record.id}`"></span>
|
51
|
-
</button>
|
52
|
-
|
53
|
-
<div x-show="record.expanded" x-collapse class="px-8 pb-4">
|
54
|
-
<template x-for="change in record.changes" :key="change.timestamp">
|
55
|
-
<div class="mt-3">
|
56
|
-
<div class="font-medium mb-2" :class="getOperationColor(change.operation)">
|
57
|
-
<span x-text="change.operation"></span> at
|
58
|
-
<span x-text="new Date(change.timestamp).toLocaleTimeString()"></span>
|
59
|
-
</div>
|
60
|
-
|
61
|
-
<div x-show="!showFullRecords" class="space-y-1">
|
62
|
-
<template x-for="col in change.changes" :key="col.column">
|
63
|
-
<div class="text-sm">
|
64
|
-
<span class="font-medium" x-text="col.column + ':'"></span>
|
65
|
-
<template x-if="col.old_value">
|
66
|
-
<span>
|
67
|
-
<span class="text-red-600 line-through" x-text="col.old_value"></span>
|
68
|
-
<span class="mx-1">→</span>
|
69
|
-
</span>
|
70
|
-
</template>
|
71
|
-
<span class="text-green-600" x-text="col.new_value || 'null'"></span>
|
72
|
-
</div>
|
73
|
-
</template>
|
74
|
-
</div>
|
75
|
-
|
76
|
-
<div x-show="showFullRecords" class="bg-gray-50 p-3 rounded text-sm font-mono">
|
77
|
-
<pre x-text="JSON.stringify(change.record_snapshot, null, 2)"></pre>
|
78
|
-
</div>
|
79
|
-
</div>
|
80
|
-
</template>
|
81
|
-
</div>
|
82
|
-
</div>
|
83
|
-
</template>
|
84
|
-
</div>
|
85
|
-
</div>
|
86
|
-
</template>
|
87
|
-
</div>
|
88
|
-
</div>
|
1
|
+
<!-- This view is only used for redirects now -->
|
2
|
+
<div>
|
3
|
+
<p>Redirecting to changes...</p>
|
89
4
|
</div>
|
90
|
-
|
91
|
-
<script>
|
92
|
-
function sessionViewer(sessionData) {
|
93
|
-
return {
|
94
|
-
session: sessionData,
|
95
|
-
showFullRecords: false,
|
96
|
-
groupedChanges: [],
|
97
|
-
|
98
|
-
init() {
|
99
|
-
// Calculate summary from changes
|
100
|
-
const summary = {};
|
101
|
-
this.session.changes.forEach(change => {
|
102
|
-
const key = `${change.table_name},${change.operation}`;
|
103
|
-
summary[key] = (summary[key] || 0) + 1;
|
104
|
-
});
|
105
|
-
this.session.summary = summary;
|
106
|
-
|
107
|
-
// Group changes by table and record
|
108
|
-
const tables = {};
|
109
|
-
|
110
|
-
this.session.changes.forEach(change => {
|
111
|
-
if (!tables[change.table_name]) {
|
112
|
-
tables[change.table_name] = {
|
113
|
-
name: change.table_name,
|
114
|
-
expanded: false,
|
115
|
-
changes: [],
|
116
|
-
records: {}
|
117
|
-
};
|
118
|
-
}
|
119
|
-
|
120
|
-
tables[change.table_name].changes.push(change);
|
121
|
-
|
122
|
-
if (!tables[change.table_name].records[change.record_id]) {
|
123
|
-
tables[change.table_name].records[change.record_id] = {
|
124
|
-
id: change.record_id,
|
125
|
-
expanded: false,
|
126
|
-
changes: []
|
127
|
-
};
|
128
|
-
}
|
129
|
-
|
130
|
-
tables[change.table_name].records[change.record_id].changes.push(change);
|
131
|
-
});
|
132
|
-
|
133
|
-
// Convert to array format
|
134
|
-
this.groupedChanges = Object.values(tables).map(table => ({
|
135
|
-
...table,
|
136
|
-
records: Object.values(table.records)
|
137
|
-
}));
|
138
|
-
},
|
139
|
-
|
140
|
-
getOperationColor(operation) {
|
141
|
-
const colors = {
|
142
|
-
'INSERT': 'text-green-600',
|
143
|
-
'UPDATE': 'text-blue-600',
|
144
|
-
'DELETE': 'text-red-600'
|
145
|
-
};
|
146
|
-
return colors[operation] || 'text-gray-600';
|
147
|
-
}
|
148
|
-
};
|
149
|
-
}
|
150
|
-
</script>
|
@@ -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>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%# Reusable data table component %>
|
2
|
+
<div class="border border-gray-300 rounded">
|
3
|
+
<div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
|
4
|
+
<h3 class="text-xs font-medium text-gray-700"><%= local_assigns[:title] %></h3>
|
5
|
+
</div>
|
6
|
+
<div class="max-h-64 overflow-auto">
|
7
|
+
<table class="compact-table w-full">
|
8
|
+
<thead>
|
9
|
+
<tr>
|
10
|
+
<% (local_assigns[:headers] || []).each do |header| %>
|
11
|
+
<th class="<%= header[:class] || 'text-left' %>"><%= header[:label] %></th>
|
12
|
+
<% end %>
|
13
|
+
</tr>
|
14
|
+
</thead>
|
15
|
+
<tbody>
|
16
|
+
<%= yield %>
|
17
|
+
</tbody>
|
18
|
+
</table>
|
19
|
+
</div>
|
20
|
+
</div>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<%# Reusable header component for all pages %>
|
2
|
+
<div class="h-10 bg-navy-dark text-white flex items-center px-4">
|
3
|
+
<h1 class="text-sm font-medium"><%= local_assigns[:title] %></h1>
|
4
|
+
<span class="ml-auto text-xs text-blue-light">
|
5
|
+
<%= local_assigns[:subtitle] %>
|
6
|
+
</span>
|
7
|
+
</div>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%# Standard page layout wrapper %>
|
2
|
+
<div class="h-full flex flex-col">
|
3
|
+
<%= render 'dbwatcher/shared/header', title: local_assigns[:page_title], subtitle: local_assigns[:page_subtitle] %>
|
4
|
+
|
5
|
+
<% if local_assigns[:tabs] %>
|
6
|
+
<%= render 'dbwatcher/shared/tab_bar', tabs: local_assigns[:tabs] %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<% if local_assigns[:toolbar] %>
|
10
|
+
<!-- Toolbar -->
|
11
|
+
<div class="h-8 bg-gray-100 border-b border-gray-300 flex items-center px-4 gap-2">
|
12
|
+
<%= local_assigns[:toolbar] %>
|
13
|
+
</div>
|
14
|
+
<% end %>
|
15
|
+
|
16
|
+
<!-- Content Area -->
|
17
|
+
<div class="flex-1 overflow-auto p-4">
|
18
|
+
<%= yield %>
|
19
|
+
</div>
|
20
|
+
</div>
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<%# Reusable section panel component %>
|
2
|
+
<div class="<%= local_assigns[:classes] || 'mt-4' %> border border-gray-300 rounded">
|
3
|
+
<div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
|
4
|
+
<h3 class="text-xs font-medium text-gray-700"><%= local_assigns[:title] %></h3>
|
5
|
+
</div>
|
6
|
+
<div class="<%= local_assigns[:content_classes] || 'p-4' %>">
|
7
|
+
<%= yield %>
|
8
|
+
</div>
|
9
|
+
</div>
|
@@ -0,0 +1,11 @@
|
|
1
|
+
<%# Enhanced stats card component %>
|
2
|
+
<div class="border border-gray-300 rounded bg-white p-3">
|
3
|
+
<div class="flex items-center justify-between mb-2">
|
4
|
+
<span class="text-xs text-gray-600"><%= local_assigns[:label] %></span>
|
5
|
+
<%= local_assigns[:icon_html]&.html_safe if local_assigns[:icon_html] %>
|
6
|
+
</div>
|
7
|
+
<div class="text-2xl font-bold <%= local_assigns[:value_class] || 'text-navy-dark' %>">
|
8
|
+
<%= local_assigns[:value] || 0 %>
|
9
|
+
</div>
|
10
|
+
<div class="text-xs text-gray-500 mt-1"><%= local_assigns[:description] %></div>
|
11
|
+
</div>
|