litestack 0.2.3 → 0.2.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/Gemfile +2 -0
- data/README.md +4 -10
- data/bench/bench_jobs_rails.rb +1 -1
- data/bench/bench_jobs_raw.rb +1 -1
- data/bin/liteboard +81 -0
- data/lib/generators/litestack/install/USAGE +11 -0
- data/lib/generators/litestack/install/install_generator.rb +35 -0
- data/lib/generators/litestack/install/templates/cable.yml +11 -0
- data/lib/generators/litestack/install/templates/database.yml +30 -0
- data/lib/litestack/liteboard/liteboard.rb +168 -0
- data/lib/litestack/liteboard/views/event.erb +32 -0
- data/lib/litestack/liteboard/views/index.erb +22 -0
- data/lib/litestack/liteboard/views/layout.erb +152 -0
- data/lib/litestack/liteboard/views/topic.erb +48 -0
- data/lib/litestack/litecable.rb +1 -1
- data/lib/litestack/litecache.rb +25 -11
- data/lib/litestack/litedb.rb +115 -1
- data/lib/litestack/litejob.rb +2 -2
- data/lib/litestack/litejobqueue.rb +8 -8
- data/lib/litestack/litemetric.rb +185 -73
- data/lib/litestack/litemetric.sql.yml +307 -39
- data/lib/litestack/litemetric_collector.sql.yml +56 -0
- data/lib/litestack/litequeue.rb +20 -10
- data/lib/litestack/litequeue.sql.yml +11 -0
- data/lib/litestack/litesupport.rb +97 -38
- data/lib/litestack/railtie.rb +10 -0
- data/lib/litestack/version.rb +1 -1
- data/lib/litestack.rb +1 -0
- data/lib/sequel/adapters/litedb.rb +1 -1
- data/template.rb +7 -0
- metadata +74 -5
- data/lib/litestack/metrics_app.rb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d6db833e61264ea2036338102d0ed1d3e9eb0a5d94a235db5a8f8d4cf390c65f
|
4
|
+
data.tar.gz: 5a48dd08c18ab39f2a19462025e351da8f3e2eedf55f779019cef3ed277e12bc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a6dd503c63cb0ca49092f9f612a6c087d07b13eefa083ca930931b956e5cf81357f130932e2514b340d2e240b73df377bf3f5a55516b7bf444bbe4149c60656c
|
7
|
+
data.tar.gz: a1749b7eba0f8d155fc5e18b7a2da8d26df25ee4bf954e8c15724b8cb8164fb9f23aeab9387793420714e27fd2e13d357a91ece891dfca49afb7dbf6f8a91564
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,12 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.2.6] - 2023-07-16
|
4
|
+
|
5
|
+
- Much improved database location setting (thanks Brad Gessler)
|
6
|
+
- A Rails generator for better Rails Litestack defaults (thanks Brad Gessler)
|
7
|
+
- Revamped Litemetric, now much faster and more accurate (still experimental)
|
8
|
+
- Introduced Liteboard, a dashboard for viewing Litemetric data
|
9
|
+
|
3
10
|
## [0.2.3] - 2023-05-20
|
4
11
|
|
5
12
|
- Cut back on options defined in the Litejob Rails adapter
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -31,19 +31,13 @@ Litestack is still pretty young and under heavy development, but you are welcome
|
|
31
31
|
|
32
32
|
## Installation
|
33
33
|
|
34
|
-
Add
|
34
|
+
Add the `litestack` gem line to your application's Gemfile:
|
35
35
|
|
36
|
-
|
37
|
-
gem 'litestack'
|
38
|
-
```
|
39
|
-
|
40
|
-
And then execute:
|
41
|
-
|
42
|
-
$ bundle install
|
36
|
+
$ bundle add litestack
|
43
37
|
|
44
|
-
|
38
|
+
To configure a Rails application to run the full litestack, run:
|
45
39
|
|
46
|
-
$
|
40
|
+
$ rails generate litestack:install
|
47
41
|
|
48
42
|
## Usage
|
49
43
|
|
data/bench/bench_jobs_rails.rb
CHANGED
data/bench/bench_jobs_raw.rb
CHANGED
@@ -28,7 +28,7 @@ end
|
|
28
28
|
|
29
29
|
require './uljob.rb'
|
30
30
|
|
31
|
-
STDERR.puts "litejob started in #{Litesupport.
|
31
|
+
STDERR.puts "litejob started in #{Litesupport.scheduler} environmnet"
|
32
32
|
|
33
33
|
t = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
34
34
|
bench("enqueuing litejobs", count) do |i|
|
data/bin/liteboard
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'optparse'
|
5
|
+
require_relative '../lib/litestack/liteboard/liteboard'
|
6
|
+
DEFAULTS = {
|
7
|
+
config_path: Litemetric::DEFAULT_OPTIONS[:config_path],
|
8
|
+
path: Litemetric::DEFAULT_OPTIONS[:path]
|
9
|
+
}
|
10
|
+
|
11
|
+
options = {
|
12
|
+
config_path: nil,
|
13
|
+
path: nil,
|
14
|
+
deamonize: false,
|
15
|
+
Port: 9292,
|
16
|
+
Host: 'localhost',
|
17
|
+
environment: 'production',
|
18
|
+
quiet: false
|
19
|
+
}
|
20
|
+
|
21
|
+
OptionParser.new do |parser|
|
22
|
+
parser.banner = "Usage: liteboard [options]"
|
23
|
+
parser.on("-d", "--database PATH", "path to SQLite file (default: #{DEFAULTS[:path]})") { |v| options[:path] = v }
|
24
|
+
parser.on("-c", "--config PATH", "path to a litemetric config file (default: #{DEFAULTS[:config_path]}) ") { |v| options[:config_path] = v }
|
25
|
+
parser.on("-s", "--server SERVER", "use SERVER (e.g. puma/falcon/iodine)") { |v| options[:port] = v }
|
26
|
+
parser.on("-H", "--host HOST", "listen on HOST (default: #{options[:Host]})") { |v| options[:Host] = v }
|
27
|
+
parser.on("-p", "--port PORT", "use PORT (default: #{options[:Port]})") { |v| options[:Port] = v.to_i rescue options[:Port] }
|
28
|
+
parser.on("-D", "--deamonize", "run in the background") { |v| options[:deamonize] = true }
|
29
|
+
parser.on("-E", "--env ENVIRONMENT", "which environment to use (default: #{options[:environment]})") { |v| options[:environment] = v }
|
30
|
+
parser.on("-q", "--quiet", "turn off logging") { |v| options[:quiet] = true }
|
31
|
+
parser.on("-h", "--help", "print this message") do
|
32
|
+
puts parser
|
33
|
+
exit
|
34
|
+
end
|
35
|
+
end.parse!
|
36
|
+
|
37
|
+
def check_database(path)
|
38
|
+
unless File.exist?(path)
|
39
|
+
puts "liteboard: missing database file, please ensure the db path is correct"
|
40
|
+
puts "liteboard: exiting"
|
41
|
+
exit
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
check_database(options[:path]) if options[:path]
|
46
|
+
|
47
|
+
# if there is a config file then we need to check it
|
48
|
+
config = nil
|
49
|
+
if options[:config_path]
|
50
|
+
begin
|
51
|
+
config = Yaml.load_file(options[:config_path])
|
52
|
+
rescue
|
53
|
+
puts "liteboard: missing or bad config file, please ensure the config file path is correct"
|
54
|
+
puts "liteboard: exiting"
|
55
|
+
exit
|
56
|
+
end
|
57
|
+
else # no config path! use the default
|
58
|
+
config = Yaml.load_file(DEFAULTS[:config_path]) rescue nil
|
59
|
+
end
|
60
|
+
|
61
|
+
if config
|
62
|
+
if options[:path].nil?
|
63
|
+
path = config['path'] || config[options[:environment]]['path']
|
64
|
+
options[:path] = path
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# if still no path we assume a default db path
|
69
|
+
options[:path] = DEFAULTS[:path] if options[:path].nil?
|
70
|
+
|
71
|
+
|
72
|
+
# check the validity of the path before starting the server
|
73
|
+
check_database(options[:path])
|
74
|
+
Litemetric.options = options
|
75
|
+
litemetric = Litemetric.instance
|
76
|
+
options[:app] = Liteboard.app
|
77
|
+
|
78
|
+
require_relative '../lib/litestack'
|
79
|
+
puts "Starting Liteboard version #{Litestack::VERSION}"
|
80
|
+
|
81
|
+
Rack::Server.start(options)
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Description:
|
2
|
+
Installs litestack gem and configures the project to use litestack
|
3
|
+
in development, test, and production environments.
|
4
|
+
|
5
|
+
Example:
|
6
|
+
bin/rails generate litestack:install
|
7
|
+
|
8
|
+
This will modify:
|
9
|
+
config/environments/production.rb
|
10
|
+
config/database.yml
|
11
|
+
config/cable.yml
|
@@ -0,0 +1,35 @@
|
|
1
|
+
class Litestack::InstallGenerator < Rails::Generators::Base
|
2
|
+
source_root File.expand_path("templates", __dir__)
|
3
|
+
|
4
|
+
# Force copy configuratioon files so Rails installs don't ask questions
|
5
|
+
# that less experienced people might not understand. The more Sr folks.
|
6
|
+
# will know to check git to look at what changed.
|
7
|
+
def modify_database_adapater
|
8
|
+
copy_file "database.yml", "config/database.yml", force: true
|
9
|
+
end
|
10
|
+
|
11
|
+
def modify_action_cable_adapter
|
12
|
+
copy_file "cable.yml", "config/cable.yml", force: true
|
13
|
+
end
|
14
|
+
|
15
|
+
def modify_cache_store_adapter
|
16
|
+
gsub_file "config/environments/production.rb",
|
17
|
+
"# config.cache_store = :mem_cache_store",
|
18
|
+
"config.cache_store = :litecache"
|
19
|
+
end
|
20
|
+
|
21
|
+
def modify_active_job_adapter
|
22
|
+
gsub_file "config/environments/production.rb",
|
23
|
+
"# config.active_job.queue_adapter = :resque",
|
24
|
+
"config.active_job.queue_adapter = :litejob"
|
25
|
+
end
|
26
|
+
|
27
|
+
def modify_gitignore
|
28
|
+
append_file ".gitignore", <<~TEXT
|
29
|
+
|
30
|
+
# Ignore default Litestack SQLite databases.
|
31
|
+
/db/**/*.sqlite3
|
32
|
+
/db/**/*.sqlite3-*
|
33
|
+
TEXT
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# SQLite. Versions 3.8.0 and up are supported.
|
2
|
+
# gem install sqlite3
|
3
|
+
#
|
4
|
+
# Ensure the SQLite 3 gem is defined in your Gemfile
|
5
|
+
# gem "sqlite3"
|
6
|
+
#
|
7
|
+
# `Litesupport.root.join("data.sqlite3")` stores
|
8
|
+
# application data in the path `./db/#{Rails.env}/data.sqlite3`
|
9
|
+
default: &default
|
10
|
+
adapter: litedb
|
11
|
+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
12
|
+
timeout: 5000
|
13
|
+
database: <%= Litesupport.root.join("data.sqlite3") %>
|
14
|
+
|
15
|
+
development:
|
16
|
+
<<: *default
|
17
|
+
|
18
|
+
# Warning: The database defined as "test" will be erased and
|
19
|
+
# re-generated from your development database when you run "rake".
|
20
|
+
# Do not set this db to the same as development or production.
|
21
|
+
test:
|
22
|
+
<<: *default
|
23
|
+
|
24
|
+
# Warning: Make sure your production database path is on a persistent
|
25
|
+
# volume, otherwise your application data could be deleted between deploys.
|
26
|
+
#
|
27
|
+
# You may also set the Litesupport.root in production via the
|
28
|
+
# `LITESTACK_DATA_PATH` environment variable.
|
29
|
+
production:
|
30
|
+
<<: *default
|
@@ -0,0 +1,168 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'hanami/router'
|
3
|
+
require 'tilt'
|
4
|
+
require 'erubi'
|
5
|
+
|
6
|
+
# require relative so we pick the gem version that corresponds to the liteboard binary
|
7
|
+
require_relative '../../litestack/litemetric'
|
8
|
+
|
9
|
+
class Liteboard
|
10
|
+
|
11
|
+
@@resolutions = {'minute' => [300, 12], 'hour' => [3600, 24], 'day' => [3600*24, 7], 'week' => [3600*24*7, 53], 'year' => [3600*24*365, 100] }
|
12
|
+
@@res_mapping = {'hour' => 'minute', 'day' => 'hour', 'week' => 'day', 'year' => 'week'}
|
13
|
+
@@templates = {}
|
14
|
+
@@app = Hanami::Router.new do
|
15
|
+
|
16
|
+
get "/", to: ->(env) do
|
17
|
+
Liteboard.new(env).call(:index)
|
18
|
+
end
|
19
|
+
|
20
|
+
get "/topics/:topic", to: ->(env) do
|
21
|
+
Liteboard.new(env).call(:topic)
|
22
|
+
end
|
23
|
+
|
24
|
+
get "/topics/:topic/events/:event", to: ->(env) do
|
25
|
+
Liteboard.new(env).call(:event)
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
|
31
|
+
def initialize(env)
|
32
|
+
@env = env
|
33
|
+
@params = @env["router.params"]
|
34
|
+
@running = true
|
35
|
+
@lm = Litemetric.instance
|
36
|
+
end
|
37
|
+
|
38
|
+
def params(key)
|
39
|
+
URI.decode_uri_component("#{@params[key]}")
|
40
|
+
end
|
41
|
+
|
42
|
+
def call(method)
|
43
|
+
before
|
44
|
+
res = send(method)
|
45
|
+
after(res)
|
46
|
+
end
|
47
|
+
|
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
|
+
def after(body=nil)
|
55
|
+
[200, {'Cache-Control' => 'no-cache'}, [body]]
|
56
|
+
end
|
57
|
+
|
58
|
+
def before
|
59
|
+
@res = params(:res) || 'day'
|
60
|
+
@resolution = @@res_mapping[@res]
|
61
|
+
if not @resolution
|
62
|
+
@res = 'day'
|
63
|
+
@resolution = @@res_mapping[@res]
|
64
|
+
end
|
65
|
+
@step = @@resolutions[@resolution][0]
|
66
|
+
@count = @@resolutions[@resolution][1]
|
67
|
+
@order = params(:order)
|
68
|
+
@order = nil if @order == ''
|
69
|
+
@dir = params(:dir)
|
70
|
+
@dir = 'desc' if @dir.nil? || @dir == ''
|
71
|
+
@dir = @dir.downcase
|
72
|
+
@idir = if @dir == "asc"
|
73
|
+
"desc"
|
74
|
+
else
|
75
|
+
"asc"
|
76
|
+
end
|
77
|
+
@search = params(:search)
|
78
|
+
@search = nil if @search == ''
|
79
|
+
end
|
80
|
+
|
81
|
+
def index
|
82
|
+
@order = 'topic' unless @order
|
83
|
+
topics = @lm.topics
|
84
|
+
@topics = @lm.topic_summaries(@resolution, @step * @count, @order, @dir, @search)
|
85
|
+
@topics.each do |topic|
|
86
|
+
data_points = @lm.topic_data_points(@step, @count, @resolution, topic[0])
|
87
|
+
topic << data_points.collect{|r| [r[0],r[2]]}
|
88
|
+
end
|
89
|
+
render :index
|
90
|
+
end
|
91
|
+
|
92
|
+
def topic
|
93
|
+
@order = 'rcount' unless @order
|
94
|
+
@topic = params(:topic)
|
95
|
+
@events = @lm.events_summaries(@topic, @resolution, @order, @dir, @search, @step * @count)
|
96
|
+
@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]]}
|
100
|
+
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?
|
106
|
+
end
|
107
|
+
render :topic
|
108
|
+
end
|
109
|
+
|
110
|
+
def event
|
111
|
+
@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
|
121
|
+
end
|
122
|
+
|
123
|
+
def index_sort_url(field)
|
124
|
+
"/?#{compose_query(field)}"
|
125
|
+
end
|
126
|
+
|
127
|
+
def topic_sort_url(field)
|
128
|
+
"/topics/#{encode(@topic)}?#{compose_query(field)}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def event_sort_url(field)
|
132
|
+
"/topics/#{encode(@topic)}/events/#{encode(@event)}?#{compose_query(field)}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def compose_query(field)
|
136
|
+
field.downcase!
|
137
|
+
"res=#{@res}&order=#{field}&dir=#{@order == field ? @idir : @dir}&search=#{@search}"
|
138
|
+
end
|
139
|
+
|
140
|
+
def sorted?(field)
|
141
|
+
@order == field
|
142
|
+
end
|
143
|
+
|
144
|
+
def dir(field)
|
145
|
+
if sorted?(field)
|
146
|
+
if @dir == 'asc'
|
147
|
+
return "<span class='material-icons'>arrow_drop_up</span>"
|
148
|
+
else
|
149
|
+
return "<span class='material-icons'>arrow_drop_down</span>"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
' '
|
153
|
+
end
|
154
|
+
|
155
|
+
def encode(text)
|
156
|
+
URI.encode_uri_component(text)
|
157
|
+
end
|
158
|
+
|
159
|
+
def self.app
|
160
|
+
@@app
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
|
167
|
+
|
168
|
+
#Rack::Server.start({app: Litebaord.app, daemonize: false})
|
@@ -0,0 +1,32 @@
|
|
1
|
+
<h5><a href="/?res=<%=@res%>">All Topics</a> > <a href="/topics/<%=@topic%>?res=<%=@res%>"><%=@topic%></a> > <%= @event %></h5>
|
2
|
+
<div id="search"><form><input id="search-field" type="text" placeholder="Search keys" onkeydown="search_kd(this)" onkeyup="search_ku(this)" value="<%=@search%>"/></form></div>
|
3
|
+
<table class="table sortable">
|
4
|
+
<tr>
|
5
|
+
<th width="16%" class="<%='sorted' if @order == 'key'%>"><a href="<%=event_sort_url('key')%>">Key</a> <%=dir('key')%></th>
|
6
|
+
<th width="8%" class="<%='sorted' if @order == 'rcount'%>"><a href="<%=event_sort_url('rcount')%>">Event Count</a> <%=dir('rcount')%></th>
|
7
|
+
<th width="8%" class="<%='sorted' if @order == 'ravg'%>"><a href="<%=event_sort_url('ravg')%>">Avg Value</a> <%=dir('ravg')%></th>
|
8
|
+
<th width="8%" class="<%='sorted' if @order == 'rtotal'%>"><a href="<%=event_sort_url('rtotal')%>">Total Value</a> <%=dir('rtotal')%></th>
|
9
|
+
<th width="8%" class="<%='sorted' if @order == 'rmin'%>"><a href="<%=event_sort_url('rmin')%>">Min Value</a> <%=dir('rmin')%></th>
|
10
|
+
<th width="8%" class="<%='sorted' if @order == 'rmax'%>"><a href="<%=event_sort_url('rmax')%>">Max Value</a> <%=dir('rmax')%></th>
|
11
|
+
<th width="22%">Events over time</th>
|
12
|
+
<th width="22%">Average value over time</th>
|
13
|
+
</tr>
|
14
|
+
<% @keys.each do |key|%>
|
15
|
+
<tr>
|
16
|
+
<td title="<%=key[0]%>"><div class="label"><span><%=key[0]%></span></div></td>
|
17
|
+
<td><%=key[1]%></td>
|
18
|
+
<td><%="%0.2f" % [key[2]] if key[2]%></td>
|
19
|
+
<td><%="%0.2f" % [key[3]] if key[3]%></td>
|
20
|
+
<td><%="%0.2f" % [key[4]] if key[4]%></td>
|
21
|
+
<td><%="%0.2f" % [key[5]] if key[5]%></td>
|
22
|
+
<td class="chart"><span class="inlinecolumn hidden" data-label="Count"><%=Oj.dump(key[6]) if key[7]%></span></td>
|
23
|
+
<td class="chart"><span class="inlinecolumn hidden" data-label="Avg Value"><%=Oj.dump(key[7]) if key[7]%></span></td>
|
24
|
+
</tr>
|
25
|
+
<% end %>
|
26
|
+
<% if @keys.empty? %>
|
27
|
+
<tr>
|
28
|
+
<td class="empty" colspan="8">No data to display</td>
|
29
|
+
</tr>
|
30
|
+
<% end %>
|
31
|
+
</table>
|
32
|
+
|
@@ -0,0 +1,22 @@
|
|
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
|
+
|
@@ -0,0 +1,152 @@
|
|
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
|
+
table.head 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 { color: #078; }
|
25
|
+
a .logo { color: #000;}
|
26
|
+
a:visited { color: #078; }
|
27
|
+
//table.summary { width: 50% }
|
28
|
+
span.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
|
+
<table class="head">
|
45
|
+
<tr>
|
46
|
+
<td>
|
47
|
+
Showing data for the last <select onchange="window.location = locationWithParam('res', this.value)">
|
48
|
+
<%= mapping = {'hour' => '60 minutes', 'day' => '24 hours', 'week' => '7 days', 'year' => '52 weeks'}%>
|
49
|
+
<% ['hour', 'day', 'week', 'year'].each do |res| %>
|
50
|
+
<option value=<%=res%> <%='selected' if res == @res%>><%=mapping[res]%></option>
|
51
|
+
<% end %>
|
52
|
+
</select>
|
53
|
+
</td>
|
54
|
+
<td></td>
|
55
|
+
</tr>
|
56
|
+
<tr>
|
57
|
+
<td></td><td></td>
|
58
|
+
</tr>
|
59
|
+
</table>
|
60
|
+
<%= yield %>
|
61
|
+
</div>
|
62
|
+
<div id="footer">
|
63
|
+
Powered by <a href="https://www.github.com/oldmoe/litestack" target="_blank"><span class="logo"><span class="logo-half">lite</span>stack</span></a>
|
64
|
+
</div>
|
65
|
+
<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>
|
66
|
+
</body>
|
67
|
+
<script>
|
68
|
+
google.charts.load('current', {'packages':['corechart']});
|
69
|
+
|
70
|
+
google.charts.setOnLoadCallback(drawChart);
|
71
|
+
|
72
|
+
function drawChart() {
|
73
|
+
elements = document.querySelectorAll(".inlinecolumn")
|
74
|
+
elements.forEach(element => {
|
75
|
+
//console.log(element)
|
76
|
+
var label = element.dataset.label;
|
77
|
+
var mydata = eval(element.innerText)
|
78
|
+
//console.log(mydata)
|
79
|
+
if(mydata.length > 0) {
|
80
|
+
var data = new google.visualization.DataTable();
|
81
|
+
|
82
|
+
data.addColumn('string', 'Time');
|
83
|
+
data.addColumn('number', label);
|
84
|
+
mydata.forEach(row => {
|
85
|
+
row[1] = Number(row[1])
|
86
|
+
data.addRows([row])
|
87
|
+
})
|
88
|
+
//data.addRows(mydata)
|
89
|
+
var options = {
|
90
|
+
animation: {'startup': true, 'duration': 300},
|
91
|
+
width: 400,
|
92
|
+
height: 40,
|
93
|
+
backgroundColor: 'none',
|
94
|
+
curveType: 'function',
|
95
|
+
colors : ['#089'],
|
96
|
+
vAxis: {'gridlines': {'count' : 0}, 'textPosition' : 'none', 'baselineColor' : 'none', 'minValue' : 0 , 'maxValue' : 50},
|
97
|
+
hAxis: { 'count' : 0, 'textPosition' : 'none', 'baselineColor' : 'none'},
|
98
|
+
legend: {'position': 'none'}
|
99
|
+
}
|
100
|
+
var chart = new google.visualization.AreaChart(element);
|
101
|
+
chart.draw(data, options);
|
102
|
+
element.classList.remove("hidden")
|
103
|
+
}
|
104
|
+
})
|
105
|
+
}
|
106
|
+
function search_kd(el){
|
107
|
+
//store the current value
|
108
|
+
el.oldvalue = el.value
|
109
|
+
}
|
110
|
+
function search_ku(el){
|
111
|
+
//check if the value has changed and if so
|
112
|
+
// set a new timer to fire a request in 300ms
|
113
|
+
// removing any existing timer first
|
114
|
+
if(el.value == el.oldvalue){
|
115
|
+
return
|
116
|
+
}else{
|
117
|
+
el.oldvalue = null
|
118
|
+
}
|
119
|
+
if(el.timeout){
|
120
|
+
window.clearTimeout(el.timeout)
|
121
|
+
}
|
122
|
+
el.timeout = window.setTimeout(function(){
|
123
|
+
el.timeout = null
|
124
|
+
window.location = locationWithParam('search', el.value)
|
125
|
+
}, 300)
|
126
|
+
}
|
127
|
+
|
128
|
+
$(document).ready(function(){
|
129
|
+
el = $('#search-field')[0]
|
130
|
+
el.focus()
|
131
|
+
if(el.value && el.value.length > 0){
|
132
|
+
el.setSelectionRange(el.value.length, el.value.length)
|
133
|
+
var list = $("table.sortable div.label") //[0].children[0].children
|
134
|
+
for(var i=0; i < list.length; i++){
|
135
|
+
//console.log(list[i])
|
136
|
+
var link = list[i].children[0] //.children[0].children[0]
|
137
|
+
var re = new RegExp("("+el.value+")", "giu")
|
138
|
+
link.innerHTML = link.innerHTML.replaceAll(re, "<span class='token'>$1</span>") ;
|
139
|
+
}
|
140
|
+
}
|
141
|
+
})
|
142
|
+
|
143
|
+
function locationWithParam(param, value){
|
144
|
+
var query = window.location.search
|
145
|
+
var params = new URLSearchParams(query)
|
146
|
+
params.set(param, value)
|
147
|
+
var l = window.location
|
148
|
+
return l.origin + l.pathname + '?' + params.toString()
|
149
|
+
}
|
150
|
+
</script>
|
151
|
+
</html>
|
152
|
+
|