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,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module DiagramHelper
|
5
|
+
# Generate diagram configuration for Alpine.js
|
6
|
+
def diagram_config(session, active_tab)
|
7
|
+
{
|
8
|
+
auto_generate: active_tab == "diagrams",
|
9
|
+
default_type: "database_tables",
|
10
|
+
endpoint: diagram_data_api_v1_session_path(session),
|
11
|
+
container_id: "diagram-container"
|
12
|
+
}.to_json
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generate diagram type options dynamically from registry
|
16
|
+
def diagram_type_options
|
17
|
+
registry = Dbwatcher::Services::DiagramTypeRegistry.new
|
18
|
+
options = registry.available_types_with_metadata.map do |type, metadata|
|
19
|
+
[metadata[:display_name], type]
|
20
|
+
end
|
21
|
+
|
22
|
+
options_for_select(options)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Generate CSS variables for diagram container height calculation
|
26
|
+
def diagram_container_css_variables
|
27
|
+
{
|
28
|
+
"--header-height": "64px",
|
29
|
+
"--tab-bar-height": "40px",
|
30
|
+
"--toolbar-height": "72px",
|
31
|
+
"--footer-height": "0px",
|
32
|
+
"--diagram-height": "calc(100vh - var(--header-height) - var(--tab-bar-height) - " \
|
33
|
+
"var(--toolbar-height) - var(--footer-height) - 2rem)",
|
34
|
+
"--diagram-min-height": "500px"
|
35
|
+
}.map { |key, value| "#{key}: #{value}" }.join("; ")
|
36
|
+
end
|
37
|
+
|
38
|
+
# Generate button classes for diagram controls
|
39
|
+
def diagram_button_classes(type = :default)
|
40
|
+
base_classes = "compact-button text-xs rounded"
|
41
|
+
|
42
|
+
button_styles = {
|
43
|
+
primary: "bg-blue-medium text-white px-3 py-1 hover:bg-navy-dark",
|
44
|
+
secondary: "bg-navy-dark text-white px-2 py-1 hover:bg-blue-medium",
|
45
|
+
toggle: "bg-blue-medium text-white px-2 py-1 hover:bg-navy-dark flex items-center gap-1",
|
46
|
+
icon: "bg-white border border-gray-300 hover:bg-gray-50 p-1",
|
47
|
+
danger: "bg-red-500 text-white px-2 py-1 hover:bg-red-600",
|
48
|
+
success: "bg-green-500 text-white px-2 py-1 hover:bg-green-600"
|
49
|
+
}
|
50
|
+
|
51
|
+
style = button_styles[type] || button_styles[:primary]
|
52
|
+
"#{base_classes} #{style}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Generate a code view with syntax highlighting for Mermaid code
|
56
|
+
def diagram_code_view(content)
|
57
|
+
content_tag(:div, class: "diagram-code-view") do
|
58
|
+
content_tag(:pre,
|
59
|
+
class: "text-xs font-mono p-4 bg-gray-50 rounded border border-gray-200 " \
|
60
|
+
"overflow-x-auto whitespace-pre-wrap",
|
61
|
+
style: "max-height: calc(100vh - 220px); overflow-y: auto;") do
|
62
|
+
content_tag(:code, content)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Generate copy to clipboard button
|
68
|
+
def copy_to_clipboard_button(target_id)
|
69
|
+
button_tag(
|
70
|
+
type: "button",
|
71
|
+
class: diagram_button_classes(:icon),
|
72
|
+
"x-on:click": "copyToClipboard('#{target_id}')",
|
73
|
+
title: "Copy to clipboard"
|
74
|
+
) do
|
75
|
+
copy_icon_svg
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Generate copy icon SVG
|
82
|
+
def copy_icon_svg
|
83
|
+
content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
|
84
|
+
tag.path(
|
85
|
+
stroke_linecap: "round",
|
86
|
+
stroke_linejoin: "round",
|
87
|
+
stroke_width: "2",
|
88
|
+
d: "M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2" \
|
89
|
+
"M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"
|
90
|
+
)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# Generate toggle view button
|
95
|
+
def toggle_view_button
|
96
|
+
button_tag(
|
97
|
+
type: "button",
|
98
|
+
class: diagram_button_classes(:toggle),
|
99
|
+
"x-on:click": "toggleViewMode()",
|
100
|
+
title: "Toggle between code and preview"
|
101
|
+
) do
|
102
|
+
concat(content_tag(:svg, class: "w-4 h-4", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24") do
|
103
|
+
tag.path(stroke_linecap: "round", stroke_linejoin: "round", stroke_width: "2",
|
104
|
+
d: "M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4")
|
105
|
+
end)
|
106
|
+
concat(content_tag(:span, "x-text": "viewMode === 'preview' ? 'View Code' : 'View Preview'"))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module FormattingHelper
|
5
|
+
# Truncate cell values for display in compact view
|
6
|
+
def truncate_cell_value(value, max_length = 50)
|
7
|
+
return "" if value.nil?
|
8
|
+
|
9
|
+
formatted_value = format_cell_value_simple(value)
|
10
|
+
|
11
|
+
if formatted_value.length > max_length
|
12
|
+
"#{formatted_value[0...max_length]}..."
|
13
|
+
else
|
14
|
+
formatted_value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Format cell values for display
|
19
|
+
def format_cell_value(value)
|
20
|
+
return "" if value.nil?
|
21
|
+
|
22
|
+
case value
|
23
|
+
when String
|
24
|
+
format_string_value(value)
|
25
|
+
when Hash, Array
|
26
|
+
JSON.pretty_generate(value)
|
27
|
+
when Time, DateTime
|
28
|
+
format_datetime_value(value)
|
29
|
+
when Date
|
30
|
+
value.strftime("%Y-%m-%d")
|
31
|
+
else
|
32
|
+
value.to_s
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Simple formatting for truncated display
|
37
|
+
def format_cell_value_simple(value)
|
38
|
+
return "" if value.nil?
|
39
|
+
|
40
|
+
case value
|
41
|
+
when String
|
42
|
+
format_string_value_simple(value)
|
43
|
+
when Hash
|
44
|
+
format_hash_simple(value)
|
45
|
+
when Array
|
46
|
+
format_array_simple(value)
|
47
|
+
when Time, DateTime
|
48
|
+
format_datetime_value(value)
|
49
|
+
when Date
|
50
|
+
value.strftime("%Y-%m-%d")
|
51
|
+
else
|
52
|
+
value.to_s
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Format string values with JSON detection
|
59
|
+
def format_string_value(value)
|
60
|
+
return value unless json?(value)
|
61
|
+
|
62
|
+
begin
|
63
|
+
JSON.pretty_generate(JSON.parse(value))
|
64
|
+
rescue JSON::ParserError
|
65
|
+
value
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def format_string_value_simple(value)
|
70
|
+
return value unless json?(value)
|
71
|
+
|
72
|
+
begin
|
73
|
+
parsed = JSON.parse(value)
|
74
|
+
format_parsed_json_simple(parsed)
|
75
|
+
rescue JSON::ParserError
|
76
|
+
value
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def format_parsed_json_simple(parsed)
|
81
|
+
if parsed.is_a?(Array)
|
82
|
+
format_array_simple(parsed)
|
83
|
+
elsif parsed.is_a?(Hash)
|
84
|
+
format_hash_simple(parsed)
|
85
|
+
else
|
86
|
+
parsed.to_s
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def format_array_simple(array)
|
91
|
+
"[#{array.length} items]"
|
92
|
+
end
|
93
|
+
|
94
|
+
def format_hash_simple(hash)
|
95
|
+
"{#{hash.keys.length} keys}"
|
96
|
+
end
|
97
|
+
|
98
|
+
def format_datetime_value(value)
|
99
|
+
value.strftime("%Y-%m-%d %H:%M:%S")
|
100
|
+
end
|
101
|
+
|
102
|
+
def json?(string)
|
103
|
+
return false unless string.is_a?(String)
|
104
|
+
|
105
|
+
string.strip.start_with?("{", "[")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module SessionHelper
|
5
|
+
# Get session change count with fallback
|
6
|
+
def session_change_count(session)
|
7
|
+
safe_value(session, :change_count, 0).to_i
|
8
|
+
end
|
9
|
+
|
10
|
+
# Determine if session is active
|
11
|
+
def session_active?(session)
|
12
|
+
safe_value(session, :ended_at).blank?
|
13
|
+
end
|
14
|
+
|
15
|
+
# Format session name for display
|
16
|
+
def display_session_name(name)
|
17
|
+
name.to_s.gsub(/^HTTP \w+ /, "")
|
18
|
+
end
|
19
|
+
|
20
|
+
# Generate session ID display (wider format)
|
21
|
+
def display_session_id(id)
|
22
|
+
return "N/A" unless id
|
23
|
+
|
24
|
+
# Show more characters of the session ID for better readability
|
25
|
+
"#{id[0..15]}..."
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
<%# Dashboard Overview Page %>
|
2
|
+
<div class="h-full flex flex-col">
|
3
|
+
<!-- Compact Header -->
|
4
|
+
<div class="h-10 bg-navy-dark text-white flex items-center px-4">
|
5
|
+
<h1 class="text-sm font-medium">Dashboard</h1>
|
6
|
+
<span class="ml-auto text-xs text-blue-light">
|
7
|
+
<%= Time.current.strftime("%Y-%m-%d %H:%M:%S") %>
|
8
|
+
</span>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<!-- Tab Bar -->
|
12
|
+
<div class="tab-bar">
|
13
|
+
<div class="tab-item active">Overview</div>
|
14
|
+
<div class="tab-item">Statistics</div>
|
15
|
+
<div class="tab-item">Recent Activity</div>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<!-- Content Area -->
|
19
|
+
<div class="flex-1 overflow-auto p-4">
|
20
|
+
<!-- Compact Stats Grid -->
|
21
|
+
<div class="grid grid-cols-4 gap-3 mb-4">
|
22
|
+
<!-- Sessions Card -->
|
23
|
+
<%= render 'dbwatcher/shared/stats_card',
|
24
|
+
label: 'Active Sessions',
|
25
|
+
value: @recent_sessions&.count || 0,
|
26
|
+
description: 'Last 24 hours',
|
27
|
+
icon_html: stats_icon(:sessions) %>
|
28
|
+
|
29
|
+
<!-- Tables Card -->
|
30
|
+
<%= render 'dbwatcher/shared/stats_card',
|
31
|
+
label: 'Modified Tables',
|
32
|
+
value: @active_tables&.count || 0,
|
33
|
+
description: 'With changes',
|
34
|
+
icon_html: stats_icon(:tables) %>
|
35
|
+
|
36
|
+
<!-- Queries Card -->
|
37
|
+
<%= render 'dbwatcher/shared/stats_card',
|
38
|
+
label: 'SQL Queries',
|
39
|
+
value: @query_stats&.dig(:total) || 0,
|
40
|
+
description: 'Today',
|
41
|
+
icon_html: stats_icon(:queries) %>
|
42
|
+
|
43
|
+
<!-- Performance Card -->
|
44
|
+
<% slow_queries = @query_stats&.dig(:slow_queries) || 0 %>
|
45
|
+
<%= render 'dbwatcher/shared/stats_card',
|
46
|
+
label: 'Slow Queries',
|
47
|
+
value: slow_queries,
|
48
|
+
value_class: (slow_queries > 0 ? 'text-red-600' : 'text-navy-dark'),
|
49
|
+
description: '> 100ms',
|
50
|
+
icon_html: stats_icon(:performance) %>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<!-- Two Column Layout -->
|
54
|
+
<div class="grid grid-cols-2 gap-4">
|
55
|
+
<!-- Recent Sessions -->
|
56
|
+
<div class="border border-gray-300 rounded">
|
57
|
+
<div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
|
58
|
+
<h3 class="text-xs font-medium text-gray-700">Recent Sessions</h3>
|
59
|
+
</div>
|
60
|
+
<div class="max-h-64 overflow-auto">
|
61
|
+
<% if @recent_sessions&.any? %>
|
62
|
+
<table class="compact-table w-full">
|
63
|
+
<thead>
|
64
|
+
<tr>
|
65
|
+
<th class="text-left">Session</th>
|
66
|
+
<th class="text-center">Changes</th>
|
67
|
+
<th class="text-right">Time</th>
|
68
|
+
</tr>
|
69
|
+
</thead>
|
70
|
+
<tbody>
|
71
|
+
<% @recent_sessions.each do |session| %>
|
72
|
+
<tr class="hover:bg-blue-50">
|
73
|
+
<td class="truncate max-w-xs" title="<%= session[:name] || session['name'] %>">
|
74
|
+
<% session_id = session[:id] || session['id'] %>
|
75
|
+
<% session_name = (session[:name] || session['name']).to_s.gsub(/^HTTP \w+ /, '') %>
|
76
|
+
<%= link_to session_name, session_path(session_id), class: "text-navy-dark hover:text-blue-medium" %>
|
77
|
+
</td>
|
78
|
+
<td class="text-center">
|
79
|
+
<span class="badge bg-gray-600 text-white">
|
80
|
+
<%= session[:change_count] || session['change_count'] || 0 %>
|
81
|
+
</span>
|
82
|
+
</td>
|
83
|
+
<td class="text-right text-gray-500">
|
84
|
+
<% started_at = session[:started_at] || session['started_at'] %>
|
85
|
+
<%= Time.parse(started_at).strftime("%H:%M:%S") rescue 'N/A' %>
|
86
|
+
</td>
|
87
|
+
</tr>
|
88
|
+
<% end %>
|
89
|
+
</tbody>
|
90
|
+
</table>
|
91
|
+
<% else %>
|
92
|
+
<div class="p-4 text-center text-gray-500 text-xs">No recent sessions</div>
|
93
|
+
<% end %>
|
94
|
+
</div>
|
95
|
+
</div>
|
96
|
+
|
97
|
+
<!-- Active Tables -->
|
98
|
+
<div class="border border-gray-300 rounded">
|
99
|
+
<div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
|
100
|
+
<h3 class="text-xs font-medium text-gray-700">Most Active Tables</h3>
|
101
|
+
</div>
|
102
|
+
<div class="max-h-64 overflow-auto">
|
103
|
+
<% if @active_tables&.any? %>
|
104
|
+
<table class="compact-table w-full">
|
105
|
+
<thead>
|
106
|
+
<tr>
|
107
|
+
<th class="text-left">Table</th>
|
108
|
+
<th class="text-center">Operations</th>
|
109
|
+
<th class="text-right">Changes</th>
|
110
|
+
</tr>
|
111
|
+
</thead>
|
112
|
+
<tbody>
|
113
|
+
<% @active_tables.first(10).each do |table_name, count| %>
|
114
|
+
<tr class="hover:bg-blue-50">
|
115
|
+
<td class="font-medium text-navy-dark">
|
116
|
+
<%= link_to table_name, table_path(table_name), class: "text-navy-dark hover:text-blue-medium" %>
|
117
|
+
</td>
|
118
|
+
<td class="text-center">
|
119
|
+
<div class="flex gap-1 justify-center">
|
120
|
+
<span class="badge badge-insert" title="Inserts">I</span>
|
121
|
+
<span class="badge badge-update" title="Updates">U</span>
|
122
|
+
<span class="badge badge-delete" title="Deletes">D</span>
|
123
|
+
</div>
|
124
|
+
</td>
|
125
|
+
<td class="text-right">
|
126
|
+
<span class="text-sm font-medium"><%= count %></span>
|
127
|
+
</td>
|
128
|
+
</tr>
|
129
|
+
<% end %>
|
130
|
+
</tbody>
|
131
|
+
</table>
|
132
|
+
<% else %>
|
133
|
+
<div class="p-4 text-center text-gray-500 text-xs">No active tables</div>
|
134
|
+
<% end %>
|
135
|
+
</div>
|
136
|
+
</div>
|
137
|
+
</div>
|
138
|
+
|
139
|
+
<!-- Query Activity Section -->
|
140
|
+
<div class="mt-4 border border-gray-300 rounded">
|
141
|
+
<div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
|
142
|
+
<h3 class="text-xs font-medium text-gray-700">Query Activity</h3>
|
143
|
+
</div>
|
144
|
+
<div class="p-4">
|
145
|
+
<% if @query_stats&.dig(:by_operation)&.any? %>
|
146
|
+
<div class="grid grid-cols-4 gap-4 text-center">
|
147
|
+
<% @query_stats[:by_operation].each do |operation, count| %>
|
148
|
+
<div class="border border-gray-200 rounded p-2">
|
149
|
+
<div class="text-lg font-bold text-navy-dark"><%= count %></div>
|
150
|
+
<div class="text-xs text-gray-500 uppercase"><%= operation %></div>
|
151
|
+
</div>
|
152
|
+
<% end %>
|
153
|
+
</div>
|
154
|
+
<% else %>
|
155
|
+
<div class="text-center text-gray-500 text-xs">No query activity</div>
|
156
|
+
<% end %>
|
157
|
+
</div>
|
158
|
+
</div>
|
159
|
+
|
160
|
+
<!-- Quick Actions -->
|
161
|
+
<div class="mt-4 border border-gray-300 rounded">
|
162
|
+
<div class="bg-gray-100 px-3 py-2 border-b border-gray-300">
|
163
|
+
<h3 class="text-xs font-medium text-gray-700">Quick Actions</h3>
|
164
|
+
</div>
|
165
|
+
<div class="p-3">
|
166
|
+
<div class="flex gap-2">
|
167
|
+
<%= link_to "View All Sessions", sessions_path,
|
168
|
+
class: "px-3 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700" %>
|
169
|
+
<%= link_to "Browse Tables", tables_path,
|
170
|
+
class: "px-3 py-1 text-xs bg-green-600 text-white rounded hover:bg-green-700" %>
|
171
|
+
<%= link_to "SQL Logs", queries_path,
|
172
|
+
class: "px-3 py-1 text-xs bg-purple-600 text-white rounded hover:bg-purple-700" %>
|
173
|
+
</div>
|
174
|
+
</div>
|
175
|
+
</div>
|
176
|
+
</div>
|
177
|
+
</div>
|
@@ -0,0 +1,240 @@
|
|
1
|
+
<div class="h-full flex flex-col" x-data="queryLogs()">
|
2
|
+
<!-- Header with embedded stats -->
|
3
|
+
<div class="h-10 bg-navy-dark text-white flex items-center px-4">
|
4
|
+
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
5
|
+
<path fill-rule="evenodd" d="M2 5a2 2 0 012-2h12a2 2 0 012 2v10a2 2 0 01-2 2H4a2 2 0 01-2-2V5zm3.293 1.293a1 1 0 011.414 0l3 3a1 1 0 010 1.414l-3 3a1 1 0 01-1.414-1.414L7.586 10 5.293 7.707a1 1 0 010-1.414zM11 12a1 1 0 100 2h3a1 1 0 100-2h-3z" clip-rule="evenodd"/>
|
6
|
+
</svg>
|
7
|
+
<h1 class="text-sm font-medium">SQL Query Logs</h1>
|
8
|
+
|
9
|
+
<!-- Quick Stats -->
|
10
|
+
<div class="ml-auto flex items-center gap-4 text-xs">
|
11
|
+
<span x-text="`${filteredQueries.length} queries`"></span>
|
12
|
+
<span class="text-gold-light" x-text="`${slowQueries.length} slow`"></span>
|
13
|
+
</div>
|
14
|
+
</div>
|
15
|
+
|
16
|
+
<%= render 'dbwatcher/shared/tab_bar', tabs: [
|
17
|
+
{ name: 'All Queries', active: true },
|
18
|
+
{ name: 'Slow Queries', active: false },
|
19
|
+
{ name: 'Recent', active: false }
|
20
|
+
] %>
|
21
|
+
|
22
|
+
<!-- Compact Toolbar -->
|
23
|
+
<div class="bg-gray-100 border-b border-gray-300 px-3 py-1 flex items-center gap-3">
|
24
|
+
<!-- Filter Controls -->
|
25
|
+
<select x-model="filters.operation"
|
26
|
+
@change="applyFilters()"
|
27
|
+
class="compact-select">
|
28
|
+
<option value="">All Operations</option>
|
29
|
+
<option value="SELECT">SELECT</option>
|
30
|
+
<option value="INSERT">INSERT</option>
|
31
|
+
<option value="UPDATE">UPDATE</option>
|
32
|
+
<option value="DELETE">DELETE</option>
|
33
|
+
</select>
|
34
|
+
|
35
|
+
<input type="text"
|
36
|
+
x-model="filters.table"
|
37
|
+
@input="applyFilters()"
|
38
|
+
placeholder="Filter by table..."
|
39
|
+
class="compact-input flex-1 max-w-xs">
|
40
|
+
|
41
|
+
<input type="number"
|
42
|
+
x-model="filters.minDuration"
|
43
|
+
@input="applyFilters()"
|
44
|
+
placeholder="Min ms"
|
45
|
+
class="compact-input w-20">
|
46
|
+
|
47
|
+
<input type="date"
|
48
|
+
value="<%= @date %>"
|
49
|
+
@change="changeDate($event.target.value)"
|
50
|
+
class="compact-input">
|
51
|
+
|
52
|
+
<!-- Time Range Filters -->
|
53
|
+
<input type="time"
|
54
|
+
x-model="filters.startTime"
|
55
|
+
@input="applyFilters()"
|
56
|
+
placeholder="Start time"
|
57
|
+
title="Start time"
|
58
|
+
class="compact-input w-24">
|
59
|
+
|
60
|
+
<input type="time"
|
61
|
+
x-model="filters.endTime"
|
62
|
+
@input="applyFilters()"
|
63
|
+
placeholder="End time"
|
64
|
+
title="End time"
|
65
|
+
class="compact-input w-24">
|
66
|
+
|
67
|
+
<button @click="clearFilters()"
|
68
|
+
title="Clear all filters"
|
69
|
+
class="compact-button bg-gray-500 text-white hover:bg-gray-600">
|
70
|
+
Clear
|
71
|
+
</button>
|
72
|
+
|
73
|
+
<div class="ml-auto flex items-center gap-2">
|
74
|
+
<button @click="exportQueries()"
|
75
|
+
class="compact-button bg-white border border-gray-300 hover:bg-gray-50">
|
76
|
+
<svg class="w-3 h-3 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
77
|
+
<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"/>
|
78
|
+
</svg>
|
79
|
+
Export
|
80
|
+
</button>
|
81
|
+
|
82
|
+
<button @click="refreshData()"
|
83
|
+
class="compact-button bg-blue-medium text-white hover:bg-blue-700">
|
84
|
+
<svg class="w-3 h-3 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
85
|
+
<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"/>
|
86
|
+
</svg>
|
87
|
+
Refresh
|
88
|
+
</button>
|
89
|
+
|
90
|
+
<%= button_to clear_queries_path,
|
91
|
+
method: :delete,
|
92
|
+
class: "compact-button bg-red-600 text-white hover:bg-red-700",
|
93
|
+
data: {
|
94
|
+
confirm: "Are you sure you want to clear all SQL query logs? This action cannot be undone."
|
95
|
+
} do %>
|
96
|
+
<svg class="w-3 h-3 inline mr-1" fill="currentColor" viewBox="0 0 20 20">
|
97
|
+
<path fill-rule="evenodd" d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"/>
|
98
|
+
<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"/>
|
99
|
+
</svg>
|
100
|
+
Clear Logs
|
101
|
+
<% end %>
|
102
|
+
</div>
|
103
|
+
</div>
|
104
|
+
|
105
|
+
<!-- Content Area -->
|
106
|
+
<div class="flex-1 overflow-auto">
|
107
|
+
<div class="h-full">
|
108
|
+
<table class="compact-table w-full">
|
109
|
+
<thead>
|
110
|
+
<tr>
|
111
|
+
<th class="text-left w-20">Time</th>
|
112
|
+
<th class="text-center w-16">Op</th>
|
113
|
+
<th class="text-left w-32">Tables</th>
|
114
|
+
<th class="text-right w-16">Duration</th>
|
115
|
+
<th class="text-left">SQL</th>
|
116
|
+
</tr>
|
117
|
+
</thead>
|
118
|
+
<tbody>
|
119
|
+
<template x-for="query in filteredQueries" :key="query.id">
|
120
|
+
<tr class="cursor-pointer hover:bg-blue-50"
|
121
|
+
@click="selectQuery(query)"
|
122
|
+
:class="{ 'selected': selectedQuery?.id === query.id }">
|
123
|
+
<td class="text-xs text-gray-600" x-text="formatTime(query.timestamp)"></td>
|
124
|
+
<td class="text-center">
|
125
|
+
<span class="badge"
|
126
|
+
:class="`badge-${query.operation.toLowerCase()}`"
|
127
|
+
x-text="query.operation.charAt(0)"></span>
|
128
|
+
</td>
|
129
|
+
<td class="font-medium text-navy-dark truncate"
|
130
|
+
:title="query.tables.join(', ')"
|
131
|
+
x-text="query.tables.join(', ')"></td>
|
132
|
+
<td class="text-right text-xs"
|
133
|
+
:class="query.duration > 100 ? 'text-red-600 font-medium' : 'text-gray-600'"
|
134
|
+
x-text="`${query.duration}ms`"></td>
|
135
|
+
<td class="font-mono text-xs truncate max-w-md"
|
136
|
+
:title="query.sql"
|
137
|
+
x-text="query.sql"></td>
|
138
|
+
</tr>
|
139
|
+
</template>
|
140
|
+
</tbody>
|
141
|
+
</table>
|
142
|
+
</div>
|
143
|
+
</div>
|
144
|
+
</div>
|
145
|
+
<script>
|
146
|
+
function queryLogs() {
|
147
|
+
return {
|
148
|
+
filters: {
|
149
|
+
operation: '',
|
150
|
+
table: '',
|
151
|
+
minDuration: '',
|
152
|
+
startTime: '',
|
153
|
+
endTime: ''
|
154
|
+
},
|
155
|
+
selectedQuery: null,
|
156
|
+
queries: <%= @queries.to_json.html_safe %>,
|
157
|
+
|
158
|
+
get filteredQueries() {
|
159
|
+
return this.queries.filter(query => {
|
160
|
+
if (this.filters.operation && query.operation !== this.filters.operation) return false;
|
161
|
+
if (this.filters.table && !query.tables.some(t => t.toLowerCase().includes(this.filters.table.toLowerCase()))) return false;
|
162
|
+
if (this.filters.minDuration && query.duration < parseFloat(this.filters.minDuration)) return false;
|
163
|
+
|
164
|
+
// Time filtering
|
165
|
+
if (this.filters.startTime || this.filters.endTime) {
|
166
|
+
const queryTime = new Date(query.timestamp);
|
167
|
+
const queryTimeStr = queryTime.toTimeString().substr(0, 5); // HH:MM format
|
168
|
+
|
169
|
+
if (this.filters.startTime && queryTimeStr < this.filters.startTime) return false;
|
170
|
+
if (this.filters.endTime && queryTimeStr > this.filters.endTime) return false;
|
171
|
+
}
|
172
|
+
|
173
|
+
return true;
|
174
|
+
});
|
175
|
+
},
|
176
|
+
|
177
|
+
get slowQueries() {
|
178
|
+
return this.queries.filter(q => q.duration > 100);
|
179
|
+
},
|
180
|
+
|
181
|
+
selectQuery(query) {
|
182
|
+
this.selectedQuery = this.selectedQuery?.id === query.id ? null : query;
|
183
|
+
},
|
184
|
+
|
185
|
+
formatTime(timestamp) {
|
186
|
+
return new Date(timestamp).toLocaleTimeString('en-US', {
|
187
|
+
hour12: false,
|
188
|
+
hour: '2-digit',
|
189
|
+
minute: '2-digit',
|
190
|
+
second: '2-digit'
|
191
|
+
});
|
192
|
+
},
|
193
|
+
|
194
|
+
applyFilters() {
|
195
|
+
// Filters are applied via computed property
|
196
|
+
},
|
197
|
+
|
198
|
+
exportQueries() {
|
199
|
+
const data = this.filteredQueries.map(q => ({
|
200
|
+
time: this.formatTime(q.timestamp),
|
201
|
+
operation: q.operation,
|
202
|
+
tables: q.tables.join(', '),
|
203
|
+
duration: q.duration,
|
204
|
+
sql: q.sql
|
205
|
+
}));
|
206
|
+
|
207
|
+
const csv = [
|
208
|
+
['Time', 'Operation', 'Tables', 'Duration (ms)', 'SQL'],
|
209
|
+
...data.map(row => [row.time, row.operation, row.tables, row.duration, row.sql])
|
210
|
+
].map(row => row.map(cell => `"${cell}"`).join(',')).join('\n');
|
211
|
+
|
212
|
+
const blob = new Blob([csv], { type: 'text/csv' });
|
213
|
+
const url = URL.createObjectURL(blob);
|
214
|
+
const a = document.createElement('a');
|
215
|
+
a.href = url;
|
216
|
+
a.download = `queries-${new Date().toISOString().split('T')[0]}.csv`;
|
217
|
+
a.click();
|
218
|
+
URL.revokeObjectURL(url);
|
219
|
+
},
|
220
|
+
|
221
|
+
refreshData() {
|
222
|
+
window.location.reload();
|
223
|
+
},
|
224
|
+
|
225
|
+
changeDate(date) {
|
226
|
+
const params = new URLSearchParams(window.location.search);
|
227
|
+
params.set('date', date);
|
228
|
+
window.location.search = params.toString();
|
229
|
+
},
|
230
|
+
|
231
|
+
clearFilters() {
|
232
|
+
this.filters.operation = '';
|
233
|
+
this.filters.table = '';
|
234
|
+
this.filters.minDuration = '';
|
235
|
+
this.filters.startTime = '';
|
236
|
+
this.filters.endTime = '';
|
237
|
+
}
|
238
|
+
}
|
239
|
+
}
|
240
|
+
</script>
|