litestack 0.2.2 → 0.2.6
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 +4 -4
- data/CHANGELOG.md +16 -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/active_job/queue_adapters/litejob_adapter.rb +0 -4
- 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,21 @@
|
|
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
|
+
|
10
|
+
## [0.2.3] - 2023-05-20
|
11
|
+
|
12
|
+
- Cut back on options defined in the Litejob Rails adapter
|
13
|
+
|
14
|
+
## [0.2.2] - 2023-05-18
|
15
|
+
|
16
|
+
- Fix default queue location in Litejob
|
17
|
+
|
18
|
+
|
3
19
|
## [0.2.1] - 2023-05-08
|
4
20
|
|
5
21
|
- Fix a race condition in Litecable
|
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)
|
@@ -33,11 +33,7 @@ module ActiveJob
|
|
33
33
|
|
34
34
|
DEFAULT_OPTIONS = {
|
35
35
|
config_path: "./config/litejob.yml",
|
36
|
-
path: "./queue.db",
|
37
|
-
queues: [["default", 1]],
|
38
36
|
logger: nil, # Rails performs its logging already
|
39
|
-
retries: 5, # It is recommended to stop retries at the Rails level
|
40
|
-
workers: 5
|
41
37
|
}
|
42
38
|
|
43
39
|
include ::Litejob
|
@@ -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
|
+
|