litestack 0.2.3 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Gemfile +2 -0
  4. data/README.md +5 -11
  5. data/assets/event_page.png +0 -0
  6. data/assets/index_page.png +0 -0
  7. data/assets/topic_page.png +0 -0
  8. data/bench/bench_jobs_rails.rb +1 -1
  9. data/bench/bench_jobs_raw.rb +1 -1
  10. data/bin/liteboard +81 -0
  11. data/lib/action_cable/subscription_adapter/litecable.rb +1 -11
  12. data/lib/generators/litestack/install/USAGE +11 -0
  13. data/lib/generators/litestack/install/install_generator.rb +35 -0
  14. data/lib/generators/litestack/install/templates/cable.yml +11 -0
  15. data/lib/generators/litestack/install/templates/database.yml +34 -0
  16. data/lib/litestack/liteboard/liteboard.rb +305 -0
  17. data/lib/litestack/liteboard/views/event.erb +32 -0
  18. data/lib/litestack/liteboard/views/index.erb +54 -0
  19. data/lib/litestack/liteboard/views/layout.erb +303 -0
  20. data/lib/litestack/liteboard/views/litecable.erb +118 -0
  21. data/lib/litestack/liteboard/views/litecache.erb +144 -0
  22. data/lib/litestack/liteboard/views/litedb.erb +168 -0
  23. data/lib/litestack/liteboard/views/litejob.erb +151 -0
  24. data/lib/litestack/liteboard/views/topic.erb +48 -0
  25. data/lib/litestack/litecable.rb +25 -35
  26. data/lib/litestack/litecable.sql.yml +1 -1
  27. data/lib/litestack/litecache.rb +31 -28
  28. data/lib/litestack/litedb.rb +124 -1
  29. data/lib/litestack/litejob.rb +2 -2
  30. data/lib/litestack/litejobqueue.rb +8 -8
  31. data/lib/litestack/litemetric.rb +177 -88
  32. data/lib/litestack/litemetric.sql.yml +312 -42
  33. data/lib/litestack/litemetric_collector.sql.yml +56 -0
  34. data/lib/litestack/litequeue.rb +28 -29
  35. data/lib/litestack/litequeue.sql.yml +11 -0
  36. data/lib/litestack/litesupport.rb +137 -57
  37. data/lib/litestack/railtie.rb +10 -0
  38. data/lib/litestack/version.rb +1 -1
  39. data/lib/litestack.rb +1 -0
  40. data/lib/sequel/adapters/litedb.rb +1 -1
  41. data/template.rb +7 -0
  42. metadata +81 -5
  43. data/lib/litestack/metrics_app.rb +0 -5
@@ -0,0 +1,54 @@
1
+
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>
@@ -0,0 +1,303 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>liteboard</title>
5
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script>
6
+ <script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
8
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Antonio">
9
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
+ <style>
11
+ body {background-color: #fff}
12
+ input, select { border-radius: 3px}
13
+ div#header { width: 100%; border-bottom: 1px solid #089;}
14
+ div.label {max-width:400px;overflow:hidden}
15
+ h1 { font-family: antonio }
16
+ #content{ padding-right: 12px; padding-left: 12px; padding-bottom: 60px;}
17
+ table.head { margin-top: 12px; margin-bottom:12px}
18
+ select { color: #078; background-color: #fff; font-weight: normal }
19
+ .table th { color: #078; font-weight: normal; }
20
+ .table th.sorted { font-weight: bold }
21
+ .table td { color: #444; vertical-align:middle; font-size: 18px}
22
+ .table td:first-child { color: #444; vertical-align:middle; font-size: 15px; font-weight:normal}
23
+ .table td.empty { text-align:center}
24
+ a, a.nav-link { color: #078; }
25
+ .nav-pills .nav-link.active { color: #fff; background-color: #078}
26
+ a .logo { color: #000;}
27
+ a:visited { color: #078; }
28
+ .hidden { display: none}
29
+ div#search {margin-bottom: 8px}
30
+ div#footer {position:fixed; left:0px; height: 40px; width:100%; background-color:#0891; border-top: #0893 1px solid; padding: 8px; bottom: 0; text-align: right}
31
+ .logo{font-family: antonio}
32
+ .logo-half{ color: #078 }
33
+ .smaller { font-size: 24px; font-weight: normal}
34
+ .token {background-color: #ed9}
35
+ svg > g > g.google-visualization-tooltip { pointer-events : none }
36
+ .material-icons { vertical-align: middle}
37
+ </style>
38
+ </head>
39
+ <body>
40
+ <div id="content">
41
+ <div id="header">
42
+ <h1><span class="logo"><span class="logo-half">lite</span>board | </span> <span class="logo smaller">the <span class="logo-half">lite</span>metric dashboard</span></span></h1>
43
+ </div>
44
+ <div class="container">
45
+ <div class = "row">
46
+ &nbsp;<br/>
47
+ </div>
48
+ <div class="row">
49
+ <div class="col">
50
+ <nav class="navbar bg-body-tertiary">
51
+ <div>&nbsp;&nbsp;Showing data for the last <select onchange="window.location = locationWithParam('res', this.value)">
52
+ <%= mapping = {'hour' => '60 minutes', 'day' => '24 hours', 'week' => '7 days', 'year' => '52 weeks'}%>
53
+ <% ['hour', 'day', 'week', 'year'].each do |res| %>
54
+ <option value=<%=res%> <%='selected' if res == @res%>><%=mapping[res]%></option>
55
+ <% end %>
56
+ </select></div>
57
+ </nav>
58
+ </div>
59
+ </div>
60
+ <div class = "row">
61
+ &nbsp;<br/>
62
+ </div>
63
+ <%= yield %>
64
+
65
+ </div>
66
+
67
+ <div class="container" style="position: fixed; left:0px; top: 145px">
68
+ <div class="row justify-content-center">
69
+ <div class="col">
70
+ <div class="card" style="width: 15rem;">
71
+ <div class="card-body">
72
+ <ul class="nav nav-pills nav-fill flex-column list-group list-group-flush">
73
+ <li class="list-group-item"><a class="nav-link <%='active' unless @topic%>" href="<%=index_url%>">Home</a></li>
74
+ <%@topics.each do |topic|%>
75
+ <li class="list-group-item"><a class="nav-link <%='active' if @topic == topic[0]%>" href="<%=topic_url(topic[0])%>"><%=topic[0]%></a></li>
76
+ <%end%>
77
+ </ul>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ </div>
85
+ <div id="footer">
86
+ Powered by <a href="https://www.github.com/oldmoe/litestack" target="_blank"><span class="logo"><span class="logo-half">lite</span>stack</span></a>
87
+ </div>
88
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
89
+ </body>
90
+ <script>
91
+ google.charts.load('current', {'packages':['corechart', 'bar']});
92
+
93
+ google.charts.setOnLoadCallback(drawMiniColumnChart);
94
+ google.charts.setOnLoadCallback(drawColumnChart);
95
+ google.charts.setOnLoadCallback(drawPieChart);
96
+ google.charts.setOnLoadCallback(drawStackedColumnChart);
97
+
98
+
99
+ function drawMiniColumnChart() {
100
+ elements = document.querySelectorAll(".inlineminicolumn")
101
+ elements.forEach(element => {
102
+ var label = element.dataset.label;
103
+ var mydata = eval(element.innerText)
104
+ element.innerText = ''
105
+ element.classList.remove("hidden")
106
+ if(mydata.length > 1) {
107
+ mydata.forEach(row => {
108
+ if(mydata[0].length == 5){ // we are doing custom tooltips
109
+ if(row[0] != "Time"){
110
+ row[1] = Number(row[1].toPrecision(4))
111
+ row[3] = Number(row[2].toPrecision(4))
112
+ row[2] = row[0]+': '+mydata[0][1]+' '+row[1]
113
+ row[4] = row[0]+': '+mydata[0][3]+' '+row[3]
114
+ }
115
+ }
116
+ })
117
+ var data = google.visualization.arrayToDataTable(
118
+ mydata
119
+ )
120
+ var options = {
121
+ animation: {'startup': true, 'duration': 300},
122
+ width: 300,
123
+ height: 70,
124
+ chartArea: {width:'100%', height: '100%'},
125
+ backgroundColor: 'none',
126
+ bar: {groupWidth: "61.8%"},
127
+ colors : ['#089', 'silver' ],
128
+ vAxis: {'gridlines': {'count' : 0}, 'textPosition' : 'none', 'baselineColor' : 'none'},
129
+ hAxis: { 'count' : 0, 'textPosition' : 'none', 'baselineColor' : 'none'},
130
+ legend: {'position': 'none'},
131
+ tooltip: {showColorCode: true, isHtml: true},
132
+ isStacked: true
133
+ }
134
+ var chart = new google.visualization.ColumnChart(element);
135
+ chart.draw(data, options);
136
+ }
137
+ })
138
+ }
139
+
140
+ function drawColumnChart() {
141
+ elements = document.querySelectorAll(".inlinecolumn")
142
+ elements.forEach(element => {
143
+ var label = element.dataset.label;
144
+ var mydata = eval(element.innerText)
145
+ element.innerText = ''
146
+ element.classList.remove("hidden")
147
+ if(mydata.length > 1) {
148
+ mydata.forEach(row => {
149
+ if(mydata[0].length == 5){ // we are doing custom tooltips
150
+ if(row[0] != "Time"){
151
+ row[1] = Number(row[1].toPrecision(4))
152
+ row[3] = Number(row[2].toPrecision(4))
153
+ row[2] = row[0]+': '+mydata[0][1]+' '+row[1]
154
+ row[4] = row[0]+': '+mydata[0][3]+' '+row[3]
155
+ }
156
+ }
157
+ })
158
+ var data = google.visualization.arrayToDataTable(
159
+ mydata
160
+ )
161
+ var options = {
162
+ animation: {'startup': true, 'duration': 300},
163
+ width: 550,
164
+ height: 350,
165
+ chartArea: {width:'100%', height: '80%'},
166
+ backgroundColor: 'none',
167
+ bar: {groupWidth: "61.8%"},
168
+ colors : ['#089', 'silver' ],
169
+ vAxis: {'gridlines': {'count' : 0}, 'textPosition' : 'none', 'baselineColor' : 'none'},
170
+ hAxis: { 'count' : 0, 'textPosition' : 'none', 'baselineColor' : 'none'},
171
+ legend: {'position': 'bottom'},
172
+ tooltip: {showColorCode: true, isHtml: true},
173
+ isStacked: true
174
+ }
175
+ var chart = new google.visualization.ColumnChart(element);
176
+ chart.draw(data, options);
177
+ }
178
+ })
179
+ }
180
+
181
+ function drawPieChart() {
182
+ elements = document.querySelectorAll(".inlinepie")
183
+ elements.forEach(element => {
184
+ var label = element.dataset.label;
185
+ var mydata = eval(element.innerText)
186
+ element.innerText = ''
187
+ element.classList.remove("hidden")
188
+ if(mydata.length >= 2) {
189
+ var data = google.visualization.arrayToDataTable(
190
+ mydata
191
+ )
192
+ var options = {
193
+ animation: {'startup': true, 'duration': 300},
194
+ annotations: {textStyle: {bold: true}, alwaysOutside: false },
195
+ width: 600,
196
+ height: 350,
197
+ backgroundColor: 'none',
198
+ bar: {groupWidth: "80%"},
199
+ colors : ['#089', 'silver', '#545B77' ],
200
+ axisTitlesPosition: 'none',
201
+ chartArea: {width:'90%', height: '85%'},
202
+ tooltip : {showColorCode: true},
203
+ vAxis: {gridlines: {count : 0}, textPosition : 'in', baselineColor : 'none', textStyle: {color: '#089', bold: true} },
204
+ hAxis: {gridlines: {count : 0}, textPosition : 'none', baselineColor : 'none'},
205
+ legend: {'position': 'bottom'},
206
+ isStacked: true,
207
+ bars: 'horizontal'
208
+ }
209
+ var chart = new google.visualization.PieChart(element);
210
+ if(mydata[1][1] == 0 && mydata[2][1] == 0){
211
+ return 0
212
+ }
213
+ chart.draw(data, options);
214
+ }
215
+ })
216
+ }
217
+
218
+
219
+ function drawStackedColumnChart() {
220
+ elements = document.querySelectorAll(".inlinestackedcolumn")
221
+ elements.forEach(element => {
222
+ var label = element.dataset.label;
223
+ var mydata = eval(element.innerText)
224
+ element.innerText = ''
225
+ element.classList.remove("hidden")
226
+ if(mydata.length > 1) {
227
+ console.log(mydata)
228
+ var data = google.visualization.arrayToDataTable(
229
+ mydata
230
+ )
231
+ var options = {
232
+ animation: {'startup': true, 'duration': 300},
233
+ annotations: {textStyle: {bold: true}, alwaysOutside: false },
234
+ width: 600,
235
+ height: 350,
236
+ backgroundColor: 'none',
237
+ bar: {groupWidth: "80%"},
238
+ colors : [ '#089', 'silver', '#545B77', 'silver'],
239
+ axisTitlesPosition: 'none',
240
+ chartArea: {width:'100%', height: '85%'},
241
+ tooltip : {showColorCode: true},
242
+ vAxis: {gridlines: {count : 0}, textPosition : 'none', baselineColor : 'none', textStyle: {color: '#089', bold: true} },
243
+ hAxis: {gridlines: {count : 0}, textPosition : 'none', baselineColor : 'none'},
244
+ legend: {'position': 'bottom'},
245
+ isStacked: true,
246
+ bars: 'horizontal'
247
+ }
248
+ var chart = new google.visualization.AreaChart(element);
249
+ chart.draw(data, options);
250
+ }
251
+ })
252
+ }
253
+
254
+
255
+ function search_kd(el){
256
+ //store the current value
257
+ el.oldvalue = el.value
258
+ }
259
+ function search_ku(el){
260
+ //check if the value has changed and if so
261
+ // set a new timer to fire a request in 300ms
262
+ // removing any existing timer first
263
+ if(el.value == el.oldvalue){
264
+ return
265
+ }else{
266
+ el.oldvalue = null
267
+ }
268
+ if(el.timeout){
269
+ window.clearTimeout(el.timeout)
270
+ }
271
+ el.timeout = window.setTimeout(function(){
272
+ el.timeout = null
273
+ window.location = locationWithParam('search', el.value)
274
+ }, 500)
275
+ }
276
+
277
+ $(document).ready(function(){
278
+ /*
279
+ el = $('#search-field')[0]
280
+ el.focus()
281
+ if(el.value && el.value.length > 0){
282
+ el.setSelectionRange(el.value.length, el.value.length)
283
+ var list = $("table.sortable div.label") //[0].children[0].children
284
+ for(var i=0; i < list.length; i++){
285
+ //console.log(list[i])
286
+ var link = list[i].children[0] //.children[0].children[0]
287
+ var re = new RegExp("("+el.value+")", "giu")
288
+ link.innerHTML = link.innerHTML.replaceAll(re, "<span class='token'>$1</span>") ;
289
+ }
290
+ }
291
+ */
292
+ })
293
+
294
+ function locationWithParam(param, value){
295
+ var query = window.location.search
296
+ var params = new URLSearchParams(query)
297
+ params.set(param, value)
298
+ var l = window.location
299
+ return l.origin + l.pathname + '?' + params.toString()
300
+ }
301
+ </script>
302
+ </html>
303
+
@@ -0,0 +1,118 @@
1
+ <div class = "row">
2
+
3
+ <div class = "col">
4
+ <div class="card">
5
+ <div class="card-header">
6
+ Subscriptions
7
+ </div>
8
+ <div class="card-body">
9
+ <h1><%=format(@subscription_count)%></h1>
10
+ </div>
11
+ </div>
12
+ </div>
13
+
14
+ <div class = "col">
15
+ <div class="card">
16
+ <div class="card-header">
17
+ Messages Received
18
+ </div>
19
+ <div class="card-body">
20
+ <h1><%=format(@broadcast_count)%></h1>
21
+ </div>
22
+ </div>
23
+ </div>
24
+
25
+ <div class = "col">
26
+ <div class="card">
27
+ <div class="card-header">
28
+ Messages delivered
29
+ </div>
30
+ <div class="card-body">
31
+ <h1><%=format(@message_count)%></h1>
32
+ </div>
33
+ </div>
34
+ </div>
35
+
36
+ </div>
37
+
38
+ <div class = "row">
39
+ &nbsp;<br/>
40
+ </div>
41
+
42
+ <div class="row">
43
+
44
+ <div class = "col">
45
+ <div class="card">
46
+ <div class="card-header">
47
+ Subscriptions over time
48
+ </div>
49
+ <div class="card-body">
50
+ <span class="hidden inlinecolumn">
51
+ <%=[["Time", "Count"]] + @subscriptions_over_time.to_a%>
52
+ </span>
53
+ </div>
54
+ </div>
55
+ </div>
56
+
57
+ <div class = "col">
58
+ <div class="card">
59
+ <div class="card-header">
60
+ Messages received/delivered over time
61
+ </div>
62
+ <div class="card-body">
63
+ <span class="hidden inlinestackedcolumn">
64
+ <%=[["Time", "Recieved Count", "Delivered Count"]] + @messages_over_time.to_a%>
65
+ </span>
66
+ </div>
67
+ </div>
68
+ </div>
69
+
70
+ </div>
71
+
72
+ <div class = "row">
73
+ &nbsp;<br/>
74
+ </div>
75
+
76
+ <div class = "row">
77
+
78
+ <div class = "col-6">
79
+ <div class="card">
80
+ <div class="card-header">
81
+ Channels with most subscriptions
82
+ </div>
83
+ <div class="card-body">
84
+ <table class="table">
85
+ <%@top_subscribed_channels.each do |r| %>
86
+ <tr>
87
+ <td><%=r['key']%></td>
88
+ <td align="right"><h6><%=format(r['rcount'])%>&nbsp;subs</h6></td>
89
+ </tr>
90
+ <% end %>
91
+ </table>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <div class = "col-6">
97
+ <div class="card">
98
+ <div class="card-header">
99
+ Channels with most messages delivered
100
+ </div>
101
+ <div class="card-body">
102
+ <table class="table">
103
+ <%@top_messaged_channels.each do |r| %>
104
+ <tr>
105
+ <td><%=r['key']%></td>
106
+ <td align="right"><h6><%=format(r['rcount'])%>&nbsp;messages</h6></td>
107
+ </tr>
108
+ <% end %>
109
+ </table>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ </div>
115
+
116
+ <div class = "row">
117
+ &nbsp;<br/>
118
+ </div>
@@ -0,0 +1,144 @@
1
+
2
+ <div class = "row">
3
+
4
+ <div class = "col">
5
+ <div class="card">
6
+ <div class="card-header">
7
+ Current size / Max size
8
+ </div>
9
+ <div class="card-body">
10
+ <h1><%=format(round(@size))%>MB / <%=format(round(@max_size))%>MB <span class="fs-4"><%=round(@full)%>% full</span></h1>
11
+ </div>
12
+ </div>
13
+ </div>
14
+
15
+ <div class = "col">
16
+ <div class="card">
17
+ <div class="card-header">
18
+ Number of entries
19
+ </div>
20
+ <div class="card-body">
21
+ <h1><%=format(@entries)%></h1>
22
+ </div>
23
+ </div>
24
+ </div>
25
+
26
+ </div>
27
+
28
+ <div class = "row">
29
+ &nbsp;<br/>
30
+ </div>
31
+
32
+ <div class = "row">
33
+
34
+ <div class = "col">
35
+ <div class="card">
36
+ <div class="card-header">
37
+ Reads / Writes
38
+ </div>
39
+ <div class="card-body">
40
+ <h1><%=format(@reads)%> / <%=format(@writes)%> <span class="fs-4"><%=round(@reads.to_f/@writes) rescue 0%> reads/write</span></h1>
41
+ <hr/>
42
+ <span class="hidden inlinepie"><%=[["name", "value"],["reads", @reads],["writes", @writes]]%></span>
43
+ </div>
44
+ </div>
45
+ </div>
46
+
47
+ <div class = "col">
48
+ <div class="card">
49
+ <div class="card-header">
50
+ Read hits / Misses
51
+ </div>
52
+ <div class="card-body">
53
+ <h1><%=format(@hits)%> / <%=format(@misses)%> <span class="fs-4"><%=round(@hitrate*100)%>% hit rate</span></h1>
54
+ <hr/>
55
+ <span class="hidden inlinepie"><%=[["name", "value"],["hits", @hits],["misses", @misses]]%></span>
56
+ </div>
57
+ </div>
58
+ </div>
59
+
60
+ </div>
61
+
62
+ <div class = "row">
63
+ &nbsp;<br/>
64
+ </div>
65
+
66
+ <div class = "row">
67
+
68
+ <div class = "col">
69
+ <div class="card">
70
+ <div class="card-header">
71
+ Reads, writes over time
72
+ </div>
73
+ <div class="card-body">
74
+ <div class="hidden inlinecolumn">
75
+ <%=Oj.dump([["Time", "Reads", "Writes"]] + @reads_vs_writes)%>
76
+ </div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <div class = "col">
82
+ <div class="card">
83
+ <div class="card-header">
84
+ Hits vs misses over time
85
+ </div>
86
+ <div class="card-body">
87
+ <span class="hidden inlinecolumn">
88
+ <%=Oj.dump([["Time", "Hits", "Misses"]] + @hits_vs_misses)%>
89
+ </span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ </div>
95
+
96
+ <div class = "row">
97
+ &nbsp;<br/>
98
+ </div>
99
+
100
+ <div class = "row">
101
+
102
+ <div class = "col">
103
+ <div class="card">
104
+ <div class="card-header">
105
+ Most read entries
106
+ </div>
107
+ <div class="card-body">
108
+ <table class="table">
109
+ <%@top_reads.each do |r| %>
110
+ <tr>
111
+ <td><%=r['key']%></td>
112
+ <td align="right"><h5><%=format(r['rcount'])%></h5></td>
113
+ </tr>
114
+ <% end %>
115
+ </table>
116
+ </div>
117
+ </div>
118
+ </div>
119
+
120
+ <div class = "col">
121
+ <div class="card">
122
+ <div class="card-header">
123
+ Most written entries
124
+ </div>
125
+ <div class="card-body">
126
+ <table class="table">
127
+ <%@top_writes.each do |r| %>
128
+ <tr>
129
+ <td><%=r['key']%></td>
130
+ <td align="right"><h5><%=format(r['rcount'])%></h5></td>
131
+ </tr>
132
+ <% end %>
133
+ </table>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ </div>
139
+
140
+ <div class = "row">
141
+ &nbsp;<br/>
142
+ </div>
143
+
144
+