solid_log-ui 0.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +295 -0
- data/Rakefile +12 -0
- data/app/assets/javascripts/application.js +6 -0
- data/app/assets/javascripts/solid_log/checkbox_dropdown.js +171 -0
- data/app/assets/javascripts/solid_log/filter_state.js +138 -0
- data/app/assets/javascripts/solid_log/jump_to_live.js +119 -0
- data/app/assets/javascripts/solid_log/live_tail.js +476 -0
- data/app/assets/javascripts/solid_log/live_tail.js.bak +270 -0
- data/app/assets/javascripts/solid_log/log_filters.js +37 -0
- data/app/assets/javascripts/solid_log/stream_scroll.js +195 -0
- data/app/assets/javascripts/solid_log/timeline_histogram.js +162 -0
- data/app/assets/javascripts/solid_log/toast.js +50 -0
- data/app/assets/stylesheets/solid_log/application.css +1329 -0
- data/app/assets/stylesheets/solid_log/components.css +1506 -0
- data/app/assets/stylesheets/solid_log/mission_control.css +398 -0
- data/app/channels/solid_log/ui/application_cable/channel.rb +8 -0
- data/app/channels/solid_log/ui/application_cable/connection.rb +10 -0
- data/app/channels/solid_log/ui/log_stream_channel.rb +132 -0
- data/app/controllers/solid_log/ui/base_controller.rb +122 -0
- data/app/controllers/solid_log/ui/dashboard_controller.rb +32 -0
- data/app/controllers/solid_log/ui/entries_controller.rb +34 -0
- data/app/controllers/solid_log/ui/fields_controller.rb +57 -0
- data/app/controllers/solid_log/ui/streams_controller.rb +204 -0
- data/app/controllers/solid_log/ui/timelines_controller.rb +29 -0
- data/app/controllers/solid_log/ui/tokens_controller.rb +46 -0
- data/app/helpers/solid_log/ui/application_helper.rb +99 -0
- data/app/helpers/solid_log/ui/dashboard_helper.rb +46 -0
- data/app/helpers/solid_log/ui/entries_helper.rb +16 -0
- data/app/helpers/solid_log/ui/timeline_helper.rb +39 -0
- data/app/services/solid_log/ui/live_tail_broadcaster.rb +81 -0
- data/app/views/layouts/solid_log/ui/application.html.erb +53 -0
- data/app/views/solid_log/ui/dashboard/index.html.erb +178 -0
- data/app/views/solid_log/ui/entries/show.html.erb +132 -0
- data/app/views/solid_log/ui/fields/index.html.erb +133 -0
- data/app/views/solid_log/ui/shared/_checkbox_dropdown.html.erb +64 -0
- data/app/views/solid_log/ui/shared/_multiselect_filter.html.erb +37 -0
- data/app/views/solid_log/ui/shared/_toast.html.erb +7 -0
- data/app/views/solid_log/ui/shared/_toast_message.html.erb +30 -0
- data/app/views/solid_log/ui/streams/_filter_form.html.erb +207 -0
- data/app/views/solid_log/ui/streams/_footer.html.erb +37 -0
- data/app/views/solid_log/ui/streams/_log_entries.html.erb +5 -0
- data/app/views/solid_log/ui/streams/_log_row.html.erb +5 -0
- data/app/views/solid_log/ui/streams/_log_row_compact.html.erb +37 -0
- data/app/views/solid_log/ui/streams/_log_row_expanded.html.erb +67 -0
- data/app/views/solid_log/ui/streams/_log_stream_content.html.erb +8 -0
- data/app/views/solid_log/ui/streams/_timeline.html.erb +68 -0
- data/app/views/solid_log/ui/streams/index.html.erb +22 -0
- data/app/views/solid_log/ui/timelines/show_job.html.erb +78 -0
- data/app/views/solid_log/ui/timelines/show_request.html.erb +88 -0
- data/app/views/solid_log/ui/tokens/index.html.erb +95 -0
- data/app/views/solid_log/ui/tokens/new.html.erb +47 -0
- data/config/importmap.rb +15 -0
- data/config/routes.rb +27 -0
- data/lib/solid_log/ui/api_client.rb +117 -0
- data/lib/solid_log/ui/configuration.rb +99 -0
- data/lib/solid_log/ui/data_source.rb +146 -0
- data/lib/solid_log/ui/engine.rb +76 -0
- data/lib/solid_log/ui/version.rb +5 -0
- data/lib/solid_log/ui.rb +27 -0
- data/lib/solid_log-ui.rb +2 -0
- metadata +290 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<title>SolidLog - Log Management</title>
|
|
5
|
+
<%= csrf_meta_tags %>
|
|
6
|
+
<%= csp_meta_tag %>
|
|
7
|
+
|
|
8
|
+
<%= yield :head %>
|
|
9
|
+
|
|
10
|
+
<%= stylesheet_link_tag "solid_log/application", "data-turbo-track": "reload" %>
|
|
11
|
+
<%= stylesheet_link_tag "solid_log/components", "data-turbo-track": "reload" %>
|
|
12
|
+
<%= javascript_importmap_tags %>
|
|
13
|
+
<%= javascript_include_tag "solid_log/stream_scroll", defer: true %>
|
|
14
|
+
<%= javascript_include_tag "solid_log/live_tail", defer: true %>
|
|
15
|
+
<%= javascript_include_tag "solid_log/jump_to_live", defer: true %>
|
|
16
|
+
<%= javascript_include_tag "solid_log/checkbox_dropdown", defer: true %>
|
|
17
|
+
<%= javascript_include_tag "solid_log/timeline_histogram", defer: true %>
|
|
18
|
+
<%= javascript_include_tag "solid_log/log_filters", defer: true %>
|
|
19
|
+
<%= javascript_include_tag "solid_log/filter_state", defer: true %>
|
|
20
|
+
<%= javascript_include_tag "solid_log/toast", defer: true %>
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
22
|
+
</head>
|
|
23
|
+
<body data-streams-path="<%= streams_path %>">
|
|
24
|
+
<nav class="top-nav">
|
|
25
|
+
<div class="nav-brand">
|
|
26
|
+
<%= link_to root_path, class: "brand-link" do %>
|
|
27
|
+
<strong>SolidLog</strong>
|
|
28
|
+
<% end %>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="nav-links">
|
|
31
|
+
<%= link_to "Dashboard", dashboard_path, class: nav_link_class(dashboard_path) %>
|
|
32
|
+
<%= link_to "Streams", streams_path, class: nav_link_class(streams_path) %>
|
|
33
|
+
<%= link_to "Fields", fields_path, class: nav_link_class(fields_path) %>
|
|
34
|
+
<%= link_to "Tokens", tokens_path, class: nav_link_class(tokens_path) %>
|
|
35
|
+
</div>
|
|
36
|
+
</nav>
|
|
37
|
+
|
|
38
|
+
<main class="main-content">
|
|
39
|
+
<%= yield %>
|
|
40
|
+
</main>
|
|
41
|
+
|
|
42
|
+
<%= render "solid_log/ui/shared/toast" %>
|
|
43
|
+
|
|
44
|
+
<footer class="footer">
|
|
45
|
+
<% if content_for?(:stream_footer) %>
|
|
46
|
+
<%= yield :stream_footer %>
|
|
47
|
+
<% end %>
|
|
48
|
+
<div class="footer-content">
|
|
49
|
+
<span>SolidLog <%= SolidLog::VERSION rescue "1.0.0" %></span>
|
|
50
|
+
</div>
|
|
51
|
+
</footer>
|
|
52
|
+
</body>
|
|
53
|
+
</html>
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
<div class="dashboard">
|
|
2
|
+
<div class="page-header">
|
|
3
|
+
<h1>SolidLog Dashboard</h1>
|
|
4
|
+
<p class="subtitle">Real-time log monitoring and analytics</p>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
<div class="stats-grid">
|
|
8
|
+
<div class="stat-card">
|
|
9
|
+
<div class="stat-label">Total Log Entries</div>
|
|
10
|
+
<div class="stat-value"><%= format_count(@health_metrics[:storage][:total_entries]) %></div>
|
|
11
|
+
<div class="stat-footer">
|
|
12
|
+
<%= link_to "View all →", streams_path, class: "stat-link" %>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div class="stat-card">
|
|
17
|
+
<div class="stat-label">Parse Backlog</div>
|
|
18
|
+
<div class="stat-value"><%= format_count(@health_metrics[:parsing][:unparsed_count]) %></div>
|
|
19
|
+
<div class="stat-footer">
|
|
20
|
+
<%= health_status_badge(@health_metrics[:parsing][:unparsed_count]) %>
|
|
21
|
+
<span class="stat-text"><%= @health_metrics[:parsing][:parse_backlog_percentage] %>%</span>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="stat-card">
|
|
26
|
+
<div class="stat-label">Error Rate (1h)</div>
|
|
27
|
+
<div class="stat-value error-count"><%= @health_metrics[:performance][:error_rate] %>%</div>
|
|
28
|
+
<div class="stat-footer">
|
|
29
|
+
<%= link_to "View errors →", streams_path(filters: { levels: ["error", "fatal"] }), class: "stat-link" %>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<div class="stat-card">
|
|
34
|
+
<div class="stat-label">Database Size</div>
|
|
35
|
+
<div class="stat-value"><%= @health_metrics[:storage][:database_size] %></div>
|
|
36
|
+
<div class="stat-footer">
|
|
37
|
+
<span class="stat-text"><%= format_count(@health_metrics[:storage][:total_fields]) %> fields tracked</span>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="stat-card">
|
|
42
|
+
<div class="stat-label">Avg Response Time (1h)</div>
|
|
43
|
+
<div class="stat-value"><%= @health_metrics[:performance][:avg_duration] %>ms</div>
|
|
44
|
+
<div class="stat-footer">
|
|
45
|
+
<span class="stat-text">Performance metric</span>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="stat-card">
|
|
50
|
+
<div class="stat-label">Ingested Today</div>
|
|
51
|
+
<div class="stat-value"><%= format_count(@health_metrics[:ingestion][:today_raw]) %></div>
|
|
52
|
+
<div class="stat-footer">
|
|
53
|
+
<span class="stat-text">Last hour: <%= format_count(@health_metrics[:ingestion][:last_hour_raw]) %></span>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="dashboard-row">
|
|
59
|
+
<div class="dashboard-col-8">
|
|
60
|
+
<div class="card">
|
|
61
|
+
<div class="card-header">
|
|
62
|
+
<h2>Recent Errors</h2>
|
|
63
|
+
</div>
|
|
64
|
+
<div class="card-body">
|
|
65
|
+
<% if @recent_errors.empty? %>
|
|
66
|
+
<p class="empty-state">No recent errors. System healthy! ✓</p>
|
|
67
|
+
<% else %>
|
|
68
|
+
<div class="log-list">
|
|
69
|
+
<% @recent_errors.each do |entry| %>
|
|
70
|
+
<div class="log-item">
|
|
71
|
+
<div class="log-item-header">
|
|
72
|
+
<%= level_badge(entry.level) %>
|
|
73
|
+
<span class="log-time"><%= time_ago_in_words(entry.created_at) %> ago</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div class="log-message">
|
|
76
|
+
<%= link_to truncate_message(entry.message, length: 150), entry_path(entry) %>
|
|
77
|
+
</div>
|
|
78
|
+
<% if entry.controller.present? %>
|
|
79
|
+
<div class="log-meta">
|
|
80
|
+
<%= entry.controller %>#<%= entry.action %>
|
|
81
|
+
<% if entry.status_code.present? %>
|
|
82
|
+
- <%= http_status_badge(entry.status_code) %>
|
|
83
|
+
<% end %>
|
|
84
|
+
</div>
|
|
85
|
+
<% end %>
|
|
86
|
+
</div>
|
|
87
|
+
<% end %>
|
|
88
|
+
</div>
|
|
89
|
+
<% end %>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
<div class="dashboard-col-4">
|
|
95
|
+
<div class="card">
|
|
96
|
+
<div class="card-header">
|
|
97
|
+
<h2>Log Level Distribution</h2>
|
|
98
|
+
</div>
|
|
99
|
+
<div class="card-body">
|
|
100
|
+
<% if @log_level_distribution.empty? %>
|
|
101
|
+
<p class="empty-state">No log entries yet</p>
|
|
102
|
+
<% else %>
|
|
103
|
+
<div class="distribution-list">
|
|
104
|
+
<% total = @log_level_distribution.values.sum %>
|
|
105
|
+
<% @log_level_distribution.sort_by { |k, v| -v }.each do |level, count| %>
|
|
106
|
+
<div class="distribution-item">
|
|
107
|
+
<div class="distribution-header">
|
|
108
|
+
<%= level_badge(level) %>
|
|
109
|
+
<span class="distribution-count"><%= format_count(count) %></span>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="distribution-bar-container">
|
|
112
|
+
<div class="distribution-bar" style="width: <%= (count.to_f / total * 100).round(1) %>%"></div>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="distribution-percentage">
|
|
115
|
+
<%= format_percentage(count, total) %>
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
<% end %>
|
|
119
|
+
</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<div class="card" style="margin-top: 1.5rem;">
|
|
125
|
+
<div class="card-header">
|
|
126
|
+
<h2>Quick Stats</h2>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="card-body">
|
|
129
|
+
<div class="quick-stats">
|
|
130
|
+
<div class="quick-stat">
|
|
131
|
+
<span class="quick-stat-label">Oldest Entry</span>
|
|
132
|
+
<span class="quick-stat-value">
|
|
133
|
+
<%= @health_metrics[:storage][:oldest_entry] ? time_ago_or_never(@health_metrics[:storage][:oldest_entry]) : "N/A" %>
|
|
134
|
+
</span>
|
|
135
|
+
</div>
|
|
136
|
+
<div class="quick-stat">
|
|
137
|
+
<span class="quick-stat-label">Latest Entry</span>
|
|
138
|
+
<span class="quick-stat-value">
|
|
139
|
+
<%= @health_metrics[:storage][:newest_entry] ? time_ago_or_never(@health_metrics[:storage][:newest_entry]) : "N/A" %>
|
|
140
|
+
</span>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="quick-stat">
|
|
143
|
+
<span class="quick-stat-label">Cache Entries</span>
|
|
144
|
+
<span class="quick-stat-value"><%= format_count(@health_metrics[:performance][:cache_entries]) %></span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<% if @field_recommendations.any? %>
|
|
151
|
+
<div class="card" style="margin-top: 1.5rem;">
|
|
152
|
+
<div class="card-header">
|
|
153
|
+
<h2>Field Promotion Recommendations</h2>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="card-body">
|
|
156
|
+
<div class="recommendation-list">
|
|
157
|
+
<% @field_recommendations.each do |rec| %>
|
|
158
|
+
<div class="recommendation-item">
|
|
159
|
+
<div class="recommendation-header">
|
|
160
|
+
<code><%= rec[:field].name %></code>
|
|
161
|
+
<span class="badge badge-warning">Priority: <%= rec[:priority] %>/10</span>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="recommendation-meta">
|
|
164
|
+
<%= rec[:field].field_type %> • <%= format_count(rec[:usage_count]) %> uses
|
|
165
|
+
</div>
|
|
166
|
+
<div class="recommendation-action">
|
|
167
|
+
<%= rec[:recommendation] %>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<% end %>
|
|
171
|
+
</div>
|
|
172
|
+
<%= link_to "View All Fields →", fields_path, class: "btn btn-secondary btn-block", style: "margin-top: 1rem;" %>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<% end %>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<div class="entry-detail">
|
|
2
|
+
<div class="page-header">
|
|
3
|
+
<h1>Log Entry Details</h1>
|
|
4
|
+
<div class="page-actions">
|
|
5
|
+
<%= link_to "← Back to Stream", streams_path, class: "btn btn-secondary" %>
|
|
6
|
+
</div>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="entry-detail-content">
|
|
10
|
+
<div class="card">
|
|
11
|
+
<div class="card-header">
|
|
12
|
+
<h2>Metadata</h2>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="card-body">
|
|
15
|
+
<dl class="metadata-list">
|
|
16
|
+
<dt>Timestamp</dt>
|
|
17
|
+
<dd><%= @entry.created_at.iso8601 %> (<%= time_ago_in_words(@entry.created_at) %> ago)</dd>
|
|
18
|
+
|
|
19
|
+
<dt>Level</dt>
|
|
20
|
+
<dd>
|
|
21
|
+
<%= level_badge(@entry.level) %>
|
|
22
|
+
<%= link_to "Filter by #{@entry.level} →", streams_path(filters: { levels: [@entry.level] }), class: "metadata-link" %>
|
|
23
|
+
</dd>
|
|
24
|
+
|
|
25
|
+
<% if @entry.app.present? %>
|
|
26
|
+
<dt>Application</dt>
|
|
27
|
+
<dd>
|
|
28
|
+
<%= @entry.app %>
|
|
29
|
+
<%= link_to "Filter by app →", streams_path(filters: { app: [@entry.app] }), class: "metadata-link" %>
|
|
30
|
+
</dd>
|
|
31
|
+
<% end %>
|
|
32
|
+
|
|
33
|
+
<% if @entry.env.present? %>
|
|
34
|
+
<dt>Environment</dt>
|
|
35
|
+
<dd>
|
|
36
|
+
<%= @entry.env %>
|
|
37
|
+
<%= link_to "Filter by env →", streams_path(filters: { env: [@entry.env] }), class: "metadata-link" %>
|
|
38
|
+
</dd>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<% if @entry.request_id.present? %>
|
|
42
|
+
<dt>Request ID</dt>
|
|
43
|
+
<dd>
|
|
44
|
+
<%= @entry.request_id %>
|
|
45
|
+
<%= link_to "View timeline →", request_timeline_path(@entry.request_id), class: "metadata-link" %>
|
|
46
|
+
<%= link_to "Filter →", streams_path(filters: { request_id: @entry.request_id }), class: "metadata-link" %>
|
|
47
|
+
</dd>
|
|
48
|
+
<% end %>
|
|
49
|
+
|
|
50
|
+
<% if @entry.job_id.present? %>
|
|
51
|
+
<dt>Job ID</dt>
|
|
52
|
+
<dd>
|
|
53
|
+
<%= @entry.job_id %>
|
|
54
|
+
<%= link_to "View timeline →", job_timeline_path(@entry.job_id), class: "metadata-link" %>
|
|
55
|
+
<%= link_to "Filter →", streams_path(filters: { job_id: @entry.job_id }), class: "metadata-link" %>
|
|
56
|
+
</dd>
|
|
57
|
+
<% end %>
|
|
58
|
+
|
|
59
|
+
<% if @entry.controller.present? %>
|
|
60
|
+
<dt>Controller</dt>
|
|
61
|
+
<dd>
|
|
62
|
+
<%= @entry.controller %>#<%= @entry.action %>
|
|
63
|
+
<%= link_to "Filter by controller →", streams_path(filters: { controller: [@entry.controller] }), class: "metadata-link" %>
|
|
64
|
+
<% if @entry.action.present? %>
|
|
65
|
+
<%= link_to "Filter by action →", streams_path(filters: { action: [@entry.action] }), class: "metadata-link" %>
|
|
66
|
+
<% end %>
|
|
67
|
+
</dd>
|
|
68
|
+
<% end %>
|
|
69
|
+
|
|
70
|
+
<% if @entry.path.present? %>
|
|
71
|
+
<dt>HTTP Request</dt>
|
|
72
|
+
<dd>
|
|
73
|
+
<%= @entry.method %> <%= @entry.path %>
|
|
74
|
+
<%= link_to "Filter by path →", streams_path(filters: { path: [@entry.path] }), class: "metadata-link" %>
|
|
75
|
+
<% if @entry.method.present? %>
|
|
76
|
+
<%= link_to "Filter by method →", streams_path(filters: { method: [@entry.method] }), class: "metadata-link" %>
|
|
77
|
+
<% end %>
|
|
78
|
+
</dd>
|
|
79
|
+
<% end %>
|
|
80
|
+
|
|
81
|
+
<% if @entry.status_code.present? %>
|
|
82
|
+
<dt>Status Code</dt>
|
|
83
|
+
<dd>
|
|
84
|
+
<%= http_status_badge(@entry.status_code) %>
|
|
85
|
+
<%= link_to "Filter by status →", streams_path(filters: { status_code: [@entry.status_code.to_s] }), class: "metadata-link" %>
|
|
86
|
+
</dd>
|
|
87
|
+
<% end %>
|
|
88
|
+
|
|
89
|
+
<% if @entry.duration.present? %>
|
|
90
|
+
<dt>Duration</dt>
|
|
91
|
+
<dd><%= format_duration(@entry.duration) %></dd>
|
|
92
|
+
<% end %>
|
|
93
|
+
</dl>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<div class="card">
|
|
98
|
+
<div class="card-header">
|
|
99
|
+
<h2>Message</h2>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="card-body">
|
|
102
|
+
<pre class="log-message-full"><%= @entry.message %></pre>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<% if @entry.extra_fields.present? && @entry.extra_fields != "{}" %>
|
|
107
|
+
<div class="card">
|
|
108
|
+
<div class="card-header">
|
|
109
|
+
<h2>Additional Fields</h2>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="card-body">
|
|
112
|
+
<pre class="json-display"><%= pretty_json(@entry.extra_fields) %></pre>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
<% end %>
|
|
116
|
+
|
|
117
|
+
<% if @correlated_entries&.any? %>
|
|
118
|
+
<div class="card">
|
|
119
|
+
<div class="card-header">
|
|
120
|
+
<h2>Correlated Log Entries (<%= @correlated_entries.size %>)</h2>
|
|
121
|
+
</div>
|
|
122
|
+
<div class="card-body">
|
|
123
|
+
<div class="log-stream">
|
|
124
|
+
<% @correlated_entries.each do |correlated_entry| %>
|
|
125
|
+
<%= render "solid_log/ui/streams/log_row", entry: correlated_entry %>
|
|
126
|
+
<% end %>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<% end %>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<div class="fields-page">
|
|
2
|
+
<div class="page-header">
|
|
3
|
+
<div>
|
|
4
|
+
<h1>Field Registry</h1>
|
|
5
|
+
<p class="subtitle">Track and manage dynamic log fields</p>
|
|
6
|
+
</div>
|
|
7
|
+
<div class="page-actions">
|
|
8
|
+
<%= link_to "← Dashboard", dashboard_path, class: "btn btn-secondary" %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="fields-container">
|
|
13
|
+
<div class="fields-stats">
|
|
14
|
+
<div class="stat-card-inline">
|
|
15
|
+
<div class="stat-label">Total Fields</div>
|
|
16
|
+
<div class="stat-value"><%= @total_fields %></div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="stat-card-inline">
|
|
19
|
+
<div class="stat-label">Hot Fields (>1000 uses)</div>
|
|
20
|
+
<div class="stat-value"><%= @hot_fields.size %></div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="stat-card-inline">
|
|
23
|
+
<div class="stat-label">Promoted Fields</div>
|
|
24
|
+
<div class="stat-value"><%= @fields.count(&:promoted?) %></div>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<% if @fields.empty? %>
|
|
29
|
+
<div class="empty-state-large">
|
|
30
|
+
<h2>No fields tracked yet</h2>
|
|
31
|
+
<p>Fields will appear here automatically as logs are parsed.</p>
|
|
32
|
+
</div>
|
|
33
|
+
<% else %>
|
|
34
|
+
<div class="card">
|
|
35
|
+
<div class="card-header">
|
|
36
|
+
<h2>All Fields</h2>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="card-body no-padding">
|
|
39
|
+
<div class="table-responsive">
|
|
40
|
+
<table class="data-table">
|
|
41
|
+
<thead>
|
|
42
|
+
<tr>
|
|
43
|
+
<th>Field Name</th>
|
|
44
|
+
<th>Data Type</th>
|
|
45
|
+
<th>Filter Type</th>
|
|
46
|
+
<th>Usage Count</th>
|
|
47
|
+
<th>Last Seen</th>
|
|
48
|
+
<th>Status</th>
|
|
49
|
+
<th>Actions</th>
|
|
50
|
+
</tr>
|
|
51
|
+
</thead>
|
|
52
|
+
<tbody>
|
|
53
|
+
<% @fields.each do |field| %>
|
|
54
|
+
<tr class="<%= 'field-promoted' if field.promoted? %>">
|
|
55
|
+
<td class="field-name">
|
|
56
|
+
<code><%= field.name %></code>
|
|
57
|
+
</td>
|
|
58
|
+
<td><%= field.field_type %></td>
|
|
59
|
+
<td>
|
|
60
|
+
<%= form_with model: field, url: update_filter_type_field_path(field), method: :patch, class: "inline-form" do |f| %>
|
|
61
|
+
<%= f.select :filter_type,
|
|
62
|
+
SolidLog::Field::FILTER_TYPES.map { |ft| [ft.titleize, ft] },
|
|
63
|
+
{},
|
|
64
|
+
{ class: "form-select form-select-small",
|
|
65
|
+
onchange: "this.form.requestSubmit()" } %>
|
|
66
|
+
<% end %>
|
|
67
|
+
</td>
|
|
68
|
+
<td class="text-right">
|
|
69
|
+
<%= number_with_delimiter(field.usage_count) %>
|
|
70
|
+
<% if field.usage_count >= 1000 %>
|
|
71
|
+
<span class="badge badge-warning">HOT</span>
|
|
72
|
+
<% end %>
|
|
73
|
+
</td>
|
|
74
|
+
<td><%= time_ago_in_words(field.last_seen_at) %> ago</td>
|
|
75
|
+
<td>
|
|
76
|
+
<% if field.promoted? %>
|
|
77
|
+
<span class="badge badge-success">Promoted</span>
|
|
78
|
+
<% else %>
|
|
79
|
+
<span class="badge badge-secondary">Standard</span>
|
|
80
|
+
<% end %>
|
|
81
|
+
</td>
|
|
82
|
+
<td class="table-actions">
|
|
83
|
+
<% if field.promoted? %>
|
|
84
|
+
<%= button_to "Demote", demote_field_path(field), method: :post, class: "btn-link-small" %>
|
|
85
|
+
<% else %>
|
|
86
|
+
<%= button_to "Promote", promote_field_path(field), method: :post, class: "btn-link-small" %>
|
|
87
|
+
<% end %>
|
|
88
|
+
<%= button_to "Remove", field_path(field), method: :delete,
|
|
89
|
+
data: { confirm: "Remove field '#{field.name}' from registry?" },
|
|
90
|
+
class: "btn-link-small text-danger" %>
|
|
91
|
+
</td>
|
|
92
|
+
</tr>
|
|
93
|
+
<% end %>
|
|
94
|
+
</tbody>
|
|
95
|
+
</table>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div class="field-help">
|
|
101
|
+
<h3>About Field Promotion</h3>
|
|
102
|
+
<p>
|
|
103
|
+
<strong>Promoted fields</strong> are marked for optimization. High-usage fields (1000+ occurrences)
|
|
104
|
+
are good candidates for promotion to dedicated database columns for faster querying.
|
|
105
|
+
</p>
|
|
106
|
+
<p>
|
|
107
|
+
<strong>To complete the promotion process:</strong>
|
|
108
|
+
</p>
|
|
109
|
+
<ol>
|
|
110
|
+
<li>Click "Promote" to mark the field in the UI</li>
|
|
111
|
+
<li>Generate a migration: <code>rails g solid_log:promote_field field_name --type=string</code></li>
|
|
112
|
+
<li>Run the migration: <code>rails db:migrate</code></li>
|
|
113
|
+
</ol>
|
|
114
|
+
<p class="text-muted">
|
|
115
|
+
Note: The "Promote" button only marks fields for tracking. You must generate and run a migration
|
|
116
|
+
to create the actual database column for optimized queries.
|
|
117
|
+
</p>
|
|
118
|
+
|
|
119
|
+
<h3>Filter Types</h3>
|
|
120
|
+
<p>
|
|
121
|
+
Choose the appropriate filter type for how users will search this field:
|
|
122
|
+
</p>
|
|
123
|
+
<ul>
|
|
124
|
+
<li><strong>Multiselect</strong> - Select multiple values from a dropdown or checkboxes (e.g., status, category)</li>
|
|
125
|
+
<li><strong>Tokens</strong> - Enter comma-separated values for high-cardinality fields (e.g., user_id, session_id)</li>
|
|
126
|
+
<li><strong>Range</strong> - Filter by min/max values (e.g., price, age, timestamps)</li>
|
|
127
|
+
<li><strong>Exact</strong> - Select a single exact value (e.g., boolean flags)</li>
|
|
128
|
+
<li><strong>Contains</strong> - Text search with partial matching (e.g., names, descriptions)</li>
|
|
129
|
+
</ul>
|
|
130
|
+
</div>
|
|
131
|
+
<% end %>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<%# Checkbox dropdown component for multi-select filters
|
|
2
|
+
Required params:
|
|
3
|
+
- field_name: the form field name (e.g., "filters[controller]")
|
|
4
|
+
- options: array of values to display
|
|
5
|
+
- selected: array of currently selected values
|
|
6
|
+
- label: display label for the dropdown
|
|
7
|
+
Optional params:
|
|
8
|
+
- id_prefix: prefix for checkbox IDs (defaults to field_name)
|
|
9
|
+
%>
|
|
10
|
+
|
|
11
|
+
<%
|
|
12
|
+
id_prefix = local_assigns[:id_prefix] || field_name.gsub(/[\[\]]/, '_')
|
|
13
|
+
dropdown_id = "#{id_prefix}_dropdown"
|
|
14
|
+
selected_count = selected.count { |v| v.present? }
|
|
15
|
+
selected_items = selected.reject(&:blank?)
|
|
16
|
+
%>
|
|
17
|
+
|
|
18
|
+
<div class="checkbox-dropdown-portal" data-controller="checkbox-dropdown">
|
|
19
|
+
<button type="button" class="checkbox-dropdown-toggle" data-action="click->checkbox-dropdown#toggle" aria-expanded="false">
|
|
20
|
+
<div class="dropdown-toggle-content">
|
|
21
|
+
<% if selected_items.any? %>
|
|
22
|
+
<span class="dropdown-label"><%= selected_items.join(', ').truncate(50) %></span>
|
|
23
|
+
<% else %>
|
|
24
|
+
<span class="dropdown-label dropdown-label-placeholder"><%= label %></span>
|
|
25
|
+
<% end %>
|
|
26
|
+
<span class="badge badge-small" style="<%= 'display: none;' if selected_count == 0 %>"><%= selected_count %></span>
|
|
27
|
+
</div>
|
|
28
|
+
<span class="dropdown-arrow">▼</span>
|
|
29
|
+
</button>
|
|
30
|
+
|
|
31
|
+
<!-- Portal/Popover Menu (positioned absolutely across full sidebar width) -->
|
|
32
|
+
<div class="checkbox-dropdown-popover" data-checkbox-dropdown-target="menu" style="display: none;">
|
|
33
|
+
<div class="popover-header">
|
|
34
|
+
<h4><%= label %></h4>
|
|
35
|
+
<button type="button" class="popover-close" data-action="click->checkbox-dropdown#close">×</button>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div class="popover-search">
|
|
39
|
+
<input type="text"
|
|
40
|
+
placeholder="Search..."
|
|
41
|
+
data-action="input->checkbox-dropdown#filter"
|
|
42
|
+
data-checkbox-dropdown-target="search"
|
|
43
|
+
class="form-input form-input-small">
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="popover-options" data-checkbox-dropdown-target="options">
|
|
47
|
+
<% options.each do |option| %>
|
|
48
|
+
<div class="checkbox-item" data-checkbox-dropdown-target="option" data-value="<%= option.to_s.downcase %>">
|
|
49
|
+
<%= check_box_tag "#{field_name}[]", option,
|
|
50
|
+
selected.include?(option),
|
|
51
|
+
id: "#{id_prefix}_#{option.to_s.parameterize}",
|
|
52
|
+
data: { action: "change->checkbox-dropdown#updateCount" } %>
|
|
53
|
+
<%= label_tag "#{id_prefix}_#{option.to_s.parameterize}", option %>
|
|
54
|
+
</div>
|
|
55
|
+
<% end %>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="popover-footer">
|
|
59
|
+
<button type="button" class="btn btn-primary btn-small btn-block" data-action="click->checkbox-dropdown#close">
|
|
60
|
+
Done
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
<%# Multi-select filter component with automatic dropdown for N > 3
|
|
2
|
+
Required params:
|
|
3
|
+
- field_name: the form field name (e.g., "filters[controller]")
|
|
4
|
+
- options: array of values to display
|
|
5
|
+
- selected: array of currently selected values
|
|
6
|
+
- label: display label for the filter
|
|
7
|
+
Optional params:
|
|
8
|
+
- id_prefix: prefix for checkbox IDs (defaults to field_name)
|
|
9
|
+
- threshold: number of options at which to use dropdown (default: 4)
|
|
10
|
+
- form: form builder object (required if not using checkbox_tag directly)
|
|
11
|
+
%>
|
|
12
|
+
|
|
13
|
+
<%
|
|
14
|
+
threshold = local_assigns[:threshold] || 4
|
|
15
|
+
id_prefix = local_assigns[:id_prefix] || field_name.gsub(/[\[\]]/, '_')
|
|
16
|
+
form = local_assigns[:form]
|
|
17
|
+
%>
|
|
18
|
+
|
|
19
|
+
<% if options.size < threshold %>
|
|
20
|
+
<%# Show checkboxes inline for small lists %>
|
|
21
|
+
<% options.each do |option| %>
|
|
22
|
+
<div class="checkbox-item">
|
|
23
|
+
<%= check_box_tag "#{field_name}[]", option,
|
|
24
|
+
selected.include?(option),
|
|
25
|
+
id: "#{id_prefix}_#{option.to_s.parameterize}" %>
|
|
26
|
+
<%= label_tag "#{id_prefix}_#{option.to_s.parameterize}", option %>
|
|
27
|
+
</div>
|
|
28
|
+
<% end %>
|
|
29
|
+
<% else %>
|
|
30
|
+
<%# Use dropdown for large lists %>
|
|
31
|
+
<%= render "solid_log/ui/shared/checkbox_dropdown",
|
|
32
|
+
field_name: field_name,
|
|
33
|
+
options: options,
|
|
34
|
+
selected: selected,
|
|
35
|
+
label: label,
|
|
36
|
+
id_prefix: id_prefix %>
|
|
37
|
+
<% end %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%
|
|
2
|
+
type ||= 'info'
|
|
3
|
+
icons = {
|
|
4
|
+
'success' => '✓',
|
|
5
|
+
'info' => 'ℹ',
|
|
6
|
+
'warning' => '⚠',
|
|
7
|
+
'error' => '✕'
|
|
8
|
+
}
|
|
9
|
+
toast_id = "toast-#{SecureRandom.hex(4)}"
|
|
10
|
+
%>
|
|
11
|
+
<div class="toast toast-<%= type %>" id="<%= toast_id %>">
|
|
12
|
+
<span class="toast-icon"><%= icons[type] || icons['info'] %></span>
|
|
13
|
+
<span class="toast-message"><%= message %></span>
|
|
14
|
+
<button type="button" class="toast-close" onclick="this.closest('.toast').remove()" aria-label="Close">×</button>
|
|
15
|
+
</div>
|
|
16
|
+
<script>
|
|
17
|
+
(function() {
|
|
18
|
+
const toast = document.getElementById('<%= toast_id %>');
|
|
19
|
+
if (toast) {
|
|
20
|
+
// Auto-dismiss after 3 seconds
|
|
21
|
+
setTimeout(function() {
|
|
22
|
+
toast.classList.add('toast-dismissing');
|
|
23
|
+
// Remove after animation completes
|
|
24
|
+
setTimeout(function() {
|
|
25
|
+
toast.remove();
|
|
26
|
+
}, 300);
|
|
27
|
+
}, 3000);
|
|
28
|
+
}
|
|
29
|
+
})();
|
|
30
|
+
</script>
|