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.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +295 -0
  4. data/Rakefile +12 -0
  5. data/app/assets/javascripts/application.js +6 -0
  6. data/app/assets/javascripts/solid_log/checkbox_dropdown.js +171 -0
  7. data/app/assets/javascripts/solid_log/filter_state.js +138 -0
  8. data/app/assets/javascripts/solid_log/jump_to_live.js +119 -0
  9. data/app/assets/javascripts/solid_log/live_tail.js +476 -0
  10. data/app/assets/javascripts/solid_log/live_tail.js.bak +270 -0
  11. data/app/assets/javascripts/solid_log/log_filters.js +37 -0
  12. data/app/assets/javascripts/solid_log/stream_scroll.js +195 -0
  13. data/app/assets/javascripts/solid_log/timeline_histogram.js +162 -0
  14. data/app/assets/javascripts/solid_log/toast.js +50 -0
  15. data/app/assets/stylesheets/solid_log/application.css +1329 -0
  16. data/app/assets/stylesheets/solid_log/components.css +1506 -0
  17. data/app/assets/stylesheets/solid_log/mission_control.css +398 -0
  18. data/app/channels/solid_log/ui/application_cable/channel.rb +8 -0
  19. data/app/channels/solid_log/ui/application_cable/connection.rb +10 -0
  20. data/app/channels/solid_log/ui/log_stream_channel.rb +132 -0
  21. data/app/controllers/solid_log/ui/base_controller.rb +122 -0
  22. data/app/controllers/solid_log/ui/dashboard_controller.rb +32 -0
  23. data/app/controllers/solid_log/ui/entries_controller.rb +34 -0
  24. data/app/controllers/solid_log/ui/fields_controller.rb +57 -0
  25. data/app/controllers/solid_log/ui/streams_controller.rb +204 -0
  26. data/app/controllers/solid_log/ui/timelines_controller.rb +29 -0
  27. data/app/controllers/solid_log/ui/tokens_controller.rb +46 -0
  28. data/app/helpers/solid_log/ui/application_helper.rb +99 -0
  29. data/app/helpers/solid_log/ui/dashboard_helper.rb +46 -0
  30. data/app/helpers/solid_log/ui/entries_helper.rb +16 -0
  31. data/app/helpers/solid_log/ui/timeline_helper.rb +39 -0
  32. data/app/services/solid_log/ui/live_tail_broadcaster.rb +81 -0
  33. data/app/views/layouts/solid_log/ui/application.html.erb +53 -0
  34. data/app/views/solid_log/ui/dashboard/index.html.erb +178 -0
  35. data/app/views/solid_log/ui/entries/show.html.erb +132 -0
  36. data/app/views/solid_log/ui/fields/index.html.erb +133 -0
  37. data/app/views/solid_log/ui/shared/_checkbox_dropdown.html.erb +64 -0
  38. data/app/views/solid_log/ui/shared/_multiselect_filter.html.erb +37 -0
  39. data/app/views/solid_log/ui/shared/_toast.html.erb +7 -0
  40. data/app/views/solid_log/ui/shared/_toast_message.html.erb +30 -0
  41. data/app/views/solid_log/ui/streams/_filter_form.html.erb +207 -0
  42. data/app/views/solid_log/ui/streams/_footer.html.erb +37 -0
  43. data/app/views/solid_log/ui/streams/_log_entries.html.erb +5 -0
  44. data/app/views/solid_log/ui/streams/_log_row.html.erb +5 -0
  45. data/app/views/solid_log/ui/streams/_log_row_compact.html.erb +37 -0
  46. data/app/views/solid_log/ui/streams/_log_row_expanded.html.erb +67 -0
  47. data/app/views/solid_log/ui/streams/_log_stream_content.html.erb +8 -0
  48. data/app/views/solid_log/ui/streams/_timeline.html.erb +68 -0
  49. data/app/views/solid_log/ui/streams/index.html.erb +22 -0
  50. data/app/views/solid_log/ui/timelines/show_job.html.erb +78 -0
  51. data/app/views/solid_log/ui/timelines/show_request.html.erb +88 -0
  52. data/app/views/solid_log/ui/tokens/index.html.erb +95 -0
  53. data/app/views/solid_log/ui/tokens/new.html.erb +47 -0
  54. data/config/importmap.rb +15 -0
  55. data/config/routes.rb +27 -0
  56. data/lib/solid_log/ui/api_client.rb +117 -0
  57. data/lib/solid_log/ui/configuration.rb +99 -0
  58. data/lib/solid_log/ui/data_source.rb +146 -0
  59. data/lib/solid_log/ui/engine.rb +76 -0
  60. data/lib/solid_log/ui/version.rb +5 -0
  61. data/lib/solid_log/ui.rb +27 -0
  62. data/lib/solid_log-ui.rb +2 -0
  63. metadata +290 -0
@@ -0,0 +1,95 @@
1
+ <div class="tokens-page">
2
+ <div class="page-header">
3
+ <div>
4
+ <h1>API Tokens</h1>
5
+ <p class="subtitle">Manage authentication tokens for log ingestion</p>
6
+ </div>
7
+ <div class="page-actions">
8
+ <%= link_to "Create Token", new_token_path, class: "btn btn-primary" %>
9
+ <%= link_to "← Dashboard", dashboard_path, class: "btn btn-secondary" %>
10
+ </div>
11
+ </div>
12
+
13
+ <div class="tokens-container">
14
+ <% if flash[:token_plaintext].present? %>
15
+ <div class="alert alert-success">
16
+ <h3>Token Created Successfully!</h3>
17
+ <p><strong>IMPORTANT:</strong> This is the only time you'll see this token. Copy it now!</p>
18
+ <div class="token-display">
19
+ <code><%= flash[:token_plaintext] %></code>
20
+ <button onclick="copyToken('<%= flash[:token_plaintext] %>')" class="btn btn-secondary btn-small">
21
+ Copy
22
+ </button>
23
+ </div>
24
+ <p class="token-usage">
25
+ Use in Authorization header: <code>Bearer <%= flash[:token_plaintext] %></code>
26
+ </p>
27
+ </div>
28
+ <% end %>
29
+
30
+ <% if @tokens.empty? %>
31
+ <div class="empty-state-large">
32
+ <h2>No API tokens yet</h2>
33
+ <p>Create a token to start ingesting logs via the HTTP API.</p>
34
+ <%= link_to "Create Your First Token", new_token_path, class: "btn btn-primary" %>
35
+ </div>
36
+ <% else %>
37
+ <div class="card">
38
+ <div class="card-header">
39
+ <h2>Active Tokens</h2>
40
+ </div>
41
+ <div class="card-body no-padding">
42
+ <div class="table-responsive">
43
+ <table class="data-table">
44
+ <thead>
45
+ <tr>
46
+ <th>Name</th>
47
+ <th>Created</th>
48
+ <th>Last Used</th>
49
+ <th>Actions</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody>
53
+ <% @tokens.each do |token| %>
54
+ <tr>
55
+ <td class="token-name"><%= token.name %></td>
56
+ <td><%= time_ago_in_words(token.created_at) %> ago</td>
57
+ <td>
58
+ <% if token.last_used_at %>
59
+ <%= time_ago_in_words(token.last_used_at) %> ago
60
+ <% else %>
61
+ <span class="text-muted">Never</span>
62
+ <% end %>
63
+ </td>
64
+ <td class="table-actions">
65
+ <%= button_to "Revoke", token_path(token), method: :delete,
66
+ data: { confirm: "Revoke token '#{token.name}'? This cannot be undone." },
67
+ class: "btn-link-small text-danger" %>
68
+ </td>
69
+ </tr>
70
+ <% end %>
71
+ </tbody>
72
+ </table>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ <% end %>
77
+
78
+ <div class="token-help">
79
+ <h3>Using API Tokens</h3>
80
+ <p>Include the token in the Authorization header of your HTTP requests:</p>
81
+ <pre class="code-example">curl -X POST <%= request.base_url %>/api/v1/ingest \
82
+ -H "Authorization: Bearer YOUR_TOKEN_HERE" \
83
+ -H "Content-Type: application/json" \
84
+ -d '{"message":"Test log","level":"info"}'</pre>
85
+ </div>
86
+ </div>
87
+ </div>
88
+
89
+ <script>
90
+ function copyToken(token) {
91
+ navigator.clipboard.writeText(token).then(() => {
92
+ alert('Token copied to clipboard!');
93
+ });
94
+ }
95
+ </script>
@@ -0,0 +1,47 @@
1
+ <div class="tokens-page">
2
+ <div class="page-header">
3
+ <div>
4
+ <h1>Create API Token</h1>
5
+ <p class="subtitle">Generate a new token for log ingestion</p>
6
+ </div>
7
+ <div class="page-actions">
8
+ <%= link_to "← Back", tokens_path, class: "btn btn-secondary" %>
9
+ </div>
10
+ </div>
11
+
12
+ <div class="form-container">
13
+ <div class="card">
14
+ <div class="card-header">
15
+ <h2>Token Details</h2>
16
+ </div>
17
+ <div class="card-body">
18
+ <%= form_with model: @token, url: tokens_path, class: "form" do |f| %>
19
+ <div class="form-group">
20
+ <%= f.label :name, "Token Name" %>
21
+ <%= f.text_field :name,
22
+ placeholder: "e.g., Production API, Staging Server",
23
+ class: "form-input",
24
+ required: true,
25
+ autofocus: true %>
26
+ <p class="form-help">Choose a descriptive name to identify this token.</p>
27
+ </div>
28
+
29
+ <div class="form-actions">
30
+ <%= f.submit "Create Token", class: "btn btn-primary" %>
31
+ <%= link_to "Cancel", tokens_path, class: "btn btn-secondary" %>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="info-card">
38
+ <h3>Important Information</h3>
39
+ <ul>
40
+ <li>The token will only be displayed <strong>once</strong> after creation</li>
41
+ <li>Store it securely - it cannot be retrieved later</li>
42
+ <li>Tokens can be revoked at any time from the tokens list</li>
43
+ <li>Each token is hashed and stored securely</li>
44
+ </ul>
45
+ </div>
46
+ </div>
47
+ </div>
@@ -0,0 +1,15 @@
1
+ # Pin npm packages for SolidLog UI engine
2
+
3
+ pin "application", to: "application.js"
4
+ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
5
+ pin "@rails/actioncable", to: "actioncable.esm.js"
6
+
7
+ # Pin SolidLog UI JavaScript modules
8
+ pin "solid_log/stream_scroll", to: "solid_log/stream_scroll.js"
9
+ pin "solid_log/live_tail", to: "solid_log/live_tail.js"
10
+ pin "solid_log/jump_to_live", to: "solid_log/jump_to_live.js"
11
+ pin "solid_log/checkbox_dropdown", to: "solid_log/checkbox_dropdown.js"
12
+ pin "solid_log/timeline_histogram", to: "solid_log/timeline_histogram.js"
13
+ pin "solid_log/log_filters", to: "solid_log/log_filters.js"
14
+ pin "solid_log/filter_state", to: "solid_log/filter_state.js"
15
+ pin "solid_log/toast", to: "solid_log/toast.js"
data/config/routes.rb ADDED
@@ -0,0 +1,27 @@
1
+ SolidLog::UI::Engine.routes.draw do
2
+ # Root - redirect to streams
3
+ root to: redirect("/logs/streams")
4
+
5
+ # Dashboard
6
+ get "dashboard", to: "dashboard#index"
7
+
8
+ # Main log viewing
9
+ resources :streams, only: [:index]
10
+ resources :entries, only: [:index, :show]
11
+
12
+ # Timeline routes for correlation
13
+ get "timelines/request/:request_id", to: "timelines#show_request", as: :request_timeline
14
+ get "timelines/job/:job_id", to: "timelines#show_job", as: :job_timeline
15
+
16
+ # Field management
17
+ resources :fields, only: [:index, :destroy] do
18
+ member do
19
+ post :promote
20
+ post :demote
21
+ patch :update_filter_type
22
+ end
23
+ end
24
+
25
+ # Token management
26
+ resources :tokens, only: [:index, :new, :create, :destroy]
27
+ end
@@ -0,0 +1,117 @@
1
+ require "net/http"
2
+ require "json"
3
+ require "uri"
4
+
5
+ module SolidLog
6
+ module UI
7
+ class ApiClient
8
+ attr_reader :base_url, :token
9
+
10
+ def initialize(base_url: nil, token: nil)
11
+ @base_url = base_url || SolidLog::UI.configuration.service_url
12
+ @token = token || SolidLog::UI.configuration.service_token
13
+
14
+ raise ArgumentError, "base_url required for API client" if @base_url.blank?
15
+ raise ArgumentError, "token required for API client" if @token.blank?
16
+ end
17
+
18
+ # GET /api/v1/entries
19
+ def entries(params = {})
20
+ get("/api/v1/entries", params)
21
+ end
22
+
23
+ # GET /api/v1/entries/:id
24
+ def entry(id)
25
+ get("/api/v1/entries/#{id}")
26
+ end
27
+
28
+ # POST /api/v1/search
29
+ def search(query, params = {})
30
+ post("/api/v1/search", { q: query }.merge(params))
31
+ end
32
+
33
+ # GET /api/v1/facets
34
+ def facets(field)
35
+ get("/api/v1/facets", { field: field })
36
+ end
37
+
38
+ # GET /api/v1/facets/all
39
+ def all_facets
40
+ get("/api/v1/facets/all")
41
+ end
42
+
43
+ # GET /api/v1/timelines/request/:request_id
44
+ def request_timeline(request_id)
45
+ get("/api/v1/timelines/request/#{request_id}")
46
+ end
47
+
48
+ # GET /api/v1/timelines/job/:job_id
49
+ def job_timeline(job_id)
50
+ get("/api/v1/timelines/job/#{job_id}")
51
+ end
52
+
53
+ # GET /api/v1/health
54
+ def health
55
+ get("/api/v1/health")
56
+ end
57
+
58
+ private
59
+
60
+ def get(path, params = {})
61
+ uri = URI.parse("#{@base_url}#{path}")
62
+ uri.query = URI.encode_www_form(params) if params.any?
63
+
64
+ request = Net::HTTP::Get.new(uri)
65
+ request["Authorization"] = "Bearer #{@token}"
66
+ request["Content-Type"] = "application/json"
67
+
68
+ perform_request(uri, request)
69
+ end
70
+
71
+ def post(path, body = {})
72
+ uri = URI.parse("#{@base_url}#{path}")
73
+
74
+ request = Net::HTTP::Post.new(uri)
75
+ request["Authorization"] = "Bearer #{@token}"
76
+ request["Content-Type"] = "application/json"
77
+ request.body = JSON.generate(body)
78
+
79
+ perform_request(uri, request)
80
+ end
81
+
82
+ def perform_request(uri, request)
83
+ http = Net::HTTP.new(uri.host, uri.port)
84
+ http.use_ssl = (uri.scheme == "https")
85
+ http.open_timeout = 5
86
+ http.read_timeout = 30
87
+
88
+ response = http.request(request)
89
+
90
+ case response.code.to_i
91
+ when 200..299
92
+ JSON.parse(response.body)
93
+ when 404
94
+ raise NotFoundError, "Resource not found: #{uri.path}"
95
+ when 401
96
+ raise AuthenticationError, "Authentication failed. Check your service_token."
97
+ when 500..599
98
+ raise ServerError, "Server error (#{response.code}): #{response.body}"
99
+ else
100
+ raise RequestError, "Request failed (#{response.code}): #{response.body}"
101
+ end
102
+ rescue JSON::ParserError => e
103
+ raise ParseError, "Failed to parse JSON response: #{e.message}"
104
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
105
+ raise ConnectionError, "Cannot connect to service at #{@base_url}: #{e.message}"
106
+ end
107
+
108
+ # Custom errors
109
+ class RequestError < StandardError; end
110
+ class NotFoundError < RequestError; end
111
+ class AuthenticationError < RequestError; end
112
+ class ServerError < RequestError; end
113
+ class ConnectionError < RequestError; end
114
+ class ParseError < RequestError; end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,99 @@
1
+ module SolidLog
2
+ module UI
3
+ class Configuration
4
+ attr_accessor :mode,
5
+ :service_url,
6
+ :service_token,
7
+ :database_path,
8
+ :websocket_enabled,
9
+ :stream_view_style,
10
+ :facet_cache_ttl,
11
+ :per_page,
12
+ :base_controller
13
+ attr_reader :authentication_method
14
+
15
+ def initialize
16
+ # Mode: :direct_db (same database) or :http_api (remote service)
17
+ @mode = :direct_db
18
+
19
+ # HTTP API mode settings
20
+ @service_url = nil
21
+ @service_token = nil
22
+
23
+ # Direct DB mode settings
24
+ @database_path = nil
25
+
26
+ # Controller inheritance - defaults to ActionController::Base
27
+ # Set to "ApplicationController" or your app's base controller
28
+ @base_controller = "ActionController::Base"
29
+
30
+ # UI settings
31
+ # Authentication: :none, :basic, or a Proc/Symbol/String
32
+ # - :none - no authentication required
33
+ # - :basic - HTTP basic authentication (uses authenticate_with_basic_auth)
34
+ # - Proc/Lambda - custom authentication logic (called in controller context)
35
+ # - Symbol/String - method name to call on the controller
36
+ @authentication_method = :none
37
+ @websocket_enabled = true
38
+ @stream_view_style = :compact # :compact or :expanded
39
+ @facet_cache_ttl = 1.minute
40
+ @per_page = 100
41
+ end
42
+
43
+ # Set authentication method with validation
44
+ def authentication_method=(value)
45
+ case value
46
+ when :none, :basic
47
+ @authentication_method = value
48
+ when Proc
49
+ @authentication_method = value
50
+ when Symbol, String
51
+ @authentication_method = value.to_sym
52
+ else
53
+ raise ArgumentError, "authentication_method must be :none, :basic, a Proc, or a Symbol/String method name"
54
+ end
55
+ end
56
+
57
+ # Check if authentication is a proc
58
+ def authentication_proc?
59
+ authentication_method.is_a?(Proc)
60
+ end
61
+
62
+ # Check if authentication is a method name
63
+ def authentication_method_name?
64
+ authentication_method.is_a?(Symbol) && ![:none, :basic].include?(authentication_method)
65
+ end
66
+
67
+ # Validate configuration
68
+ def valid?
69
+ errors = []
70
+
71
+ case mode
72
+ when :direct_db
73
+ # Direct DB mode doesn't require additional config (uses core's connection)
74
+ when :http_api
75
+ errors << "service_url required for http_api mode" if service_url.blank?
76
+ errors << "service_token required for http_api mode" if service_token.blank?
77
+ else
78
+ errors << "mode must be :direct_db or :http_api"
79
+ end
80
+
81
+ # Authentication validation is handled by the setter
82
+
83
+ if errors.any?
84
+ raise ArgumentError, "Invalid UI configuration:\n #{errors.join("\n ")}"
85
+ end
86
+
87
+ true
88
+ end
89
+
90
+ def direct_db_mode?
91
+ mode == :direct_db
92
+ end
93
+
94
+ def http_api_mode?
95
+ mode == :http_api
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,146 @@
1
+ require_relative "api_client"
2
+
3
+ module SolidLog
4
+ module UI
5
+ class DataSource
6
+ # Query entries with filters
7
+ def entries(filters = {})
8
+ if direct_db_mode?
9
+ query_direct_db(filters)
10
+ else
11
+ query_http_api(filters)
12
+ end
13
+ end
14
+
15
+ # Get single entry by ID
16
+ def entry(id)
17
+ if direct_db_mode?
18
+ SolidLog::Entry.find(id)
19
+ else
20
+ result = api_client.entry(id)
21
+ OpenStruct.new(result["entry"])
22
+ end
23
+ end
24
+
25
+ # Full-text search
26
+ def search(query, filters = {})
27
+ if direct_db_mode?
28
+ entries = SolidLog::SearchService.search(query)
29
+ apply_filters(entries, filters)
30
+ else
31
+ result = api_client.search(query, filters)
32
+ parse_entries_response(result)
33
+ end
34
+ end
35
+
36
+ # Get facets for a field
37
+ def facets(field)
38
+ if direct_db_mode?
39
+ SolidLog::SearchService.facets_for(field)
40
+ else
41
+ result = api_client.facets(field)
42
+ result["values"]
43
+ end
44
+ end
45
+
46
+ # Get all facets
47
+ def all_facets
48
+ if direct_db_mode?
49
+ {
50
+ level: SolidLog::SearchService.facets_for("level"),
51
+ app: SolidLog::SearchService.facets_for("app"),
52
+ env: SolidLog::SearchService.facets_for("env"),
53
+ controller: SolidLog::SearchService.facets_for("controller"),
54
+ action: SolidLog::SearchService.facets_for("action"),
55
+ method: SolidLog::SearchService.facets_for("method"),
56
+ status_code: SolidLog::SearchService.facets_for("status_code")
57
+ }
58
+ else
59
+ result = api_client.all_facets
60
+ result["facets"]
61
+ end
62
+ end
63
+
64
+ # Get request timeline
65
+ def request_timeline(request_id)
66
+ if direct_db_mode?
67
+ {
68
+ request_id: request_id,
69
+ entries: SolidLog::CorrelationService.request_timeline(request_id),
70
+ stats: SolidLog::CorrelationService.request_stats(request_id)
71
+ }
72
+ else
73
+ api_client.request_timeline(request_id)
74
+ end
75
+ end
76
+
77
+ # Get job timeline
78
+ def job_timeline(job_id)
79
+ if direct_db_mode?
80
+ {
81
+ job_id: job_id,
82
+ entries: SolidLog::CorrelationService.job_timeline(job_id),
83
+ stats: SolidLog::CorrelationService.job_stats(job_id)
84
+ }
85
+ else
86
+ api_client.job_timeline(job_id)
87
+ end
88
+ end
89
+
90
+ # Get health metrics
91
+ def health
92
+ if direct_db_mode?
93
+ SolidLog::HealthService.metrics
94
+ else
95
+ api_client.health["metrics"]
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def direct_db_mode?
102
+ SolidLog::UI.configuration.direct_db_mode?
103
+ end
104
+
105
+ def http_api_mode?
106
+ SolidLog::UI.configuration.http_api_mode?
107
+ end
108
+
109
+ def api_client
110
+ @api_client ||= ApiClient.new
111
+ end
112
+
113
+ def query_direct_db(filters)
114
+ SolidLog::SearchService.query(filters).recent.limit(per_page)
115
+ end
116
+
117
+ def query_http_api(filters)
118
+ result = api_client.entries(filters.merge(limit: per_page))
119
+ parse_entries_response(result)
120
+ end
121
+
122
+ def apply_filters(scope, filters)
123
+ scope = scope.by_level(filters[:level]) if filters[:level].present?
124
+ scope = scope.by_app(filters[:app]) if filters[:app].present?
125
+ scope = scope.by_env(filters[:env]) if filters[:env].present?
126
+ scope = scope.by_controller(filters[:controller]) if filters[:controller].present?
127
+ scope = scope.by_action(filters[:action]) if filters[:action].present?
128
+ scope = scope.by_path(filters[:path]) if filters[:path].present?
129
+ scope = scope.by_method(filters[:method]) if filters[:method].present?
130
+ scope = scope.by_status_code(filters[:status_code]) if filters[:status_code].present?
131
+
132
+ scope.recent.limit(per_page)
133
+ end
134
+
135
+ def parse_entries_response(result)
136
+ # Convert API response to array of OpenStruct objects
137
+ # This makes them compatible with views that expect ActiveRecord objects
138
+ (result["entries"] || []).map { |entry| OpenStruct.new(entry) }
139
+ end
140
+
141
+ def per_page
142
+ SolidLog::UI.configuration.per_page
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,76 @@
1
+ require "importmap-rails"
2
+ require "turbo-rails"
3
+ require "stimulus-rails"
4
+
5
+ module SolidLog
6
+ module UI
7
+ class Engine < ::Rails::Engine
8
+ isolate_namespace SolidLog::UI
9
+
10
+ config.generators do |g|
11
+ g.test_framework :test_unit
12
+ g.assets false
13
+ g.helper false
14
+ end
15
+
16
+ # Configure assets (works with both Sprockets and Propshaft)
17
+ initializer "solid_log_ui.assets" do |app|
18
+ # Add asset paths for both Sprockets and Propshaft
19
+ if app.config.respond_to?(:assets)
20
+ # Sprockets
21
+ app.config.assets.paths << root.join("app/assets/stylesheets")
22
+ app.config.assets.paths << root.join("app/assets/javascripts")
23
+ app.config.assets.paths << root.join("app/assets/images")
24
+
25
+ app.config.assets.precompile += %w[
26
+ solid_log/**/*.css
27
+ solid_log/**/*.js
28
+ ]
29
+ end
30
+
31
+ # Propshaft
32
+ if Rails.application.config.respond_to?(:assets) && Rails.application.config.assets.respond_to?(:paths)
33
+ Rails.application.config.assets.paths << root.join("app/assets/stylesheets")
34
+ Rails.application.config.assets.paths << root.join("app/assets/javascripts")
35
+ Rails.application.config.assets.paths << root.join("app/assets/images")
36
+ end
37
+ end
38
+
39
+ # Configure importmap for the engine
40
+ initializer "solid_log_ui.importmap", before: "importmap" do |app|
41
+ app.config.importmap.paths << root.join("config/importmap.rb")
42
+ app.config.importmap.cache_sweepers << root.join("app/assets/javascripts")
43
+ end
44
+
45
+ # Set up inflections for UI acronym
46
+ initializer "solid_log_ui.inflections" do
47
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
48
+ inflect.acronym "UI"
49
+ end
50
+ end
51
+
52
+ # Load configuration if it exists
53
+ initializer "solid_log_ui.load_config" do
54
+ config_file = Rails.root.join("config/initializers/solid_log_ui.rb")
55
+ load config_file if File.exist?(config_file)
56
+ end
57
+
58
+ # Add SilenceMiddleware to main app's middleware stack
59
+ # This prevents the UI from logging its own queries/requests
60
+ initializer "solid_log_ui.add_middleware" do |app|
61
+ app.middleware.use SolidLog::SilenceMiddleware
62
+ end
63
+
64
+ # Register Action Cable channels
65
+ initializer "solid_log_ui.action_cable" do
66
+ engine_root = root
67
+ config.to_prepare do
68
+ # Ensure channel classes are loaded and available to ActionCable
69
+ Dir[engine_root.join("app/channels/**/*_channel.rb")].each do |file|
70
+ require_dependency file
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,5 @@
1
+ module SolidLog
2
+ module UI
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,27 @@
1
+ require "solid_log/core"
2
+ require_relative "ui/version"
3
+ require_relative "ui/configuration"
4
+ require_relative "ui/data_source"
5
+ require_relative "ui/api_client"
6
+ require_relative "ui/engine" if defined?(Rails)
7
+
8
+ module SolidLog
9
+ module UI
10
+ class << self
11
+ attr_writer :configuration
12
+
13
+ def configuration
14
+ @configuration ||= Configuration.new
15
+ end
16
+
17
+ def configure
18
+ yield(configuration)
19
+ configuration.valid?
20
+ end
21
+
22
+ def reset_configuration!
23
+ @configuration = Configuration.new
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,2 @@
1
+ # Top-level require for solid_log-ui gem
2
+ require "solid_log/ui"