inferno_session_browser 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/CHANGELOG.md +9 -0
- data/LICENSE +201 -0
- data/NOTICE +6 -0
- data/README.md +180 -0
- data/RELEASING.md +76 -0
- data/examples/host/Gemfile +3 -0
- data/examples/host/config/database.yml +4 -0
- data/examples/host/config.ru +6 -0
- data/examples/host/lib/demo.rb +9 -0
- data/examples/host/prepare.rb +14 -0
- data/lib/inferno_session_browser/assets/app.css +5 -0
- data/lib/inferno_session_browser/assets/app.js +212 -0
- data/lib/inferno_session_browser/configuration.rb +25 -0
- data/lib/inferno_session_browser/middleware.rb +87 -0
- data/lib/inferno_session_browser/parameters.rb +79 -0
- data/lib/inferno_session_browser/query.rb +261 -0
- data/lib/inferno_session_browser/registry.rb +13 -0
- data/lib/inferno_session_browser/templates/index.html.erb +42 -0
- data/lib/inferno_session_browser/version.rb +4 -0
- data/lib/inferno_session_browser.rb +35 -0
- metadata +123 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'sequel'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'time'
|
|
6
|
+
require_relative 'parameters'
|
|
7
|
+
require_relative 'registry'
|
|
8
|
+
|
|
9
|
+
module InfernoSessionBrowser
|
|
10
|
+
class Query
|
|
11
|
+
ACTIVE_STATES = %w[queued running waiting cancelling].freeze
|
|
12
|
+
RESULT_STATES = %w[pass fail skip error omit cancel wait running].freeze
|
|
13
|
+
BATCH_SIZE = 500
|
|
14
|
+
|
|
15
|
+
def initialize(db:, configuration:, base_path: '', registry: Registry.new)
|
|
16
|
+
@db, @configuration, @registry = db, configuration, registry
|
|
17
|
+
@base_path = normalize_base(base_path)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def sessions(raw)
|
|
21
|
+
parameters = Parameters.new(raw)
|
|
22
|
+
dataset, bindings = filtered_sessions(parameters)
|
|
23
|
+
rows, total = session_page(dataset, bindings, parameters)
|
|
24
|
+
ids = rows.map { |row| row[:id] }
|
|
25
|
+
urls = fhir_urls(ids)
|
|
26
|
+
counts = result_counts(@db[:results].where(test_session_id: ids), :test_session_id)
|
|
27
|
+
data = rows.map do |row|
|
|
28
|
+
suite = @registry.suite(row[:test_suite_id])
|
|
29
|
+
{
|
|
30
|
+
id: row[:id], suite_id: suite&.id || row[:test_suite_id],
|
|
31
|
+
suite_title: suite&.title || row[:test_suite_id], suite_available: !suite.nil?,
|
|
32
|
+
suite_options: display_options(row[:suite_options], suite), fhir_url: urls[row[:id]],
|
|
33
|
+
created_at: timestamp(row[:created_at]), latest_activity: timestamp(row[:latest_activity]),
|
|
34
|
+
state: row[:state], run_count: row[:run_count].to_i,
|
|
35
|
+
result_counts: counts.fetch(row[:id], empty_counts),
|
|
36
|
+
session_url: suite ? "#{@base_path}/#{encode(suite.id)}/#{encode(row[:id])}" : nil
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
{ data: data, pagination: parameters.metadata(total) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def options
|
|
43
|
+
suites = {}
|
|
44
|
+
selections = Hash.new { |hash, key| hash[key] = {} }
|
|
45
|
+
each_batch(@db[:test_sessions].select(:id, :test_suite_id, :suite_options).order(:id), {}) do |rows|
|
|
46
|
+
rows.each do |row|
|
|
47
|
+
suite = @registry.suite(row[:test_suite_id])
|
|
48
|
+
suites[row[:test_suite_id]] ||= { id: row[:test_suite_id], title: suite&.title || row[:test_suite_id] }
|
|
49
|
+
display_options(row[:suite_options], suite).each do |option|
|
|
50
|
+
key = option[:id]
|
|
51
|
+
selections[key][:title] ||= option[:title]
|
|
52
|
+
selections[key][:values] ||= {}
|
|
53
|
+
selections[key][:values][option[:value]] ||= option[:label]
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
{
|
|
58
|
+
suites: suites.values.sort_by { |suite| [suite[:title].to_s.downcase, suite[:id]] },
|
|
59
|
+
suite_options: selections.sort.map do |id, option|
|
|
60
|
+
{ id: id, title: option[:title], values: option[:values].sort.map { |value, label| { value: value, label: label } } }
|
|
61
|
+
end,
|
|
62
|
+
states: Parameters::STATES
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def runs(session_id, raw)
|
|
67
|
+
raise NotFound, 'Session not found' unless @db[:test_sessions].where(id: :$id).call(:first, id: session_id)
|
|
68
|
+
parameters = Parameters.new(raw, history: true)
|
|
69
|
+
dataset = @db[:test_runs].where(test_session_id: :$session_id)
|
|
70
|
+
bindings = { session_id: session_id }
|
|
71
|
+
total = dataset.select(Sequel.function(:count, Sequel.lit('*')).as(:total)).call(:first, bindings)[:total]
|
|
72
|
+
rows = dataset.select(:id, :status, :created_at, :updated_at, :test_suite_id, :test_group_id, :test_id)
|
|
73
|
+
.order(Sequel.desc(:created_at), Sequel.desc(:id))
|
|
74
|
+
.limit(parameters.page_size, parameters.offset).call(:select, bindings)
|
|
75
|
+
ids = rows.map { |row| row[:id] }
|
|
76
|
+
counts = result_counts(@db[:results].where(test_run_id: ids), :test_run_id)
|
|
77
|
+
# Select only outcome metadata; messages, input/output JSON, and requests are never loaded.
|
|
78
|
+
outcomes = if rows.empty?
|
|
79
|
+
{}
|
|
80
|
+
else
|
|
81
|
+
conditions = rows.map { |row| { test_run_id: row[:id], target(row).last => row[target(row).last] } }
|
|
82
|
+
@db[:results].where(Sequel.|(*conditions))
|
|
83
|
+
.select(:test_run_id, :result, row_number(:test_run_id).as(:position))
|
|
84
|
+
.from_self.where(position: 1).all.to_h { |result| [result[:test_run_id], result[:result]] }
|
|
85
|
+
end
|
|
86
|
+
data = rows.map do |row|
|
|
87
|
+
type, column = target(row)
|
|
88
|
+
id = row[column]
|
|
89
|
+
runnable = @registry.runnable(type, id)
|
|
90
|
+
{ id: row[:id], status: row[:status], created_at: timestamp(row[:created_at]),
|
|
91
|
+
updated_at: timestamp(row[:updated_at]),
|
|
92
|
+
target: { type: type, id: runnable&.id || id, title: runnable&.title || id },
|
|
93
|
+
outcome: outcomes[row[:id]], result_counts: counts.fetch(row[:id], empty_counts) }
|
|
94
|
+
end
|
|
95
|
+
{ data: data, pagination: parameters.metadata(total) }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def base_sessions
|
|
101
|
+
s = Sequel[:test_sessions]
|
|
102
|
+
statistics = @db[:test_runs].group(:test_session_id)
|
|
103
|
+
.select(:test_session_id, Sequel.function(:count, Sequel.lit('*')).as(:run_count),
|
|
104
|
+
Sequel.function(:max, :updated_at).as(:run_activity))
|
|
105
|
+
activity = @db[:results].group(:test_session_id)
|
|
106
|
+
.select(:test_session_id, Sequel.function(:max, :updated_at).as(:result_activity))
|
|
107
|
+
active = @db[:test_runs].where(status: ACTIVE_STATES)
|
|
108
|
+
.select(:test_session_id, :status, row_number(:test_session_id).as(:position))
|
|
109
|
+
.from_self.where(position: 1)
|
|
110
|
+
latest = [Sequel[:run_stats][:run_activity], Sequel[:result_stats][:result_activity]]
|
|
111
|
+
.reduce(s[:created_at]) { |maximum, date| Sequel.case({ (date > maximum) => date }, maximum) }
|
|
112
|
+
state = Sequel.case({ Sequel.~(Sequel[:active_run][:status] => nil) => Sequel[:active_run][:status],
|
|
113
|
+
{ Sequel[:run_stats][:run_count] => nil } => 'never_run' }, 'idle')
|
|
114
|
+
@db[:test_sessions]
|
|
115
|
+
.left_join(statistics, { Sequel[:run_stats][:test_session_id] => s[:id] }, table_alias: :run_stats)
|
|
116
|
+
.left_join(activity, { Sequel[:result_stats][:test_session_id] => s[:id] }, table_alias: :result_stats)
|
|
117
|
+
.left_join(active, { Sequel[:active_run][:test_session_id] => s[:id] }, table_alias: :active_run)
|
|
118
|
+
.select(s[:id], s[:test_suite_id], s[:suite_options], s[:created_at],
|
|
119
|
+
Sequel[:run_stats][:run_count], latest.as(:latest_activity), state.as(:state))
|
|
120
|
+
.from_self(alias: :session_summary)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def filtered_sessions(parameters)
|
|
124
|
+
filters = parameters.filters
|
|
125
|
+
dataset = base_sessions
|
|
126
|
+
bindings = {}
|
|
127
|
+
if filters['q']
|
|
128
|
+
dataset = dataset.where(Sequel.ilike(:id, :$q))
|
|
129
|
+
bindings[:q] = "%#{filters['q'].gsub(/[\\%_]/) { |char| "\\#{char}" }}%"
|
|
130
|
+
end
|
|
131
|
+
{ 'suite_id' => :test_suite_id, 'state' => :state }.each do |key, column|
|
|
132
|
+
next unless filters[key]
|
|
133
|
+
dataset = dataset.where(column => :"$#{key}")
|
|
134
|
+
bindings[key.to_sym] = filters[key]
|
|
135
|
+
end
|
|
136
|
+
{ 'from' => :>=, 'to' => :< }.each do |key, comparison|
|
|
137
|
+
next unless filters[key]
|
|
138
|
+
dataset = dataset.where(Sequel[filters['date_field'].to_sym].public_send(comparison, :"$#{key}"))
|
|
139
|
+
bindings[key.to_sym] = filters[key]
|
|
140
|
+
end
|
|
141
|
+
column = parameters.sort == 'suite' ? :test_suite_id : parameters.sort.to_sym
|
|
142
|
+
order = parameters.direction == 'asc' ? Sequel.asc(column) : Sequel.desc(column)
|
|
143
|
+
[dataset.order(order, Sequel.asc(:id)), bindings]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def session_page(dataset, bindings, parameters)
|
|
147
|
+
filters = parameters.filters
|
|
148
|
+
if filters['suite_options'].empty? && !filters['fhir_url']
|
|
149
|
+
total = dataset.unordered.select(Sequel.function(:count, Sequel.lit('*')).as(:total)).call(:first, bindings)[:total]
|
|
150
|
+
return [dataset.limit(parameters.page_size, parameters.offset).call(:select, bindings), total]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Option JSON and URLs are normalized in Ruby for identical SQLite/Postgres behavior.
|
|
154
|
+
# Scan lightweight candidate metadata in bounded batches, retaining only the requested page.
|
|
155
|
+
# This also ensures URL filters cannot match hidden userinfo/query/fragment secrets.
|
|
156
|
+
page = []
|
|
157
|
+
total = 0
|
|
158
|
+
each_batch(dataset, bindings) do |rows|
|
|
159
|
+
urls = filters['fhir_url'] ? fhir_urls(rows.map { |row| row[:id] }) : {}
|
|
160
|
+
rows.each do |row|
|
|
161
|
+
options = parse_options(row[:suite_options]).to_h { |option| [option['id'].to_s, option['value'].to_s] }
|
|
162
|
+
next unless filters['suite_options'].all? { |key, value| options[key] == value }
|
|
163
|
+
next if filters['fhir_url'] && !urls[row[:id]].to_s.downcase.include?(filters['fhir_url'].downcase)
|
|
164
|
+
page << row if total >= parameters.offset && page.length < parameters.page_size
|
|
165
|
+
total += 1
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
[page, total]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def each_batch(dataset, bindings)
|
|
172
|
+
offset = 0
|
|
173
|
+
loop do
|
|
174
|
+
rows = dataset.limit(BATCH_SIZE, offset).call(:select, bindings)
|
|
175
|
+
break if rows.empty?
|
|
176
|
+
yield rows
|
|
177
|
+
break if rows.length < BATCH_SIZE
|
|
178
|
+
offset += BATCH_SIZE
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def fhir_urls(ids)
|
|
183
|
+
return {} if ids.empty?
|
|
184
|
+
candidates = @db[:session_data].where(test_session_id: ids, name: @configuration.fhir_url_input_names)
|
|
185
|
+
.select(:test_session_id, :name, :value).all.group_by { |row| row[:test_session_id] }
|
|
186
|
+
candidates.transform_values do |rows|
|
|
187
|
+
@configuration.fhir_url_input_names.lazy.map do |name|
|
|
188
|
+
value = rows.find { |row| row[:name] == name }&.fetch(:value)
|
|
189
|
+
sanitized_url(value)
|
|
190
|
+
end.find { |value| !value.nil? }
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def sanitized_url(value)
|
|
195
|
+
return nil unless value.is_a?(String)
|
|
196
|
+
uri = URI.parse(value.strip)
|
|
197
|
+
return nil unless %w[http https].include?(uri.scheme&.downcase) && uri.host
|
|
198
|
+
# URI#userinfo = nil does not clear existing user/password in all Ruby versions.
|
|
199
|
+
# Rebuild from an explicit allowlist instead of mutating the original URI.
|
|
200
|
+
type = uri.scheme.downcase == 'https' ? URI::HTTPS : URI::HTTP
|
|
201
|
+
type.build(host: uri.host, port: uri.port, path: uri.path).normalize.to_s
|
|
202
|
+
rescue URI::InvalidURIError, URI::InvalidComponentError
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def result_counts(dataset, partition)
|
|
207
|
+
ranked = dataset.exclude(test_id: nil)
|
|
208
|
+
.select(partition, :result, row_number([partition, :test_id]).as(:position)).from_self
|
|
209
|
+
ranked.where(position: 1).group(partition, :result)
|
|
210
|
+
.select(partition, :result, Sequel.function(:count, Sequel.lit('*')).as(:total))
|
|
211
|
+
.all.each_with_object({}) do |row, counts|
|
|
212
|
+
counts[row[partition]] ||= empty_counts
|
|
213
|
+
counts[row[partition]][row[:result]] = row[:total] if RESULT_STATES.include?(row[:result])
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def row_number(partition)
|
|
218
|
+
Sequel.function(:row_number).over(partition: partition,
|
|
219
|
+
order: [Sequel.desc(:updated_at), Sequel.desc(:created_at), Sequel.desc(:id)])
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def empty_counts
|
|
223
|
+
RESULT_STATES.to_h { |state| [state, 0] }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def parse_options(value)
|
|
227
|
+
parsed = JSON.parse(value || '[]')
|
|
228
|
+
parsed.is_a?(Array) ? parsed.select { |option| option.is_a?(Hash) && option['id'] && option.key?('value') } : []
|
|
229
|
+
rescue JSON::ParserError, TypeError
|
|
230
|
+
[]
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def display_options(value, suite)
|
|
234
|
+
parse_options(value).map do |selected|
|
|
235
|
+
definition = suite&.suite_options&.find { |option| option.id.to_s == selected['id'].to_s }
|
|
236
|
+
choice = definition&.list_options&.find { |option| option[:value].to_s == selected['value'].to_s }
|
|
237
|
+
{ id: selected['id'].to_s, title: definition&.title || selected['id'].to_s,
|
|
238
|
+
value: selected['value'].to_s, label: choice&.fetch(:label, nil) || selected['value'].to_s }
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def target(row)
|
|
243
|
+
return ['test', :test_id] if row[:test_id]
|
|
244
|
+
return ['group', :test_group_id] if row[:test_group_id]
|
|
245
|
+
['suite', :test_suite_id]
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def timestamp(value)
|
|
249
|
+
value = @db.to_application_timestamp(value) if value.is_a?(String)
|
|
250
|
+
value&.to_time&.utc&.iso8601(3)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def encode(value)
|
|
254
|
+
URI.encode_www_form_component(value.to_s).gsub('+', '%20')
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def normalize_base(path)
|
|
258
|
+
path.to_s.split('/').reject(&:empty?).map { |part| "/#{part}" }.join
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module InfernoSessionBrowser
|
|
3
|
+
class Registry
|
|
4
|
+
def suite(id)
|
|
5
|
+
Inferno::Repositories::TestSuites.new.find(id)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def runnable(type, id)
|
|
9
|
+
repository = { 'suite' => 'TestSuites', 'group' => 'TestGroups', 'test' => 'Tests' }.fetch(type)
|
|
10
|
+
Inferno::Repositories.const_get(repository).new.find(id)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Inferno Session Browser</title>
|
|
7
|
+
<link rel="stylesheet" href="<%= ERB::Util.html_escape(mount_path) %>/assets/<%= version %>/app.css">
|
|
8
|
+
<script defer src="<%= ERB::Util.html_escape(mount_path) %>/assets/<%= version %>/app.js"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body data-browser-path="<%= ERB::Util.html_escape(mount_path) %>">
|
|
11
|
+
<main>
|
|
12
|
+
<header class="page-header">
|
|
13
|
+
<div><p class="eyebrow">INFERNO</p><h1>Session Browser</h1><p class="subtitle">Explore test sessions and their run history.</p></div>
|
|
14
|
+
<div class="refresh-controls"><label><input id="auto-refresh" type="checkbox" checked> Refresh every 10 seconds</label><button id="refresh" type="button">Refresh now</button></div>
|
|
15
|
+
</header>
|
|
16
|
+
<form id="filters" aria-label="Session filters">
|
|
17
|
+
<div class="filter-grid">
|
|
18
|
+
<label>Session ID<input name="q" type="search" placeholder="Search session IDs"></label>
|
|
19
|
+
<label>Test suite<select name="suite_id"><option value="">All suites</option></select></label>
|
|
20
|
+
<label>FHIR server URL<input name="fhir_url" type="search" placeholder="Host or path"></label>
|
|
21
|
+
<label>Run state<select name="state"><option value="">All states</option><option value="never_run">Never run</option><option value="queued">Queued</option><option value="running">Running</option><option value="waiting">Waiting</option><option value="cancelling">Cancelling</option><option value="idle">Idle</option></select></label>
|
|
22
|
+
<label>Date field<select name="date_field"><option value="created_at">Created</option><option value="latest_activity">Latest activity</option></select></label>
|
|
23
|
+
<label>From (local time)<input name="from" type="datetime-local" step="1"></label>
|
|
24
|
+
<label>Until (exclusive, local time)<input name="to" type="datetime-local" step="1"></label>
|
|
25
|
+
</div>
|
|
26
|
+
<div id="option-filters" class="filter-grid"></div>
|
|
27
|
+
<div class="filter-actions"><button type="submit" class="primary">Apply filters</button><button id="clear-filters" type="button">Clear filters</button><span id="options-error" role="status"></span></div>
|
|
28
|
+
</form>
|
|
29
|
+
<div id="error" role="alert" hidden></div>
|
|
30
|
+
<section aria-label="Test sessions">
|
|
31
|
+
<div class="table-toolbar">
|
|
32
|
+
<p id="status" role="status" aria-live="polite">Loading sessions…</p>
|
|
33
|
+
<div class="sorting"><label>Sort by<select id="sort"><option value="latest_activity">Latest activity</option><option value="created_at">Created</option><option value="suite">Test suite</option></select></label><label>Order<select id="direction"><option value="desc">Descending</option><option value="asc">Ascending</option></select></label></div>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="table-scroll"><table id="sessions-table" aria-busy="true"><caption class="sr-only">Test sessions. Expand a session to view its run history.</caption><thead><tr><th scope="col">Session</th><th scope="col">Suite / options</th><th scope="col">FHIR server</th><th scope="col"><button type="button" class="sort-heading" data-sort="created_at">Created</button></th><th scope="col"><button type="button" class="sort-heading" data-sort="latest_activity">Latest activity</button></th><th scope="col">Run state</th><th scope="col">Runs</th><th scope="col">Current test results</th><th scope="col">Actions</th></tr></thead><tbody id="session-rows"></tbody></table></div>
|
|
36
|
+
<p class="result-note">Result counts include optional tests. Run state and result counts do not indicate certification completion.</p>
|
|
37
|
+
<nav class="pagination" aria-label="Session pagination"><label>Rows per page<select id="page-size"><option value="25">25</option><option value="50">50</option><option value="100">100</option></select></label><div><button id="previous-page" type="button" disabled>Previous</button><span id="page-number">Page 1</span><button id="next-page" type="button" disabled>Next</button></div></nav>
|
|
38
|
+
</section>
|
|
39
|
+
<footer>Inferno Session Browser <%= version %> · Read-only</footer>
|
|
40
|
+
</main>
|
|
41
|
+
</body>
|
|
42
|
+
</html>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'inferno/version'
|
|
4
|
+
require_relative 'inferno_session_browser/version'
|
|
5
|
+
require_relative 'inferno_session_browser/configuration'
|
|
6
|
+
require_relative 'inferno_session_browser/middleware'
|
|
7
|
+
|
|
8
|
+
module InfernoSessionBrowser
|
|
9
|
+
class << self
|
|
10
|
+
def configuration
|
|
11
|
+
@configuration ||= Configuration.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def configure
|
|
15
|
+
yield configuration
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
module WebAppExtension
|
|
20
|
+
def app
|
|
21
|
+
original = super
|
|
22
|
+
Middleware.new(original, configuration: InfernoSessionBrowser.configuration.snapshot,
|
|
23
|
+
base_path: Inferno::Application['base_path'].to_s)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Defining this namespace does not load Inferno's web provider or database.
|
|
29
|
+
# The provider can define .app before or after this file is required.
|
|
30
|
+
module Inferno
|
|
31
|
+
module Web
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
Inferno::Web.singleton_class.prepend(InfernoSessionBrowser::WebAppExtension) unless
|
|
35
|
+
Inferno::Web.singleton_class.ancestors.include?(InfernoSessionBrowser::WebAppExtension)
|
metadata
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: inferno_session_browser
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Daniel Alistar
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-10-02 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: inferno_core
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 1.4.0
|
|
20
|
+
- - "<"
|
|
21
|
+
- !ruby/object:Gem::Version
|
|
22
|
+
version: '1.5'
|
|
23
|
+
type: :runtime
|
|
24
|
+
prerelease: false
|
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
26
|
+
requirements:
|
|
27
|
+
- - ">="
|
|
28
|
+
- !ruby/object:Gem::Version
|
|
29
|
+
version: 1.4.0
|
|
30
|
+
- - "<"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.5'
|
|
33
|
+
- !ruby/object:Gem::Dependency
|
|
34
|
+
name: rack
|
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.2'
|
|
40
|
+
- - "<"
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3'
|
|
43
|
+
type: :runtime
|
|
44
|
+
prerelease: false
|
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '2.2'
|
|
50
|
+
- - "<"
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '3'
|
|
53
|
+
- !ruby/object:Gem::Dependency
|
|
54
|
+
name: sequel
|
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - "~>"
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '5.42'
|
|
60
|
+
type: :runtime
|
|
61
|
+
prerelease: false
|
|
62
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - "~>"
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '5.42'
|
|
67
|
+
description: Adds a paginated session dashboard and run history to an existing Inferno
|
|
68
|
+
host on require.
|
|
69
|
+
email:
|
|
70
|
+
executables: []
|
|
71
|
+
extensions: []
|
|
72
|
+
extra_rdoc_files: []
|
|
73
|
+
files:
|
|
74
|
+
- CHANGELOG.md
|
|
75
|
+
- LICENSE
|
|
76
|
+
- NOTICE
|
|
77
|
+
- README.md
|
|
78
|
+
- RELEASING.md
|
|
79
|
+
- examples/host/Gemfile
|
|
80
|
+
- examples/host/config.ru
|
|
81
|
+
- examples/host/config/database.yml
|
|
82
|
+
- examples/host/lib/demo.rb
|
|
83
|
+
- examples/host/prepare.rb
|
|
84
|
+
- lib/inferno_session_browser.rb
|
|
85
|
+
- lib/inferno_session_browser/assets/app.css
|
|
86
|
+
- lib/inferno_session_browser/assets/app.js
|
|
87
|
+
- lib/inferno_session_browser/configuration.rb
|
|
88
|
+
- lib/inferno_session_browser/middleware.rb
|
|
89
|
+
- lib/inferno_session_browser/parameters.rb
|
|
90
|
+
- lib/inferno_session_browser/query.rb
|
|
91
|
+
- lib/inferno_session_browser/registry.rb
|
|
92
|
+
- lib/inferno_session_browser/templates/index.html.erb
|
|
93
|
+
- lib/inferno_session_browser/version.rb
|
|
94
|
+
homepage: https://github.com/daniel-alistar/inferno_session_browser
|
|
95
|
+
licenses:
|
|
96
|
+
- Apache-2.0
|
|
97
|
+
metadata:
|
|
98
|
+
source_code_uri: https://github.com/daniel-alistar/inferno_session_browser
|
|
99
|
+
documentation_uri: https://github.com/daniel-alistar/inferno_session_browser/blob/main/README.md
|
|
100
|
+
bug_tracker_uri: https://github.com/daniel-alistar/inferno_session_browser/issues
|
|
101
|
+
changelog_uri: https://github.com/daniel-alistar/inferno_session_browser/blob/main/CHANGELOG.md
|
|
102
|
+
allowed_push_host: https://rubygems.org
|
|
103
|
+
rubygems_mfa_required: 'true'
|
|
104
|
+
post_install_message:
|
|
105
|
+
rdoc_options: []
|
|
106
|
+
require_paths:
|
|
107
|
+
- lib
|
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
109
|
+
requirements:
|
|
110
|
+
- - ">="
|
|
111
|
+
- !ruby/object:Gem::Version
|
|
112
|
+
version: 3.3.6
|
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
requirements: []
|
|
119
|
+
rubygems_version: 3.5.22
|
|
120
|
+
signing_key:
|
|
121
|
+
specification_version: 4
|
|
122
|
+
summary: A read-only session browser for the Inferno Framework
|
|
123
|
+
test_files: []
|