litestack 0.2.6 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/BENCHMARKS.md +11 -0
  3. data/CHANGELOG.md +19 -0
  4. data/Gemfile +2 -0
  5. data/README.md +1 -1
  6. data/assets/event_page.png +0 -0
  7. data/assets/index_page.png +0 -0
  8. data/assets/topic_page.png +0 -0
  9. data/bench/bench_jobs_rails.rb +1 -1
  10. data/bench/bench_jobs_raw.rb +1 -1
  11. data/bench/uljob.rb +1 -1
  12. data/lib/action_cable/subscription_adapter/litecable.rb +1 -11
  13. data/lib/active_support/cache/litecache.rb +1 -1
  14. data/lib/generators/litestack/install/templates/database.yml +5 -1
  15. data/lib/litestack/liteboard/liteboard.rb +172 -35
  16. data/lib/litestack/liteboard/views/index.erb +52 -20
  17. data/lib/litestack/liteboard/views/layout.erb +189 -38
  18. data/lib/litestack/liteboard/views/litecable.erb +118 -0
  19. data/lib/litestack/liteboard/views/litecache.erb +144 -0
  20. data/lib/litestack/liteboard/views/litedb.erb +168 -0
  21. data/lib/litestack/liteboard/views/litejob.erb +151 -0
  22. data/lib/litestack/litecable.rb +27 -37
  23. data/lib/litestack/litecable.sql.yml +1 -1
  24. data/lib/litestack/litecache.rb +7 -18
  25. data/lib/litestack/litedb.rb +17 -2
  26. data/lib/litestack/litejob.rb +2 -3
  27. data/lib/litestack/litejobqueue.rb +51 -48
  28. data/lib/litestack/litemetric.rb +46 -69
  29. data/lib/litestack/litemetric.sql.yml +14 -12
  30. data/lib/litestack/litemetric_collector.sql.yml +4 -4
  31. data/lib/litestack/litequeue.rb +9 -20
  32. data/lib/litestack/litescheduler.rb +84 -0
  33. data/lib/litestack/litesearch/index.rb +230 -0
  34. data/lib/litestack/litesearch/model.rb +178 -0
  35. data/lib/litestack/litesearch/schema.rb +193 -0
  36. data/lib/litestack/litesearch/schema_adapters/backed_adapter.rb +147 -0
  37. data/lib/litestack/litesearch/schema_adapters/basic_adapter.rb +128 -0
  38. data/lib/litestack/litesearch/schema_adapters/contentless_adapter.rb +17 -0
  39. data/lib/litestack/litesearch/schema_adapters/standalone_adapter.rb +33 -0
  40. data/lib/litestack/litesearch/schema_adapters.rb +9 -0
  41. data/lib/litestack/litesearch.rb +37 -0
  42. data/lib/litestack/litesupport.rb +55 -125
  43. data/lib/litestack/version.rb +1 -1
  44. data/lib/litestack.rb +2 -1
  45. data/lib/sequel/adapters/litedb.rb +3 -2
  46. metadata +20 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d6db833e61264ea2036338102d0ed1d3e9eb0a5d94a235db5a8f8d4cf390c65f
4
- data.tar.gz: 5a48dd08c18ab39f2a19462025e351da8f3e2eedf55f779019cef3ed277e12bc
3
+ metadata.gz: 11ef03890b2883bd21fb443d959774f9932f56e927d2ee2a40a028713526ce9c
4
+ data.tar.gz: c531925eeffa84973475c14d4d230d8919d5e5ec095e5055f9bab6b0f94519e3
5
5
  SHA512:
6
- metadata.gz: a6dd503c63cb0ca49092f9f612a6c087d07b13eefa083ca930931b956e5cf81357f130932e2514b340d2e240b73df377bf3f5a55516b7bf444bbe4149c60656c
7
- data.tar.gz: a1749b7eba0f8d155fc5e18b7a2da8d26df25ee4bf954e8c15724b8cb8164fb9f23aeab9387793420714e27fd2e13d357a91ece891dfca49afb7dbf6f8a91564
6
+ metadata.gz: 82c0878fb57fa89290c6550ddbf5b393e436883252285dd78083ba7e96f78947aa3fdf3e92d96f3baec6b9b2b0d2edfdc470f4e1c544427f3da8db704179de27
7
+ data.tar.gz: 358bc249c3f1371714e12f4df0a0d82883e42eec14528f4faaef8a5e611e62af1039d8c00ebdc9d89be1a514a3bfd698ce3f988fa1b4f29eb1cf72fd1ca03b25
data/BENCHMARKS.md CHANGED
@@ -3,6 +3,8 @@
3
3
  This is a set of initial (simple) benchmars, designed to understand the baseline performance for different litestack components against their counterparts.
4
4
  These are not real life scenarios and I hope I will be able to produce some interesting ones soon.
5
5
 
6
+ All these benchmarks were run on an 8 core, 16 thread, AMD 5700U based laptop, in a Virtual Box VM
7
+
6
8
  > ![litedb](https://github.com/oldmoe/litestack/blob/master/assets/litedb_logo_teal.png?raw=true)
7
9
 
8
10
  ### Point Read
@@ -109,5 +111,14 @@ Two scenarios were benchmarked, an empty job and one with a 100ms sleep to simul
109
111
 
110
112
  Running Litejob with fibers is producing much faster results than any threaded solution. Still though, threaded Litejob remains ahead of Sidekiq in all scenarios.
111
113
 
114
+ > ![litecable](https://github.com/oldmoe/litestack/blob/master/assets/litecable_logo_teal.png?raw=true)
115
+
116
+ A client written using the Iodine web server was used to generate the WS load in an event driven fashion. The Rails application, the Iodine based load generator and the Redis server were all run on the same machine to exclude network overheads (Redis still pays for the TCP stack overhead though)
112
117
 
118
+ |Requests|Redis Req/Sec|Litestack Req/sec|Redis p90 Latency (ms)|Litestack p90 Latency (ms)|Redis p99 Latency (ms)|Litestack p99 Latancy (ms)|
119
+ |-:|-:|-:|-:|-:|-:|-:|
120
+ |1,000|2611|3058|34|27|153|78|
121
+ |10,000|3110|5328|81|40|138|122
122
+ |100,000|3403|5385|41|36|153|235
113
123
 
124
+ On average, Litecable is quite faster than the Redis based version and offers better latenices for over 90% of the requests, though Redis usually delivers better p99 latencies,
data/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.4.1] - 2023-10-11
4
+
5
+ - Add missing Litesearch::Model dependency
6
+
7
+ ## [0.4.0] - 2023-10-11
8
+
9
+ - Introduced Litesearch, dynamic & fast full text search capability for Litedb
10
+ - ActiveRecord and Sequel integration for Litesearch
11
+ - Slight improvement to the Sequel Litedb adapter for better Litesearch integration
12
+
13
+ ## [0.3.0] - 2023-08-13
14
+
15
+ - Reworked the Litecable thread safety model
16
+ - Fixed multiple litejob bugs (thanks Stephen Margheim)
17
+ - Fixed Railtie dependency (thanks Marco Roth)
18
+ - Litesupport fixes (thanks Stephen Margheim)
19
+ - Much improved metrics reporting for Litedb, Litecache, Litejob & Litecable
20
+ - Removed (for now, will come again later) litemetric reporting support for ad-hoc modules
21
+
3
22
  ## [0.2.6] - 2023-07-16
4
23
 
5
24
  - Much improved database location setting (thanks Brad Gessler)
data/Gemfile CHANGED
@@ -8,3 +8,5 @@ gemspec
8
8
 
9
9
 
10
10
  gem "rack", "~> 3.0"
11
+
12
+ gem "simplecov"
data/README.md CHANGED
@@ -183,7 +183,7 @@ production:
183
183
 
184
184
  ## Contributing
185
185
 
186
- Bug reports are welcome on GitHub at https://github.com/oldmoe/litestack. Please note that this is not an open contribution project and that we don't accept pull requests.
186
+ Bug reports and pull requests are welcome on GitHub at https://github.com/oldmoe/litestack.
187
187
 
188
188
  ## License
189
189
 
Binary file
Binary file
Binary file
@@ -29,7 +29,7 @@ if env == "a" # threaded
29
29
  end
30
30
 
31
31
  require_relative '../lib/active_job/queue_adapters/litejob_adapter'
32
- puts Litesupport.scheduler
32
+ puts Litescheduler.backend
33
33
 
34
34
  RailsJob.queue_adapter = :litejob
35
35
  t = Time.now.to_f
@@ -28,7 +28,7 @@ end
28
28
 
29
29
  require './uljob.rb'
30
30
 
31
- STDERR.puts "litejob started in #{Litesupport.scheduler} environmnet"
31
+ STDERR.puts "litejob started in #{Litescheduler.backend} environmnet"
32
32
 
33
33
  t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
34
34
  bench("enqueuing litejobs", count) do |i|
data/bench/uljob.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require './bench'
2
- require '../lib/litestack'
2
+ require '../lib/litestack/litejob'
3
3
 
4
4
  class MyJob
5
5
  include Litejob
@@ -10,20 +10,10 @@ module ActionCable
10
10
 
11
11
  prepend ChannelPrefix
12
12
 
13
- DEFAULT_OPTIONS = {
14
- config_path: "./config/litecable.yml",
15
- path: "./db/cable.db",
16
- sync: 0, # no need to sync at all
17
- mmap_size: 16 * 1024 * 1024, # 16MB of memory hold hot messages
18
- expire_after: 10, # remove messages older than 10 seconds
19
- listen_interval: 0.005, # check new messages every 5 milliseconds
20
- metrics: false
21
- }
22
-
23
13
  def initialize(server, logger=nil)
24
14
  @server = server
25
15
  @logger = server.logger
26
- super(DEFAULT_OPTIONS.dup)
16
+ super({config_path: "./config/litecable.yml"})
27
17
  end
28
18
 
29
19
  def shutdown
@@ -48,7 +48,7 @@ module ActiveSupport
48
48
  @cache.prune(limit)
49
49
  end
50
50
 
51
- def clear()
51
+ def clear(options = nil)
52
52
  @cache.clear
53
53
  end
54
54
 
@@ -6,10 +6,14 @@
6
6
  #
7
7
  # `Litesupport.root.join("data.sqlite3")` stores
8
8
  # application data in the path `./db/#{Rails.env}/data.sqlite3`
9
+ #
10
+ # idle_timeout should be set to zero, to avoid recycling sqlite connections
11
+ # and losing the page cache
12
+ #
9
13
  default: &default
10
14
  adapter: litedb
11
15
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
12
- timeout: 5000
16
+ idle_timeout: 0
13
17
  database: <%= Litesupport.root.join("data.sqlite3") %>
14
18
 
15
19
  development:
@@ -16,13 +16,21 @@ class Liteboard
16
16
  get "/", to: ->(env) do
17
17
  Liteboard.new(env).call(:index)
18
18
  end
19
-
20
- get "/topics/:topic", to: ->(env) do
21
- Liteboard.new(env).call(:topic)
19
+
20
+ get "/topics/Litejob", to: ->(env) do
21
+ Liteboard.new(env).call(:litejob)
22
+ end
23
+
24
+ get "/topics/Litecache", to: ->(env) do
25
+ Liteboard.new(env).call(:litecache)
22
26
  end
23
27
 
24
- get "/topics/:topic/events/:event", to: ->(env) do
25
- Liteboard.new(env).call(:event)
28
+ get "/topics/Litedb", to: ->(env) do
29
+ Liteboard.new(env).call(:litedb)
30
+ end
31
+
32
+ get "/topics/Litecable", to: ->(env) do
33
+ Liteboard.new(env).call(:litecable)
26
34
  end
27
35
 
28
36
  end
@@ -45,12 +53,6 @@ class Liteboard
45
53
  after(res)
46
54
  end
47
55
 
48
- def render(tpl_name)
49
- layout = Tilt.new("#{__dir__}/views/layout.erb")
50
- tpl = Tilt.new("#{__dir__}/views/#{tpl_name.to_s}.erb")
51
- res = layout.render(self){tpl.render(self)}
52
- end
53
-
54
56
  def after(body=nil)
55
57
  [200, {'Cache-Control' => 'no-cache'}, [body]]
56
58
  end
@@ -76,48 +78,150 @@ class Liteboard
76
78
  end
77
79
  @search = params(:search)
78
80
  @search = nil if @search == ''
81
+ @topics = @lm.topic_summaries(@resolution, @step * @count, @order, @dir, @search)
79
82
  end
80
83
 
81
84
  def index
82
85
  @order = 'topic' unless @order
83
- topics = @lm.topics
84
- @topics = @lm.topic_summaries(@resolution, @step * @count, @order, @dir, @search)
85
86
  @topics.each do |topic|
86
87
  data_points = @lm.topic_data_points(@step, @count, @resolution, topic[0])
87
- topic << data_points.collect{|r| [r[0],r[2]]}
88
+ topic << data_points.collect{|r| [r[0],r[2] || 0]}
88
89
  end
89
90
  render :index
90
91
  end
91
92
 
92
- def topic
93
+ def litecache
93
94
  @order = 'rcount' unless @order
94
- @topic = params(:topic)
95
+ @topic = 'Litecache'
95
96
  @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
96
97
  @events.each do |event|
97
- data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event[0])
98
- event << data_points.collect{|r| [r[0],r[2]]}
99
- event << data_points.collect{|r| [r[0],r[3]]}
98
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event['name'])
99
+ event['counts'] = data_points.collect{|r| [r['rtime'],r['rcount']]}
100
+ event['values'] = data_points.collect{|r| [r['rtime'],r['ravg']]}
100
101
  end
101
- @snapshot = @lm.snapshot(@topic)
102
- if @snapshot.empty?
103
- @snapshot = []
104
- else
105
- @snapshot[0] = Oj.load(@snapshot[0]) unless @snapshot[0].nil?
102
+ @snapshot = read_snapshot(@topic)
103
+ @size = @snapshot[0][:summary][:size] rescue 0
104
+ @max_size = @snapshot[0][:summary][:max_size] rescue 0
105
+ @full = (@size / @max_size)*100 rescue 0
106
+ @entries = @snapshot[0][:summary][:entries] rescue 0
107
+ @gets = @events.find{|t| t['name'] == 'get'}
108
+ @sets = @events.find{|t| t['name'] == 'set'}
109
+ @reads = @gets['rcount'] rescue 0
110
+ @writes = @sets['rcount'] rescue 0
111
+ @hitrate = @gets['ravg'] rescue 0
112
+ @hits = @reads * @hitrate
113
+ @misses = @reads - @hits
114
+ @reads_vs_writes = @gets['counts'].collect.with_index{|obj, i| obj.clone << @sets['counts'][i][1] } rescue []
115
+ @hits_vs_misses = @gets['values'].collect.with_index{|obj, i| [obj[0], obj[1].to_f * @gets['counts'][i][1].to_f, (1 - obj[1].to_f) * @gets['counts'][i][1].to_f] } rescue []
116
+ @top_reads = @lm.keys_summaries(@topic, 'get', @resolution, @order, @dir, nil, @step * @count).first(8)
117
+ @top_writes = @lm.keys_summaries(@topic, 'set', @resolution, @order, @dir, nil, @step * @count).first(8)
118
+ render :litecache
119
+ end
120
+
121
+ def litedb
122
+ @order = 'rcount' unless @order
123
+ @topic = 'Litedb'
124
+ @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
125
+ @events.each do |event|
126
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event['name'])
127
+ event['counts'] = data_points.collect{|r| [r['rtime'],r['rcount'] || 0]}
128
+ event['values'] = data_points.collect{|r| [r['rtime'],r['rtotal'] || 0]}
106
129
  end
107
- render :topic
130
+ @snapshot = read_snapshot(@topic)
131
+ @size = @snapshot[0][:summary][:size] rescue 0
132
+ @tables = @snapshot[0][:summary][:tables] rescue 0
133
+ @indexes = @snapshot[0][:summary][:indexes] rescue 0
134
+ @gets = @events.find{|t| t['name'] == 'Read'}
135
+ @sets = @events.find{|t| t['name'] == 'Write'}
136
+ @reads = @gets['rcount'] rescue 0
137
+ @writes = @sets['rcount'] rescue 0
138
+ @time = @gets['ravg'] rescue 0
139
+ @reads_vs_writes = @gets['counts'].collect.with_index{|obj, i| obj.clone << @sets['counts'][i][1] } rescue []
140
+ @reads_vs_writes_times = @gets['values'].collect.with_index{|obj, i| [obj[0], obj[1], @sets['values'][i][1].to_f] } rescue []
141
+ @read_times = @gets['rtotal'] rescue 0
142
+ @write_times = @sets['rtotal'] rescue 0
143
+ @slowest = @lm.keys_summaries(@topic, 'Read', @resolution, 'ravg', 'desc', nil, @step * @count).first(8)
144
+ @slowest += @lm.keys_summaries(@topic, 'Write', @resolution, 'ravg', 'desc', nil, @step * @count).first(8)
145
+ @slowest = @slowest.sort{|a, b| a['ravg'] <=> b['ravg']}.reverse.first(8)
146
+ @popular = @lm.keys_summaries(@topic, 'Read', @resolution, 'rtotal', 'desc', nil, @step * @count).first(8)
147
+ @popular += @lm.keys_summaries(@topic, 'Write', @resolution, 'rtotal', 'desc', nil, @step * @count).first(8)
148
+ @popular = @popular.sort{|a, b| a['rtotal'] <=> b['rtotal']}.reverse.first(8)
149
+ render :litedb
108
150
  end
109
151
 
110
- def event
152
+ def litejob
111
153
  @order = 'rcount' unless @order
112
- @topic = params(:topic)
113
- @event = params(:event)
114
- @keys = @lm.keys_summaries(@topic, @event, @resolution, @order, @dir, @search, @step * @count)
115
- @keys.each do |key|
116
- data_points = @lm.key_data_points(@step, @count, @resolution, @topic, @event, key[0])
117
- key << data_points.collect{|r| [r[0],r[2]]}
118
- key << data_points.collect{|r| [r[0],r[3]]}
119
- end
120
- render :event
154
+ @topic = 'Litejob'
155
+ @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
156
+ @events.each do |event|
157
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event['name'])
158
+ event['counts'] = data_points.collect{|r| [r['rtime'],r['rcount'] || 0]}
159
+ event['values'] = data_points.collect{|r| [r['rtime'],r['rtotal'] || 0]}
160
+ end
161
+ @snapshot = read_snapshot(@topic)
162
+ @size = @snapshot[0][:summary][:size] rescue 0
163
+ @jobs = @snapshot[0][:summary][:jobs] rescue 0
164
+ @queues = @snapshot[0][:queues] rescue {}
165
+ @processed_jobs = @events.find{|e|e['name'] == 'perform'}
166
+ @processed_count = @processed_jobs['rcount'] rescue 0
167
+ @processing_time = @processed_jobs['rtotal'] rescue 0
168
+ keys_summaries = @lm.keys_summaries(@topic, 'perform', @resolution, 'rcount', 'desc', nil, @step * @count)
169
+ @processed_count_by_queue = keys_summaries.collect{|r|[r['key'], r['rcount']]}
170
+ @processing_time_by_queue = keys_summaries.collect{|r|[r['key'], r['rtotal']]} #.sort{|r1, r2| r1['rtotal'] > r2['rtotal'] }
171
+ @processed_count_over_time = @events.find{|e| e['name'] == 'perform'}['counts'] rescue []
172
+ @processing_time_over_time = @events.find{|e| e['name'] == 'perform'}['values'] rescue []
173
+ @processed_count_over_time_by_queues = []
174
+ @processing_time_over_time_by_queues = []
175
+ keys = ['Time']
176
+ keys_summaries.each_with_index do |summary,i|
177
+ key = summary['key']
178
+ keys << key
179
+ data_points = @lm.key_data_points(@step, @count, @resolution, @topic, 'perform', key)
180
+ if i == 0
181
+ data_points.each do |dp|
182
+ @processed_count_over_time_by_queues << [dp['rtime']]
183
+ @processing_time_over_time_by_queues << [dp['rtime']]
184
+ end
185
+ end
186
+ data_points.each_with_index do |dp, j|
187
+ @processed_count_over_time_by_queues[j] << (dp['rcount'] || 0)
188
+ @processing_time_over_time_by_queues[j] << (dp['rtotal'] || 0)
189
+ end
190
+ end
191
+ @processed_count_over_time_by_queues.unshift(keys)
192
+ @processing_time_over_time_by_queues.unshift(keys)
193
+ render :litejob
194
+ end
195
+
196
+ def litecable
197
+ @order = 'rcount' unless @order
198
+ @topic = 'Litecable'
199
+ @events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
200
+ @events.each do |event|
201
+ data_points = @lm.event_data_points(@step, @count, @resolution, @topic, event['name'])
202
+ event['counts'] = data_points.collect{|r| [r['rtime'],r['rcount'] || 0]}
203
+ end
204
+
205
+ @subscription_count = @events.find{|t| t['name'] == 'subscribe'}['rcount'] rescue 0
206
+ @broadcast_count = @events.find{|t| t['name'] == 'broadcast'}['rcount'] rescue 0
207
+ @message_count = @events.find{|t| t['name'] == 'message'}['rcount'] rescue 0
208
+
209
+ @subscriptions_over_time = @events.find{|t| t['name'] == 'subscribe'}['counts'] rescue []
210
+ @broadcasts_over_time = @events.find{|t| t['name'] == 'broadcast'}['counts'] rescue []
211
+ @messages_over_time = @events.find{|t| t['name'] == 'message'}['counts'] rescue []
212
+ @messages_over_time = @messages_over_time.collect.with_index{|msg, i| [msg[0], @broadcasts_over_time[i][1], msg[1]]}
213
+
214
+ @top_subscribed_channels = @lm.keys_summaries(@topic, 'subscribe', @resolution, @order, @dir, @search, @step * @count).first(8)
215
+ @top_messaged_channels = @lm.keys_summaries(@topic, 'message', @resolution, @order, @dir, @search, @step * @count).first(8)
216
+ render :litecable
217
+ end
218
+
219
+ def index_url
220
+ "/?res=#{@res}&order=#{@order}&dir=#{@dir}&search=#{@search}"
221
+ end
222
+
223
+ def topic_url(topic)
224
+ "/topics/#{encode(topic)}?res=#{@res}&order=#{@order}&dir=#{@dir}&search=#{@search}"
121
225
  end
122
226
 
123
227
  def index_sort_url(field)
@@ -156,10 +260,43 @@ class Liteboard
156
260
  URI.encode_uri_component(text)
157
261
  end
158
262
 
263
+ def round(float)
264
+ return 0 unless float.is_a? Numeric
265
+ ((float * 100).round).to_f / 100
266
+ end
267
+
268
+ def format(float)
269
+ string = float.to_s
270
+ whole, decimal = string.split('.')
271
+ whole = whole.split('').reverse.each_slice(3).map(&:join).join(',').reverse
272
+ whole = [whole, decimal].join('.') if decimal
273
+ whole
274
+ end
275
+
159
276
  def self.app
160
277
  @@app
161
278
  end
162
279
 
280
+ private
281
+
282
+ def read_snapshot(topic)
283
+ snapshot = @lm.snapshot(topic)
284
+ if snapshot.empty?
285
+ snapshot = []
286
+ else
287
+ snapshot[0] = Oj.load(snapshot[0]) unless snapshot[0].nil?
288
+ end
289
+ snapshot
290
+ end
291
+
292
+ def render(tpl_name)
293
+ layout = Tilt.new("#{__dir__}/views/layout.erb")
294
+ tpl_path = "#{__dir__}/views/#{tpl_name.to_s}.erb"
295
+ tpl = Tilt.new(tpl_path)
296
+ res = layout.render(self){tpl.render(self)}
297
+ end
298
+
299
+
163
300
  end
164
301
 
165
302
 
@@ -1,22 +1,54 @@
1
1
 
2
- <h5>All topics</h5>
3
- <div id="search"><form><input id="search-field" type="text" placeholder="Search topics" onkeydown="search_kd(this)" onkeyup="search_ku(this)" value="<%=@search%>"/></form></div>
4
- <table class="sortable table">
5
- <tr>
6
- <th width="20%" class="<%='sorted' if @order == 'topic'%>"><a href="<%=index_sort_url('topic')%>">Topics</a> <%=dir('topic')%></th>
7
- <th width="20%" class="<%='sorted' if @order == 'rcount'%>"><a href="<%=index_sort_url('rcount')%>">Total events</a> <%=dir('rcount')%></th>
8
- <th width="40%">Events over time</th>
9
- </tr>
10
- <% @topics.each do |topic|%>
11
- <tr>
12
- <td title="<%=topic[0]%>"><div class="label"><a href="./topics/<%=encode(topic[0])%>?res=<%=@res%>"><%=topic[0]%></a></div></td>
13
- <td><%=topic[3]%></td>
14
- <td class="chart"><span class="inlinecolumn hidden" data-label="Count"><%=Oj.dump(topic[4]) if topic[4]%></span></td>
15
- </tr>
16
- <%end%>
17
- <% if @topics.empty? %>
18
- <tr>
19
- <td class="empty" colspan="5">No data to display</td>
20
- </tr>
21
- <% end %>
22
2
 
3
+
4
+
5
+ <div class="container">
6
+ <% @topics.each do |topic|%>
7
+
8
+ <div class = "row justify-content-center">
9
+
10
+ <div class = "col-6">
11
+ <div class="card">
12
+ <div class="card-header">
13
+ <a href="./topics/<%=encode(topic[0])%>?res=<%=@res%>"><%=topic[0]%></a>
14
+ </div>
15
+ <div class="card-body">
16
+ <div class="container">
17
+ <div class= "row">
18
+ <div class= "col">
19
+ <h1><%=topic[3]%> <span class="fs-4">events</span></h1>
20
+ </div>
21
+ <div class= "col">
22
+ <span class="inlineminicolumn hidden" data-label="Count"><%=Oj.dump(topic[4].unshift(['Time', 'Count'])) if topic[4]%></span>
23
+ </div>
24
+ </div>
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </div>
29
+
30
+ </div>
31
+
32
+ <div class = "row">
33
+ &nbsp;<br/>
34
+ </div>
35
+
36
+
37
+ <%end%>
38
+ <% if @topics.empty? %>
39
+ <div class = "row justify-content-center">
40
+
41
+ <div class = "col-6">
42
+ <div class="card">
43
+ <div class="card-header">
44
+ Topics
45
+ </div>
46
+ <div class="card-body justify-content-center">
47
+ No data to display
48
+ </div>
49
+ </div>
50
+ </div>
51
+
52
+ </div>
53
+ <%end%>
54
+ </div>