dbwatcher 0.1.5 → 1.0.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 +2 -2
- data/app/controllers/dbwatcher/base_controller.rb +95 -0
- data/app/controllers/dbwatcher/dashboard_controller.rb +12 -0
- data/app/controllers/dbwatcher/queries_controller.rb +24 -0
- data/app/controllers/dbwatcher/sessions_controller.rb +15 -20
- data/app/controllers/dbwatcher/tables_controller.rb +38 -0
- data/app/helpers/dbwatcher/application_helper.rb +103 -0
- data/app/helpers/dbwatcher/formatting_helper.rb +108 -0
- data/app/helpers/dbwatcher/session_helper.rb +27 -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/index.html.erb +120 -27
- data/app/views/dbwatcher/sessions/show.html.erb +326 -129
- 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 +375 -26
- data/config/routes.rb +17 -3
- data/lib/dbwatcher/configuration.rb +9 -1
- data/lib/dbwatcher/engine.rb +12 -7
- data/lib/dbwatcher/logging.rb +72 -0
- data/lib/dbwatcher/services/dashboard_data_aggregator.rb +121 -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 +172 -0
- data/lib/dbwatcher/storage/api/query_api.rb +95 -0
- data/lib/dbwatcher/storage/api/session_api.rb +134 -0
- data/lib/dbwatcher/storage/api/table_api.rb +86 -0
- data/lib/dbwatcher/storage/base_storage.rb +113 -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 +12 -2
- metadata +47 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ea9aad773959e5c5e862792305df463112927844dd7d1473130b84c0d23583e8
|
4
|
+
data.tar.gz: 89bbc03d80d56b2a478657a9b3e24b9b6ce83439fd2342a19be4d2be4147d2e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b12cccc60ec47f58f6fb779106d38d0d915df0b49c06e10a35456abcd2c7e18874a1e5db9a0428a380a5169e62c425f4f18a092c85250858b52a237c687b9f2
|
7
|
+
data.tar.gz: a95e7b8a3674d232a99668d3f1c9051b79ffa97237412bd14563e0229761d8b51aaccb27e3ae7518e52a3375afc5c016b766b5453588a6ffc24f040ffced4254
|
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
#
|
1
|
+
# DB Watcher 🔍
|
2
2
|
|
3
3
|
Track and visualize database changes in your Rails application for easier debugging and development.
|
4
4
|
|
5
|
-
|
5
|
+
DB Watcher is a powerful Rails gem that captures, stores, and visualizes all database operations in your application. Perfect for debugging complex data flows, understanding application behavior, and optimizing database performance.
|
6
6
|
|
7
7
|
## ✨ Features
|
8
8
|
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
# Base controller for all Dbwatcher controllers
|
5
|
+
# Provides common functionality and configuration for the entire engine
|
6
|
+
class BaseController < ActionController::Base
|
7
|
+
protect_from_forgery with: :exception
|
8
|
+
layout "dbwatcher/application"
|
9
|
+
|
10
|
+
before_action :set_current_time
|
11
|
+
before_action :log_request_info
|
12
|
+
|
13
|
+
# Common error handling
|
14
|
+
rescue_from StandardError, with: :handle_error
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
# Set current time for consistent timestamp usage across views
|
19
|
+
def set_current_time
|
20
|
+
@current_time = Time.current
|
21
|
+
end
|
22
|
+
|
23
|
+
# Log request information for debugging
|
24
|
+
def log_request_info
|
25
|
+
return unless Rails.logger
|
26
|
+
|
27
|
+
Rails.logger.info "DBWatcher Request: #{request.method} #{request.path} - Controller: #{self.class.name}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Common error handling for all controllers
|
31
|
+
def handle_error(exception)
|
32
|
+
Rails.logger.error "DBWatcher Error in #{self.class.name}##{action_name}: #{exception.message}"
|
33
|
+
Rails.logger.error exception.backtrace.join("\n") if Rails.env.development?
|
34
|
+
|
35
|
+
respond_to do |format|
|
36
|
+
format.html do
|
37
|
+
flash[:error] = "An error occurred while processing your request."
|
38
|
+
redirect_to root_path
|
39
|
+
end
|
40
|
+
format.json do
|
41
|
+
render json: { error: "Internal server error" }, status: :internal_server_error
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Helper method for safely extracting data from hashes with symbol/string keys
|
47
|
+
def safe_extract(data, key)
|
48
|
+
return nil unless data.is_a?(Hash)
|
49
|
+
|
50
|
+
data[key] || data[key.to_s]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Helper method for formatting timestamps consistently
|
54
|
+
def format_timestamp(timestamp_str)
|
55
|
+
return "N/A" unless timestamp_str
|
56
|
+
|
57
|
+
Time.parse(timestamp_str).strftime("%H:%M:%S")
|
58
|
+
rescue ArgumentError
|
59
|
+
"N/A"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Make helper methods available to views
|
63
|
+
helper_method :format_timestamp, :safe_extract
|
64
|
+
|
65
|
+
# Helper method for rendering JSON responses with consistent structure
|
66
|
+
def render_json_response(data, status: :ok)
|
67
|
+
render json: {
|
68
|
+
status: status,
|
69
|
+
data: data,
|
70
|
+
timestamp: @current_time.iso8601
|
71
|
+
}, status: status
|
72
|
+
end
|
73
|
+
|
74
|
+
# Helper method for handling not found resources
|
75
|
+
def handle_not_found(resource_name, redirect_path)
|
76
|
+
Rails.logger.warn "#{self.class.name}##{action_name}: #{resource_name} not found for ID: #{params[:id]}"
|
77
|
+
|
78
|
+
respond_to do |format|
|
79
|
+
format.html do
|
80
|
+
redirect_to redirect_path, alert: "#{resource_name} not found"
|
81
|
+
end
|
82
|
+
format.json do
|
83
|
+
render json: { error: "#{resource_name} not found" }, status: :not_found
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Helper method for clearing storage with consistent messaging
|
89
|
+
def clear_storage_with_message(storage_method, resource_name, redirect_path)
|
90
|
+
cleared_count = storage_method.call
|
91
|
+
redirect_to redirect_path,
|
92
|
+
notice: "#{resource_name} cleared (#{cleared_count} files removed)"
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
class DashboardController < BaseController
|
5
|
+
def index
|
6
|
+
dashboard_data = Dbwatcher::Services::DashboardDataAggregator.call
|
7
|
+
@recent_sessions = dashboard_data[:recent_sessions]
|
8
|
+
@active_tables = dashboard_data[:active_tables]
|
9
|
+
@query_stats = dashboard_data[:query_stats]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
class QueriesController < BaseController
|
5
|
+
def index
|
6
|
+
@date = params[:date] || Date.current.strftime("%Y-%m-%d")
|
7
|
+
queries = Storage.queries.for_date(@date).all
|
8
|
+
@queries = Dbwatcher::Services::QueryFilterProcessor.call(queries, params)
|
9
|
+
|
10
|
+
respond_to do |format|
|
11
|
+
format.html
|
12
|
+
format.json { render json: @queries }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def clear
|
17
|
+
clear_storage_with_message(
|
18
|
+
-> { Storage.query_storage.clear_all },
|
19
|
+
"SQL query logs",
|
20
|
+
queries_path
|
21
|
+
)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,16 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Dbwatcher
|
4
|
-
class SessionsController <
|
5
|
-
protect_from_forgery with: :exception
|
6
|
-
layout "dbwatcher/application"
|
7
|
-
|
4
|
+
class SessionsController < BaseController
|
8
5
|
def index
|
9
|
-
@sessions = Storage.
|
6
|
+
@sessions = Storage.sessions.all
|
10
7
|
end
|
11
8
|
|
12
9
|
def show
|
13
|
-
|
10
|
+
Rails.logger.info "SessionsController#show: Loading session with ID: #{params[:id]}"
|
11
|
+
@session = Storage.sessions.find(params[:id])
|
12
|
+
Rails.logger.info "SessionsController#show: Loaded session: #{@session.inspect}"
|
13
|
+
|
14
|
+
return handle_not_found("Session", sessions_path) unless @session
|
15
|
+
|
16
|
+
@tables_summary = Storage.sessions.build_tables_summary(@session)
|
17
|
+
Rails.logger.info "SessionsController#show: Tables summary: #{@tables_summary.inspect}"
|
14
18
|
|
15
19
|
respond_to do |format|
|
16
20
|
format.html
|
@@ -18,21 +22,12 @@ module Dbwatcher
|
|
18
22
|
end
|
19
23
|
end
|
20
24
|
|
21
|
-
def
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
# Helper method to safely get the sessions path
|
29
|
-
def sessions_index_path
|
30
|
-
if respond_to?(:sessions_path)
|
25
|
+
def clear
|
26
|
+
clear_storage_with_message(
|
27
|
+
-> { Storage.session_storage.clear_all },
|
28
|
+
"All sessions",
|
31
29
|
sessions_path
|
32
|
-
|
33
|
-
"/dbwatcher"
|
34
|
-
end
|
30
|
+
)
|
35
31
|
end
|
36
|
-
helper_method :sessions_index_path
|
37
32
|
end
|
38
33
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
class TablesController < BaseController
|
5
|
+
def index
|
6
|
+
@tables = Dbwatcher::Services::TableStatisticsCollector.call
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@table_name = params[:id]
|
11
|
+
@changes = Storage.tables.changes_for(@table_name).all
|
12
|
+
@sessions = @changes.map { |c| c[:session_id] }.uniq
|
13
|
+
|
14
|
+
respond_to do |format|
|
15
|
+
format.html
|
16
|
+
format.json { render json: @changes }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def changes
|
21
|
+
@table_name = params[:id]
|
22
|
+
@changes = Storage.tables.changes_for(@table_name).all
|
23
|
+
@sessions = extract_session_ids(@changes)
|
24
|
+
@records = group_changes_by_record(@changes)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# These remain as they are simple data extraction helpers
|
30
|
+
def extract_session_ids(changes)
|
31
|
+
changes.map { |c| c[:session_id] }.uniq
|
32
|
+
end
|
33
|
+
|
34
|
+
def group_changes_by_record(changes)
|
35
|
+
changes.group_by { |c| c[:record_id] }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dbwatcher
|
4
|
+
module ApplicationHelper
|
5
|
+
include FormattingHelper
|
6
|
+
include SessionHelper
|
7
|
+
|
8
|
+
# Common view helpers for templates
|
9
|
+
|
10
|
+
# Create icon HTML for stats cards
|
11
|
+
def stats_icon(type)
|
12
|
+
icon_html = icon_definitions[type.to_sym]
|
13
|
+
icon_html&.html_safe
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Icon definitions for stats cards
|
19
|
+
def icon_definitions
|
20
|
+
{
|
21
|
+
sessions: sessions_icon_svg,
|
22
|
+
tables: tables_icon_svg,
|
23
|
+
queries: queries_icon_svg,
|
24
|
+
performance: performance_icon_svg
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def sessions_icon_svg
|
29
|
+
<<~SVG
|
30
|
+
<svg class="w-4 h-4 text-blue-medium" fill="currentColor" viewBox="0 0 20 20">
|
31
|
+
<path d="M10 12a2 2 0 100-4 2 2 0 000 4z"/>
|
32
|
+
<path fill-rule="evenodd" d="M.458 10C1.732 5.943 5.522 3 10 3s8.268 2.943 9.542 7c-1.274 4.057-5.064 7-9.542 7S1.732 14.057.458 10zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd"/>
|
33
|
+
</svg>
|
34
|
+
SVG
|
35
|
+
end
|
36
|
+
|
37
|
+
def tables_icon_svg
|
38
|
+
<<~SVG
|
39
|
+
<svg class="w-4 h-4 text-gold-dark" fill="currentColor" viewBox="0 0 20 20">
|
40
|
+
<path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11 4a1 1 0 10-2 0v4a1 1 0 102 0V7zm-3 1a1 1 0 10-2 0v3a1 1 0 102 0V8zM8 9a1 1 0 00-2 0v2a1 1 0 102 0V9z" clip-rule="evenodd"/>
|
41
|
+
</svg>
|
42
|
+
SVG
|
43
|
+
end
|
44
|
+
|
45
|
+
def queries_icon_svg
|
46
|
+
<<~SVG
|
47
|
+
<svg class="w-4 h-4 text-blue-light" fill="currentColor" viewBox="0 0 20 20">
|
48
|
+
<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"/>
|
49
|
+
</svg>
|
50
|
+
SVG
|
51
|
+
end
|
52
|
+
|
53
|
+
def performance_icon_svg
|
54
|
+
<<~SVG
|
55
|
+
<svg class="w-4 h-4 text-gold-light" fill="currentColor" viewBox="0 0 20 20">
|
56
|
+
<path fill-rule="evenodd"
|
57
|
+
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"
|
58
|
+
clip-rule="evenodd"/>
|
59
|
+
</svg>
|
60
|
+
SVG
|
61
|
+
end
|
62
|
+
|
63
|
+
public
|
64
|
+
|
65
|
+
# Generate operation badges for tables
|
66
|
+
def operation_badges
|
67
|
+
content_tag(:div, class: "flex gap-1 justify-center") do
|
68
|
+
[
|
69
|
+
content_tag(:span, "I", class: "badge badge-insert", title: "Inserts"),
|
70
|
+
content_tag(:span, "U", class: "badge badge-update", title: "Updates"),
|
71
|
+
content_tag(:span, "D", class: "badge badge-delete", title: "Deletes")
|
72
|
+
].join.html_safe
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Create action buttons with consistent styling
|
77
|
+
def action_button(text, path, color: "blue")
|
78
|
+
link_to text, path, class: "px-3 py-1 text-xs bg-#{color}-600 text-white rounded hover:bg-#{color}-700"
|
79
|
+
end
|
80
|
+
|
81
|
+
# Safe value extraction with fallback
|
82
|
+
def safe_value(data, key, fallback = "N/A")
|
83
|
+
return fallback unless data.is_a?(Hash)
|
84
|
+
|
85
|
+
data[key] || data[key.to_s] || fallback
|
86
|
+
end
|
87
|
+
|
88
|
+
# Format count with proper fallback
|
89
|
+
def format_count(value)
|
90
|
+
value.to_i.to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
# Create empty state message
|
94
|
+
def empty_state(message, icon: nil)
|
95
|
+
content_tag(:div, class: "text-center py-8 text-gray-500") do
|
96
|
+
content = []
|
97
|
+
content << icon if icon
|
98
|
+
content << content_tag(:p, message, class: "text-xs")
|
99
|
+
content.join.html_safe
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
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,27 @@
|
|
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 (truncated)
|
21
|
+
def display_session_id(id)
|
22
|
+
return "N/A" unless id
|
23
|
+
|
24
|
+
"#{id[0..7]}..."
|
25
|
+
end
|
26
|
+
end
|
27
|
+
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>
|