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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb0952d46398aae021145cdfbadbcd7293da4d0eab2be6008c1d4d027b26d18d
4
- data.tar.gz: 59bd635a7fe85b0d9604eb6e4a781bf0cda462f257e1a43418eb527a965240e0
3
+ metadata.gz: d6db833e61264ea2036338102d0ed1d3e9eb0a5d94a235db5a8f8d4cf390c65f
4
+ data.tar.gz: 5a48dd08c18ab39f2a19462025e351da8f3e2eedf55f779019cef3ed277e12bc
5
5
  SHA512:
6
- metadata.gz: 97e5bf71f98f30ba2c4b46ede2d472fbde3676b38fc2eaa5fda2d0edcb6eb048b05712a227b1b9a1b34183ec34154d7e472f9fe5eec7fb27be5102828a2c9a78
7
- data.tar.gz: '041950f5996b88e5c60a7800172150199d273ab071c906cc608acb0bb2947b2c3c95ca018ea9862c07395ad4c6da730b1439450dc04112b5e70432fcd122101f'
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
@@ -6,3 +6,5 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
 
9
+
10
+ gem "rack", "~> 3.0"
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 this line to your application's Gemfile:
34
+ Add the `litestack` gem line to your application's Gemfile:
35
35
 
36
- ```ruby
37
- gem 'litestack'
38
- ```
39
-
40
- And then execute:
41
-
42
- $ bundle install
36
+ $ bundle add litestack
43
37
 
44
- Or install it yourself as:
38
+ To configure a Rails application to run the full litestack, run:
45
39
 
46
- $ gem install litestack
40
+ $ rails generate litestack:install
47
41
 
48
42
  ## Usage
49
43
 
@@ -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.environment
32
+ puts Litesupport.scheduler
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.environment} environmnet"
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,11 @@
1
+ development:
2
+ adapter: litecable
3
+
4
+ test:
5
+ adapter: test
6
+
7
+ staging:
8
+ adapter: litecable
9
+
10
+ production:
11
+ adapter: litecable
@@ -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
+ '&nbsp;&nbsp;'
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
+