dbwatcher 1.1.2 → 1.1.4

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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/config/dbwatcher_manifest.js +1 -0
  3. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +184 -97
  4. data/app/assets/javascripts/dbwatcher/components/timeline.js +211 -0
  5. data/app/assets/javascripts/dbwatcher/dbwatcher.js +5 -0
  6. data/app/assets/stylesheets/dbwatcher/application.css +298 -1
  7. data/app/assets/stylesheets/dbwatcher/application.scss +1 -0
  8. data/app/assets/stylesheets/dbwatcher/components/_timeline.scss +326 -0
  9. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +27 -17
  10. data/app/controllers/dbwatcher/sessions_controller.rb +1 -1
  11. data/app/views/dbwatcher/sessions/_layout.html.erb +5 -2
  12. data/app/views/dbwatcher/sessions/_summary.html.erb +1 -1
  13. data/app/views/dbwatcher/sessions/{_changes.html.erb → _tables.html.erb} +84 -5
  14. data/app/views/dbwatcher/sessions/_timeline.html.erb +260 -0
  15. data/app/views/dbwatcher/sessions/show.html.erb +3 -1
  16. data/app/views/layouts/dbwatcher/application.html.erb +1 -0
  17. data/config/routes.rb +2 -1
  18. data/lib/dbwatcher/configuration.rb +11 -0
  19. data/lib/dbwatcher/logging.rb +23 -1
  20. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +102 -1
  21. data/lib/dbwatcher/services/api/{changes_data_service.rb → tables_data_service.rb} +6 -6
  22. data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +62 -36
  23. data/lib/dbwatcher/services/diagram_generator.rb +35 -69
  24. data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +23 -9
  25. data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +16 -22
  26. data/lib/dbwatcher/services/diagram_strategies/diagram_strategy_helpers.rb +33 -0
  27. data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +20 -25
  28. data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +20 -25
  29. data/lib/dbwatcher/services/diagram_strategies/standard_diagram_strategy.rb +80 -0
  30. data/lib/dbwatcher/services/diagram_system.rb +14 -1
  31. data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +2 -0
  32. data/lib/dbwatcher/services/mermaid_syntax_builder.rb +10 -8
  33. data/lib/dbwatcher/services/timeline_data_service/enhancement_utilities.rb +100 -0
  34. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +125 -0
  35. data/lib/dbwatcher/services/timeline_data_service/metadata_builder.rb +93 -0
  36. data/lib/dbwatcher/services/timeline_data_service.rb +130 -0
  37. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +1 -1
  38. data/lib/dbwatcher/version.rb +1 -1
  39. data/lib/dbwatcher.rb +1 -1
  40. metadata +13 -4
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Timeline Component
3
+ * Interactive timeline visualization for database operations
4
+ * API-first implementation for DBWatcher timeline tab
5
+ */
6
+
7
+ // Register component with DBWatcher
8
+ (function() {
9
+ function registerTimeline() {
10
+ if (window.DBWatcher && window.DBWatcher.ComponentRegistry) {
11
+ DBWatcher.ComponentRegistry.register('timeline', function(config) {
12
+ return Object.assign(DBWatcher.BaseComponent(config), {
13
+ // Component-specific state
14
+ sessionId: config.sessionId,
15
+ timelineData: [],
16
+ filteredData: [],
17
+ metadata: {},
18
+
19
+ // Filter state
20
+ filters: {
21
+ tables: [],
22
+ searchText: ""
23
+ },
24
+ tableSearch: "",
25
+
26
+ // Component initialization
27
+ componentInit() {
28
+ this.loadTimelineData();
29
+ this.setupEventListeners();
30
+ },
31
+
32
+ // Component cleanup
33
+ componentDestroy() {
34
+ // Clean up any event listeners or intervals
35
+ },
36
+
37
+ // Load timeline data from API
38
+ async loadTimelineData() {
39
+ if (!this.sessionId) {
40
+ console.error('No session ID provided to timeline component');
41
+ this.handleError(new Error('No session ID provided'));
42
+ return;
43
+ }
44
+
45
+ this.setLoading(true);
46
+ this.clearError();
47
+
48
+ try {
49
+ const url = `/dbwatcher/api/v1/sessions/${this.sessionId}/timeline_data`;
50
+ const data = await this.fetchData(url);
51
+
52
+ if (!data.error) {
53
+ this.timelineData = data.timeline || [];
54
+ this.metadata = data.metadata || {};
55
+ this.filteredData = [...this.timelineData];
56
+
57
+ this.setupInitialView();
58
+ } else {
59
+ throw new Error(data.error || 'No timeline data received');
60
+ }
61
+ } catch (error) {
62
+ this.handleError(error);
63
+ } finally {
64
+ this.setLoading(false);
65
+ }
66
+ },
67
+
68
+ // Setup initial view after data load
69
+ setupInitialView() {
70
+ // Initialize filters based on available data
71
+ if (this.metadata.tables_affected) {
72
+ // Set up available filter options but don't apply any filters initially
73
+ }
74
+ },
75
+
76
+ // Setup event listeners
77
+ setupEventListeners() {
78
+ // Reserved for future keyboard shortcuts
79
+ },
80
+
81
+ // ==========================================
82
+ // Filtering functionality
83
+ // ==========================================
84
+
85
+ // Apply all filters to timeline data
86
+ applyFilters() {
87
+ this.filteredData = this.timelineData.filter((entry) => {
88
+ return (
89
+ this.matchesTableFilter(entry) &&
90
+ this.matchesSearchFilter(entry)
91
+ );
92
+ });
93
+ },
94
+
95
+ // Filter by table name
96
+ matchesTableFilter(entry) {
97
+ return (
98
+ this.filters.tables.length === 0 ||
99
+ this.filters.tables.includes(entry.table_name)
100
+ );
101
+ },
102
+
103
+ // Filter by search text
104
+ matchesSearchFilter(entry) {
105
+ if (!this.filters.searchText) return true;
106
+
107
+ const searchLower = this.filters.searchText.toLowerCase();
108
+ return (
109
+ entry.table_name.toLowerCase().includes(searchLower) ||
110
+ entry.operation.toLowerCase().includes(searchLower) ||
111
+ (entry.record_id && entry.record_id.toString().includes(searchLower))
112
+ );
113
+ },
114
+
115
+ // Clear all filters
116
+ clearFilters() {
117
+ this.filters = {
118
+ tables: [],
119
+ searchText: ""
120
+ };
121
+ this.applyFilters();
122
+ },
123
+
124
+ // ==========================================
125
+ // Utility methods
126
+ // ==========================================
127
+
128
+ // Format timestamp for display
129
+ formatTimestamp(timestamp) {
130
+ if (!timestamp) return 'N/A';
131
+ return this.formatDate(new Date(timestamp), 'MMM dd, yyyy HH:mm:ss');
132
+ },
133
+
134
+ // Format duration in milliseconds
135
+ formatDuration(ms) {
136
+ if (!ms || ms < 0) return '0ms';
137
+ if (ms < 1000) return `${ms}ms`;
138
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
139
+ return `${(ms / 60000).toFixed(1)}m`;
140
+ },
141
+
142
+ // Get color for operation type
143
+ getOperationColor(operation) {
144
+ const colors = {
145
+ INSERT: "#10b981", // green
146
+ UPDATE: "#f59e0b", // amber
147
+ DELETE: "#ef4444", // red
148
+ SELECT: "#3b82f6" // blue
149
+ };
150
+ return colors[operation] || "#6b7280";
151
+ },
152
+
153
+ // Get operation icon
154
+ getOperationIcon(operation) {
155
+ const icons = {
156
+ INSERT: "plus",
157
+ UPDATE: "pencil",
158
+ DELETE: "trash",
159
+ SELECT: "eye"
160
+ };
161
+ return icons[operation] || "circle";
162
+ },
163
+
164
+ // Get available tables for filtering
165
+ getAvailableTables() {
166
+ return this.metadata.tables_affected || [];
167
+ },
168
+
169
+ // Get available operations for filtering
170
+ getAvailableOperations() {
171
+ return Object.keys(this.metadata.operation_counts || {});
172
+ },
173
+
174
+ // Get operation count for display
175
+ getOperationCount(operation) {
176
+ return this.metadata.operation_counts?.[operation] || 0;
177
+ },
178
+
179
+ // Get total filtered operations count
180
+ getTotalFilteredOperations() {
181
+ return this.filteredData.length;
182
+ },
183
+
184
+ // Get session statistics
185
+ getSessionStats() {
186
+ return {
187
+ totalOperations: this.timelineData.length,
188
+ filteredOperations: this.filteredData.length,
189
+ tablesAffected: this.getAvailableTables().length,
190
+ sessionDuration: this.metadata.session_duration || 'N/A',
191
+ timeRange: this.metadata.time_range || {}
192
+ };
193
+ },
194
+
195
+ // Format relative time from session start
196
+ formatRelativeTime(operation) {
197
+ if (!operation) return '00:00';
198
+ return operation.relative_time || '00:00';
199
+ }
200
+ }); // End of Object.assign
201
+ }); // End of registerComponent
202
+ console.log('✅ Timeline component registered successfully');
203
+ } else {
204
+ console.warn('DBWatcher ComponentRegistry not ready, retrying timeline registration...');
205
+ setTimeout(registerTimeline, 100);
206
+ }
207
+ }
208
+
209
+ // Register immediately without waiting for DOM
210
+ registerTimeline();
211
+ })();
@@ -101,6 +101,11 @@ window.DBWatcher = {
101
101
  return factory(config);
102
102
  },
103
103
 
104
+ // Get component (alias for createComponent for compatibility)
105
+ getComponent(name, config = {}) {
106
+ return this.createComponent(name, config);
107
+ },
108
+
104
109
  // Register a component using the ComponentRegistry
105
110
  register(name, factory) {
106
111
  if (!this.ComponentRegistry) {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * DBWatcher Application Styles
3
3
  * Compiled CSS for all components
4
- * Generated at 2025-07-09 21:44:32 +0700
4
+ * Generated at 2025-07-11 21:59:56 +0700
5
5
  */
6
6
 
7
7
  /**
@@ -747,6 +747,303 @@ pre[x-ref="codeContainer"]::-webkit-scrollbar-thumb:hover {
747
747
  z-index: 100;
748
748
  }
749
749
 
750
+ .timeline-container {
751
+ @apply h-full flex flex-col;
752
+ }
753
+
754
+ .timeline-controls {
755
+ @apply p-3 border-b border-gray-300 bg-gray-50;
756
+ }
757
+
758
+ .timeline-header {
759
+ @apply flex items-center justify-between mb-3;
760
+ }
761
+
762
+ .timeline-title {
763
+ @apply text-sm font-medium text-gray-900;
764
+ }
765
+
766
+ .timeline-zoom-controls {
767
+ @apply flex items-center gap-2;
768
+ }
769
+
770
+ .timeline-filter-controls {
771
+ @apply flex items-center gap-4 text-xs flex-wrap;
772
+ }
773
+
774
+ .timeline-filter-group {
775
+ @apply flex items-center gap-2;
776
+ }
777
+
778
+ .timeline-filter-label {
779
+ @apply text-gray-700 font-medium;
780
+ }
781
+
782
+ .timeline-visualization {
783
+ @apply flex-1 overflow-hidden;
784
+ }
785
+
786
+ .timeline-time-header {
787
+ @apply h-8 bg-gray-100 border-b border-gray-200 relative;
788
+ }
789
+
790
+ .timeline-time-scale {
791
+ @apply absolute inset-0 flex items-center px-4;
792
+ }
793
+
794
+ .timeline-content {
795
+ @apply flex-1 overflow-auto p-4 bg-white;
796
+ }
797
+
798
+ .timeline-track {
799
+ @apply relative h-16 bg-gray-50 rounded border border-gray-200 mb-4;
800
+ }
801
+
802
+ .timeline-line {
803
+ @apply absolute top-1/2 left-4 right-4 h-0.5 bg-gray-300 transform -translate-y-1/2;
804
+ }
805
+
806
+ .timeline-marker {
807
+ @apply absolute top-1/2 transform -translate-y-1/2 -translate-x-1/2 cursor-pointer;
808
+ }
809
+
810
+ .timeline-marker .timeline-marker-dot {
811
+ @apply w-3 h-3 rounded-full border-2 border-white shadow-sm transition-transform;
812
+ }
813
+
814
+ .timeline-marker .timeline-marker-dot:hover {
815
+ @apply scale-125;
816
+ }
817
+
818
+ .timeline-stats {
819
+ @apply grid grid-cols-2 gap-4 text-xs;
820
+ }
821
+
822
+ @screen md {
823
+ .timeline-stats {
824
+ @apply grid-cols-4;
825
+ }
826
+ }
827
+
828
+ .timeline-stat-card {
829
+ @apply bg-gray-50 p-3 rounded;
830
+ }
831
+
832
+ .timeline-stat-label {
833
+ @apply text-gray-500 font-medium;
834
+ }
835
+
836
+ .timeline-stat-value {
837
+ @apply text-lg font-bold text-gray-900;
838
+ }
839
+
840
+ .timeline-stat-detail {
841
+ @apply text-xs text-gray-500;
842
+ }
843
+
844
+ .timeline-operations {
845
+ @apply mt-6;
846
+ }
847
+
848
+ .timeline-operations-title {
849
+ @apply text-sm font-medium text-gray-900 mb-3;
850
+ }
851
+
852
+ .timeline-operations-list {
853
+ @apply space-y-2 max-h-64 overflow-auto;
854
+ }
855
+
856
+ .timeline-operation-item {
857
+ @apply flex items-center justify-between p-2 bg-gray-50 rounded hover:bg-gray-100 cursor-pointer text-xs;
858
+ }
859
+
860
+ .timeline-operation-info {
861
+ @apply flex items-center gap-3;
862
+ }
863
+
864
+ .timeline-operation-marker {
865
+ @apply w-2 h-2 rounded-full;
866
+ }
867
+
868
+ .timeline-operation-type {
869
+ @apply font-medium;
870
+ }
871
+
872
+ .timeline-operation-time {
873
+ @apply text-gray-500;
874
+ }
875
+
876
+ .timeline-operation-record {
877
+ @apply text-gray-500;
878
+ }
879
+
880
+ .timeline-empty {
881
+ @apply text-center py-8 text-gray-500;
882
+ }
883
+
884
+ .timeline-empty-icon {
885
+ @apply w-12 h-12 mx-auto mb-4 text-gray-300;
886
+ }
887
+
888
+ .timeline-empty-action {
889
+ @apply mt-2 text-blue-600 underline;
890
+ }
891
+
892
+ .timeline-loading {
893
+ @apply flex items-center justify-center h-64;
894
+ }
895
+
896
+ .timeline-loading-spinner {
897
+ @apply animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500;
898
+ }
899
+
900
+ .timeline-loading-text {
901
+ @apply ml-2 text-gray-600;
902
+ }
903
+
904
+ .timeline-error {
905
+ @apply p-4 bg-red-50 border border-red-200 rounded m-4;
906
+ }
907
+
908
+ .timeline-error-text {
909
+ @apply text-red-700;
910
+ }
911
+
912
+ .timeline-error-retry {
913
+ @apply mt-2 text-red-600 underline;
914
+ }
915
+
916
+ .timeline-modal-overlay {
917
+ @apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50;
918
+ }
919
+
920
+ .timeline-modal-content {
921
+ @apply bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-96 overflow-auto;
922
+ }
923
+
924
+ .timeline-modal-header {
925
+ @apply p-4 border-b border-gray-200 flex justify-between items-center;
926
+ }
927
+
928
+ .timeline-modal-title {
929
+ @apply text-lg font-medium;
930
+ }
931
+
932
+ .timeline-modal-close {
933
+ @apply text-gray-400 hover:text-gray-600;
934
+ }
935
+
936
+ .timeline-modal-body {
937
+ @apply p-4;
938
+ }
939
+
940
+ .timeline-operation-details {
941
+ @apply grid grid-cols-1 gap-4 text-sm;
942
+ }
943
+
944
+ @screen md {
945
+ .timeline-operation-details {
946
+ @apply grid-cols-2;
947
+ }
948
+ }
949
+
950
+ .timeline-operation-detail-label {
951
+ @apply font-medium;
952
+ }
953
+
954
+ .timeline-operation-detail-value {
955
+ @apply ml-2;
956
+ }
957
+
958
+ .timeline-operation-detail-value.operation-badge {
959
+ @apply px-2 py-1 rounded text-xs;
960
+ }
961
+
962
+ .timeline-operation-detail-value.monospace {
963
+ @apply font-mono;
964
+ }
965
+
966
+ .timeline-operation-detail-value.small {
967
+ @apply text-xs;
968
+ }
969
+
970
+ .timeline-changes-section {
971
+ @apply mt-4;
972
+ }
973
+
974
+ .timeline-changes-title {
975
+ @apply text-sm font-medium;
976
+ }
977
+
978
+ .timeline-changes-content {
979
+ @apply mt-2 p-3 bg-gray-50 rounded text-xs overflow-auto border max-h-32;
980
+ }
981
+
982
+ .timeline-metadata-section {
983
+ @apply mt-4;
984
+ }
985
+
986
+ .timeline-metadata-title {
987
+ @apply text-sm font-medium;
988
+ }
989
+
990
+ .timeline-metadata-grid {
991
+ @apply mt-2 grid grid-cols-2 gap-2 text-xs;
992
+ }
993
+
994
+ .timeline-metadata-key {
995
+ @apply font-medium capitalize;
996
+ }
997
+
998
+ @screen sm {
999
+ .timeline-filter-controls {
1000
+ @apply flex-nowrap;
1001
+ }
1002
+ .timeline-stats {
1003
+ @apply grid-cols-4;
1004
+ }
1005
+ }
1006
+
1007
+ .operation-insert {
1008
+ @apply text-green-600 bg-green-100;
1009
+ }
1010
+
1011
+ .operation-update {
1012
+ @apply text-blue-600 bg-blue-100;
1013
+ }
1014
+
1015
+ .operation-delete {
1016
+ @apply text-red-600 bg-red-100;
1017
+ }
1018
+
1019
+ .operation-select {
1020
+ @apply text-purple-600 bg-purple-100;
1021
+ }
1022
+
1023
+ .timeline-transition-enter {
1024
+ @apply transition ease-out duration-300;
1025
+ }
1026
+
1027
+ .timeline-transition-enter-start {
1028
+ @apply opacity-0;
1029
+ }
1030
+
1031
+ .timeline-transition-enter-end {
1032
+ @apply opacity-100;
1033
+ }
1034
+
1035
+ .timeline-transition-leave {
1036
+ @apply transition ease-in duration-200;
1037
+ }
1038
+
1039
+ .timeline-transition-leave-start {
1040
+ @apply opacity-100;
1041
+ }
1042
+
1043
+ .timeline-transition-leave-end {
1044
+ @apply opacity-0;
1045
+ }
1046
+
750
1047
  /**
751
1048
  * Tabulator Vendor Overrides
752
1049
  * Overrides for the Tabulator.js vendor CSS
@@ -14,6 +14,7 @@
14
14
  @import "components/badges";
15
15
  @import "components/diagrams";
16
16
  @import "components/tabulator";
17
+ @import "components/timeline";
17
18
 
18
19
  // Vendor overrides - must be imported after our components
19
20
  @import "vendor/tabulator_overrides";