├── .gitignore
├── CHANGELOG.md
├── Gemfile
├── Gemfile.lock
├── MIT-LICENSE
├── README.md
├── Rakefile
├── active_analytics.gemspec
├── app
├── assets
│ ├── config
│ │ └── active_analytics_manifest.js
│ └── images
│ │ ├── active_analytics.png
│ │ ├── active_analytics
│ │ └── .keep
│ │ └── active_analytics_screenshot.png
├── controllers
│ └── active_analytics
│ │ ├── application_controller.rb
│ │ ├── assets_controller.rb
│ │ ├── browsers_controller.rb
│ │ ├── pages_controller.rb
│ │ ├── referrers_controller.rb
│ │ └── sites_controller.rb
├── helpers
│ └── active_analytics
│ │ ├── application_helper.rb
│ │ ├── browsers_helper.rb
│ │ ├── pages_helper.rb
│ │ ├── referrers_helper.rb
│ │ └── sites_helper.rb
├── jobs
│ └── active_analytics
│ │ └── application_job.rb
├── lib
│ └── active_analytics
│ │ └── histogram.rb
├── mailers
│ └── active_analytics
│ │ └── application_mailer.rb
├── models
│ └── active_analytics
│ │ ├── application_record.rb
│ │ ├── browsers_per_day.rb
│ │ └── views_per_day.rb
└── views
│ ├── active_analytics
│ ├── assets
│ │ ├── _charts.css
│ │ ├── _style.css
│ │ ├── application.css.erb
│ │ ├── application.js
│ │ ├── ariato.css
│ │ ├── ariato.js
│ │ └── browsers
│ │ │ ├── arc.svg
│ │ │ ├── brave.svg
│ │ │ ├── chrome.svg
│ │ │ ├── default.svg
│ │ │ ├── firefox.svg
│ │ │ ├── internet_explorer.svg
│ │ │ ├── microsoft_edge.svg
│ │ │ ├── opera.svg
│ │ │ ├── safari.svg
│ │ │ ├── samsung_internet.svg
│ │ │ ├── vivaldi.svg
│ │ │ └── yandex.svg
│ ├── browsers
│ │ ├── _table.html.erb
│ │ ├── _version_table.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── pages
│ │ ├── _table.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ ├── referrers
│ │ ├── _table.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ └── sites
│ │ ├── _histogram.html.erb
│ │ ├── _histogram_header.html.erb
│ │ ├── index.html.erb
│ │ └── show.html.erb
│ └── layouts
│ └── active_analytics
│ ├── _footer.html.erb
│ ├── _header.html.erb
│ └── application.html.erb
├── bin
└── rails
├── config
└── routes.rb
├── db
└── migrate
│ ├── 20210303094108_create_active_analytics_views_per_days.rb
│ └── 20240823150626_create_active_analytics_browsers_per_days.rb
├── lib
├── active_analytics.rb
├── active_analytics
│ ├── engine.rb
│ └── version.rb
└── tasks
│ └── active_analytics_tasks.rake
└── test
├── active_analytics_test.rb
├── controllers
└── active_analytics
│ ├── browsers_controller_test.rb
│ ├── pages_controller_test.rb
│ ├── referrers_controller_test.rb
│ └── sites_controller_test.rb
├── dummy
├── Rakefile
├── app
│ ├── assets
│ │ ├── config
│ │ │ └── manifest.js
│ │ ├── images
│ │ │ └── .keep
│ │ └── stylesheets
│ │ │ └── application.css
│ ├── channels
│ │ └── application_cable
│ │ │ ├── channel.rb
│ │ │ └── connection.rb
│ ├── controllers
│ │ ├── application_controller.rb
│ │ └── concerns
│ │ │ └── .keep
│ ├── helpers
│ │ └── application_helper.rb
│ ├── javascript
│ │ └── packs
│ │ │ └── application.js
│ ├── jobs
│ │ └── application_job.rb
│ ├── mailers
│ │ └── application_mailer.rb
│ ├── models
│ │ ├── application_record.rb
│ │ └── concerns
│ │ │ └── .keep
│ └── views
│ │ └── layouts
│ │ ├── application.html.erb
│ │ ├── mailer.html.erb
│ │ └── mailer.text.erb
├── bin
│ ├── rails
│ ├── rake
│ └── setup
├── config.ru
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── cable.yml
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers
│ │ ├── application_controller_renderer.rb
│ │ ├── assets.rb
│ │ ├── backtrace_silencers.rb
│ │ ├── content_security_policy.rb
│ │ ├── cookies_serializer.rb
│ │ ├── filter_parameter_logging.rb
│ │ ├── inflections.rb
│ │ ├── mime_types.rb
│ │ ├── permissions_policy.rb
│ │ └── wrap_parameters.rb
│ ├── locales
│ │ └── en.yml
│ ├── puma.rb
│ ├── routes.rb
│ └── storage.yml
├── db
│ └── schema.rb
├── lib
│ └── assets
│ │ └── .keep
├── log
│ └── .keep
└── public
│ ├── 404.html
│ ├── 422.html
│ ├── 500.html
│ ├── apple-touch-icon-precomposed.png
│ ├── apple-touch-icon.png
│ └── favicon.ico
├── fixtures
└── active_analytics
│ ├── browsers_per_days.yml
│ └── views_per_days.yml
├── integration
└── navigation_test.rb
├── models
└── active_analytics
│ ├── browsers_per_day_test.rb
│ └── views_per_day_test.rb
└── test_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /doc/
3 | /log/*.log
4 | /pkg/
5 | /tmp/
6 | /test/dummy/db/*.sqlite3
7 | /test/dummy/db/*.sqlite3-*
8 | /test/dummy/log/*.log
9 | /test/dummy/storage/
10 | /test/dummy/tmp/
11 | .byebug_history
12 | **/.DS_Store
13 | *.dump
14 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog of ActiveAnalytics
2 |
3 | ## Version 0.4.1 (2025-03-13)
4 |
5 | - Shortened index name in migration to prevent argument error (index name too long)
6 |
7 | ## Version 0.4.0 (2025-03-06)
8 |
9 | - Record browser statistics from user agent
10 | - Added `base_controller_class` configuration option to allow specifying a custom base controller for the ActiveAnalytics dashboard,
11 | enhancing flexibility in diverse application architectures.
12 |
13 | ## Version 0.3 (2023-09-15)
14 |
15 | * Queue requests to reduce the load on database writes
16 |
17 | Database writes are slow. On large trafic applications it might overload the database.
18 | The idea is to queue data into redis and to flush periodically into the database.
19 |
20 | ```ruby
21 | ActiveAnalytics.queue_request(request) # In an after_action
22 | ```
23 |
24 | Then call from time to time :
25 |
26 | ```ruby
27 | ActiveAnalytics.flush_queue # From a cron or a job
28 | ```
29 |
30 | * Deliver CSS and JS without asset pipeline
31 | * Reverse date range when start is after end
32 | * Add link to external page
33 | * Display views evolution against previous period
34 | * List all paths from a host referrer when available
35 | * Scope css styles with .active-analytics
36 | * Update colors to augment contrast
37 | * Add gap to separate days in chart
38 | * Add link to day on chart label
39 | * Prevent chart NaN
40 | * Add trend labels color
41 | * Remove unused css
42 | * Add environment variable ACTIVE_ANALYTICS_REDIS_URL
43 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 | git_source(:github) { |repo| "https://github.com/#{repo}.git" }
3 |
4 | # Specify your gem's dependencies in active_analytics.gemspec.
5 | gemspec
6 |
7 | group :development do
8 | gem 'sqlite3'
9 | gem "redis"
10 | end
11 |
12 | gem "browser"
13 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | PATH
2 | remote: .
3 | specs:
4 | active_analytics (0.4.1)
5 | browser (>= 1.0.0)
6 | rails (>= 5.2.0)
7 |
8 | GEM
9 | remote: https://rubygems.org/
10 | specs:
11 | actioncable (8.0.2)
12 | actionpack (= 8.0.2)
13 | activesupport (= 8.0.2)
14 | nio4r (~> 2.0)
15 | websocket-driver (>= 0.6.1)
16 | zeitwerk (~> 2.6)
17 | actionmailbox (8.0.2)
18 | actionpack (= 8.0.2)
19 | activejob (= 8.0.2)
20 | activerecord (= 8.0.2)
21 | activestorage (= 8.0.2)
22 | activesupport (= 8.0.2)
23 | mail (>= 2.8.0)
24 | actionmailer (8.0.2)
25 | actionpack (= 8.0.2)
26 | actionview (= 8.0.2)
27 | activejob (= 8.0.2)
28 | activesupport (= 8.0.2)
29 | mail (>= 2.8.0)
30 | rails-dom-testing (~> 2.2)
31 | actionpack (8.0.2)
32 | actionview (= 8.0.2)
33 | activesupport (= 8.0.2)
34 | nokogiri (>= 1.8.5)
35 | rack (>= 2.2.4)
36 | rack-session (>= 1.0.1)
37 | rack-test (>= 0.6.3)
38 | rails-dom-testing (~> 2.2)
39 | rails-html-sanitizer (~> 1.6)
40 | useragent (~> 0.16)
41 | actiontext (8.0.2)
42 | actionpack (= 8.0.2)
43 | activerecord (= 8.0.2)
44 | activestorage (= 8.0.2)
45 | activesupport (= 8.0.2)
46 | globalid (>= 0.6.0)
47 | nokogiri (>= 1.8.5)
48 | actionview (8.0.2)
49 | activesupport (= 8.0.2)
50 | builder (~> 3.1)
51 | erubi (~> 1.11)
52 | rails-dom-testing (~> 2.2)
53 | rails-html-sanitizer (~> 1.6)
54 | activejob (8.0.2)
55 | activesupport (= 8.0.2)
56 | globalid (>= 0.3.6)
57 | activemodel (8.0.2)
58 | activesupport (= 8.0.2)
59 | activerecord (8.0.2)
60 | activemodel (= 8.0.2)
61 | activesupport (= 8.0.2)
62 | timeout (>= 0.4.0)
63 | activestorage (8.0.2)
64 | actionpack (= 8.0.2)
65 | activejob (= 8.0.2)
66 | activerecord (= 8.0.2)
67 | activesupport (= 8.0.2)
68 | marcel (~> 1.0)
69 | activesupport (8.0.2)
70 | base64
71 | benchmark (>= 0.3)
72 | bigdecimal
73 | concurrent-ruby (~> 1.0, >= 1.3.1)
74 | connection_pool (>= 2.2.5)
75 | drb
76 | i18n (>= 1.6, < 2)
77 | logger (>= 1.4.2)
78 | minitest (>= 5.1)
79 | securerandom (>= 0.3)
80 | tzinfo (~> 2.0, >= 2.0.5)
81 | uri (>= 0.13.1)
82 | base64 (0.2.0)
83 | benchmark (0.4.0)
84 | bigdecimal (3.1.9)
85 | browser (6.2.0)
86 | builder (3.3.0)
87 | concurrent-ruby (1.3.5)
88 | connection_pool (2.5.0)
89 | crass (1.0.6)
90 | date (3.4.1)
91 | drb (2.2.1)
92 | erubi (1.13.1)
93 | globalid (1.2.1)
94 | activesupport (>= 6.1)
95 | i18n (1.14.7)
96 | concurrent-ruby (~> 1.0)
97 | io-console (0.8.0)
98 | irb (1.15.1)
99 | pp (>= 0.6.0)
100 | rdoc (>= 4.0.0)
101 | reline (>= 0.4.2)
102 | logger (1.6.6)
103 | loofah (2.24.0)
104 | crass (~> 1.0.2)
105 | nokogiri (>= 1.12.0)
106 | mail (2.8.1)
107 | mini_mime (>= 0.1.1)
108 | net-imap
109 | net-pop
110 | net-smtp
111 | marcel (1.0.4)
112 | mini_mime (1.1.5)
113 | mini_portile2 (2.8.8)
114 | minitest (5.25.5)
115 | net-imap (0.5.6)
116 | date
117 | net-protocol
118 | net-pop (0.1.2)
119 | net-protocol
120 | net-protocol (0.2.2)
121 | timeout
122 | net-smtp (0.5.1)
123 | net-protocol
124 | nio4r (2.7.4)
125 | nokogiri (1.18.3-x86_64-linux-gnu)
126 | racc (~> 1.4)
127 | pp (0.6.2)
128 | prettyprint
129 | prettyprint (0.2.0)
130 | psych (5.2.3)
131 | date
132 | stringio
133 | racc (1.8.1)
134 | rack (3.1.12)
135 | rack-session (2.1.0)
136 | base64 (>= 0.1.0)
137 | rack (>= 3.0.0)
138 | rack-test (2.2.0)
139 | rack (>= 1.3)
140 | rackup (2.2.1)
141 | rack (>= 3)
142 | rails (8.0.2)
143 | actioncable (= 8.0.2)
144 | actionmailbox (= 8.0.2)
145 | actionmailer (= 8.0.2)
146 | actionpack (= 8.0.2)
147 | actiontext (= 8.0.2)
148 | actionview (= 8.0.2)
149 | activejob (= 8.0.2)
150 | activemodel (= 8.0.2)
151 | activerecord (= 8.0.2)
152 | activestorage (= 8.0.2)
153 | activesupport (= 8.0.2)
154 | bundler (>= 1.15.0)
155 | railties (= 8.0.2)
156 | rails-dom-testing (2.2.0)
157 | activesupport (>= 5.0.0)
158 | minitest
159 | nokogiri (>= 1.6)
160 | rails-html-sanitizer (1.6.2)
161 | loofah (~> 2.21)
162 | nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
163 | railties (8.0.2)
164 | actionpack (= 8.0.2)
165 | activesupport (= 8.0.2)
166 | irb (~> 1.13)
167 | rackup (>= 1.0.0)
168 | rake (>= 12.2)
169 | thor (~> 1.0, >= 1.2.2)
170 | zeitwerk (~> 2.6)
171 | rake (13.2.1)
172 | rdoc (6.12.0)
173 | psych (>= 4.0.0)
174 | redis (5.4.0)
175 | redis-client (>= 0.22.0)
176 | redis-client (0.24.0)
177 | connection_pool
178 | reline (0.6.0)
179 | io-console (~> 0.5)
180 | securerandom (0.4.1)
181 | sqlite3 (2.6.0)
182 | mini_portile2 (~> 2.8.0)
183 | stringio (3.1.5)
184 | thor (1.3.2)
185 | timeout (0.4.3)
186 | tzinfo (2.0.6)
187 | concurrent-ruby (~> 1.0)
188 | uri (1.0.3)
189 | useragent (0.16.11)
190 | websocket-driver (0.7.7)
191 | base64
192 | websocket-extensions (>= 0.1.0)
193 | websocket-extensions (0.1.5)
194 | zeitwerk (2.7.2)
195 |
196 | PLATFORMS
197 | ruby
198 |
199 | DEPENDENCIES
200 | active_analytics!
201 | browser
202 | redis
203 | sqlite3
204 |
205 | BUNDLED WITH
206 | 2.2.22
207 |
--------------------------------------------------------------------------------
/MIT-LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2021 Alexis Bernard
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ActiveAnalytics
2 |
3 |
4 |
5 | Simple traffic analytics for the win of privacy.
6 |
7 | * NO cookies
8 | * NO JavaScript
9 | * NO third parties
10 | * NO bullshit
11 |
12 | **ActiveAnalytics** is a Rails engine directly mountable in your Ruby on Rails application. It doesn't reveal anything about specific visitors. It cannot be blocked by adblockers or other privacy-protecting extensions (and doesn't need to).
13 |
14 | **ActiveAnalytics** lets you know about:
15 |
16 | * **Sources**: What are the pages and domains that bring some traffic.
17 | * **Page views**: What are the pages that are the most viewed in your application.
18 | * **Next/previous page**: What are the entry and exit pages for a given page of your application.
19 | * **Browser statistics**: What browsers and which versions are visiting your site.
20 |
21 |
22 |
23 | ## Installation
24 | Add this line to your application's Gemfile:
25 | ```ruby
26 | gem 'active_analytics'
27 | ```
28 |
29 | Then execute bundle and run the migration:
30 | ```bash
31 | bundle
32 | rails active_analytics:install:migrations
33 | rails db:migrate
34 | ```
35 |
36 | Add the route to ActiveAnalytics dashboard at the desired endpoint:
37 |
38 | ```ruby
39 | # config/routes.rb
40 | mount ActiveAnalytics::Engine, at: "analytics" # http://localhost:3000/analytics
41 | ```
42 | By default ActiveAnalytics will extend `ActionController::Base`, but you can specify a custom base controller for the ActiveAnalytics dashboard:
43 |
44 | ```ruby
45 | # config/initializers/active_analytics.rb
46 | Rails.application.configure do
47 | ActiveAnalytics.base_controller_class = "AdminController"
48 | end
49 | ```
50 |
51 |
52 | The next step is to collect trafic and there is 2 options.
53 |
54 | ### Record requests synchronously
55 |
56 | This is the easiest way to start with.
57 | However it's less performant since it triggers a write into your database for each request.
58 | Your controllers have to call `ActiveAnalytics.record_request(request)` to record page views.
59 | The Rails way to achieve is to use `after_action` :
60 |
61 | ```ruby
62 | class ApplicationController < ActionController::Base
63 | after_action :record_page_view
64 |
65 | def record_page_view
66 | # This is a basic example, you might need to customize some conditions.
67 | # For most sites, it makes no sense to record anything other than HTML.
68 | if response.content_type && response.content_type.start_with?("text/html")
69 | # Add a condition to record only your canonical domain
70 | # and use a gem such as crawler_detect to skip bots.
71 | ActiveAnalytics.record_request(request)
72 | end
73 | end
74 | end
75 | ```
76 |
77 | In case you don't want to record all page views, because each application has sensitive URLs such as password reset and so on, simply define a `skip_after_action :record_page_view` in the relevant controller.
78 |
79 | ### Queue requests asynchronously
80 |
81 | It requires more work and it's relevant if your application handle a large trafic.
82 | The idea is to queue data into Redis because it does not require the database writing to the disk on each request.
83 | First you have to set the Redis URL or connection.
84 |
85 | ```ruby
86 | # File lib/patches/active_analytics.rb or config/initializers/active_analytics.rb
87 |
88 | ActiveAnalytics.redis_url = "redis://user:password@host/1" # Default ENV["ACTIVE_ANALYTICS_REDIS_URL"] || ENV["REDIS_URL"] || "redis://localhost"
89 |
90 | # If you use special connection options you have to instantiate it yourself
91 | ActiveAnalytics.redis = Redis.new(
92 | url: ENV["REDIS_URL"],
93 | reconnect_attempts: 10,
94 | ssl_params: {verify_mode: OpenSSL::SSL::VERIFY_NONE}
95 | )
96 | ```
97 |
98 | Then your controllers have to call `ActiveAnalytics.queue_request(request)` to queue page views.
99 | The Rails way to achieve is to use `after_action` :
100 |
101 | ```ruby
102 | class ApplicationController < ActionController::Base
103 | after_action :queue_page_view
104 |
105 | def queue_page_view
106 | # This is a basic example, you might need to customize some conditions.
107 | # For most sites, it makes no sense to record anything other than HTML.
108 | if response.content_type && response.content_type.start_with?("text/html")
109 | # Add a condition to record only your canonical domain
110 | # and use a gem such as crawler_detect to skip bots.
111 | ActiveAnalytics.queue_request(request)
112 | end
113 | end
114 | end
115 | ```
116 |
117 | Queued data need to be saved into the database in order to be viewable in the ActiveAnalytics dashboard.
118 | For that, call `ActiveAnalytics.flush_queue` from a cron task or a background job.
119 |
120 | It's up to you if you want to flush the queue every hour or every 10 minutes.
121 | I advise to execute the last flush of the day at 23:59.
122 | It prevents from shifting the trafic to the next day.
123 | In that case only the last minute will be shifted to the next day, even if the flush ends after midnight.
124 | This small imperfection allows a simpler implementation for now.
125 | Keep it simple !
126 |
127 |
128 | ## Authentication and permissions
129 |
130 | ActiveAnalytics cannot guess how you handle user authentication, because it is different for all Rails applications.
131 | So you have to monkey patch `ActiveAnalytics::ApplicationController` in order to inject your own mechanism.
132 | The patch can be saved wherever you want.
133 | For example, I like to have all the patches in one place, so I put them in `lib/patches`.
134 |
135 | ```ruby
136 | # lib/patches/active_analytics.rb
137 |
138 | ActiveAnalytics::ApplicationController.class_eval do
139 | before_action :require_admin
140 |
141 | def require_admin
142 | # This example supposes there are current_user and User#admin? methods
143 | raise ActionController::RoutingError.new("Not found") unless current_user.try(:admin?)
144 | end
145 | end
146 | end
147 | ```
148 |
149 | Then you have to require the monkey patch.
150 | Because it's loaded via require, it won't be reloaded in development.
151 | Since you are not supposed to change this file often, it should not be an issue.
152 |
153 | ```ruby
154 | # config/application.rb
155 | config.after_initialize do
156 | require "patches/active_analytics"
157 | end
158 | ```
159 |
160 | If you use Devise, you can check the permission directly from routes.rb :
161 |
162 | ```ruby
163 | # config/routes.rb
164 | authenticate :user, -> (u) { u.admin? } do # Supposing there is a User#admin? method
165 | mount ActiveAnalytics::Engine, at: "analytics" # http://localhost:3000/analytics
166 | end
167 | ```
168 |
169 | ## License
170 | The gem is available as open-source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
171 |
172 | Made by [Base Secrète](https://basesecrete.com).
173 |
174 | Rails developer? Check out [RoRvsWild](https://rorvswild.com), our Ruby on Rails application monitoring tool.
175 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/setup"
2 |
3 | APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4 | load "rails/tasks/engine.rake"
5 |
6 | load "rails/tasks/statistics.rake"
7 |
8 | require "bundler/gem_tasks"
9 |
10 | require "rake/testtask"
11 |
12 | Rake::TestTask.new(:test) do |t|
13 | t.libs << 'test'
14 | t.pattern = 'test/**/*_test.rb'
15 | t.verbose = false
16 | end
17 |
18 | task default: :test
19 |
--------------------------------------------------------------------------------
/active_analytics.gemspec:
--------------------------------------------------------------------------------
1 | require_relative "lib/active_analytics/version"
2 |
3 | Gem::Specification.new do |spec|
4 | spec.name = "active_analytics"
5 | spec.version = ActiveAnalytics::VERSION
6 | spec.authors = ["Alexis Bernard", "Antoine Marguerie"]
7 | spec.email = ["alexis@basesecrete.com", "antoine@basesecrete.com"]
8 | spec.homepage = "https://github.com/BaseSecrete/active_analytics"
9 | spec.summary = "First-party, privacy-focused traffic analytics for Ruby on Rails applications"
10 | spec.description = "NO cookies, NO JavaScript, NO third parties and NO bullshit."
11 | spec.license = "MIT"
12 |
13 | spec.metadata["allowed_push_host"] = "https://rubygems.org"
14 | spec.metadata["homepage_uri"] = spec.homepage
15 | spec.metadata["source_code_uri"] = "https://github.com/BaseSecrete/active_analytics"
16 | spec.metadata["changelog_uri"] = "https://github.com/BaseSecrete/active_analytics"
17 |
18 | spec.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.md"]
19 |
20 | spec.add_dependency "rails", ">= 5.2.0"
21 | spec.add_dependency "browser", ">= 1.0.0"
22 | end
23 |
--------------------------------------------------------------------------------
/app/assets/config/active_analytics_manifest.js:
--------------------------------------------------------------------------------
1 | //= link_directory ../stylesheets/active_analytics .css
2 |
--------------------------------------------------------------------------------
/app/assets/images/active_analytics.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/app/assets/images/active_analytics.png
--------------------------------------------------------------------------------
/app/assets/images/active_analytics/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/app/assets/images/active_analytics/.keep
--------------------------------------------------------------------------------
/app/assets/images/active_analytics_screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BaseSecrete/active_analytics/f80c3747ac888df0c35285cac4cee743f5f44f8d/app/assets/images/active_analytics_screenshot.png
--------------------------------------------------------------------------------
/app/controllers/active_analytics/application_controller.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | class ApplicationController < ActiveAnalytics.base_controller_class.constantize
3 | layout "active_analytics/application"
4 |
5 | helper PagesHelper
6 | helper SitesHelper
7 | helper BrowsersHelper
8 |
9 | private
10 |
11 | def from_date
12 | @from_date ||= Date.parse(params[:from])
13 | end
14 |
15 | def to_date
16 | @to_date ||= Date.parse(params[:to])
17 | end
18 |
19 | def require_date_range
20 | redirect_to(params.to_unsafe_hash.merge(from: to_date, to: from_date)) if from_date > to_date
21 | rescue TypeError, ArgumentError # Raised by Date.parse when invalid format
22 | redirect_to(params.to_unsafe_hash.merge(from: 7.days.ago.to_date, to: Date.today))
23 | end
24 |
25 | def current_views_per_days
26 | ViewsPerDay.where(site: params[:site]).between_dates(from_date, to_date)
27 | end
28 |
29 | def previous_views_per_days
30 | ViewsPerDay.where(site: params[:site]).between_dates(previous_from_date, previous_to_date)
31 | end
32 |
33 | def previous_from_date
34 | from_date - (to_date - from_date)
35 | end
36 |
37 | def previous_to_date
38 | to_date - (to_date - from_date)
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/controllers/active_analytics/assets_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_dependency "active_analytics/application_controller"
4 |
5 | module ActiveAnalytics
6 | class AssetsController < ApplicationController
7 | protect_from_forgery except: :show
8 |
9 | @@root = ActiveAnalytics::Engine.root.join("app/views", controller_path + "/").to_s
10 |
11 | def self.endpoints
12 | return @endpoints if @endpoints
13 | paths = Dir["#{@@root}**/*"].keep_if { |path| File.file?(path) }
14 | files = paths.map { |path| path.to_s.delete_prefix(@@root).delete_suffix(".erb") }
15 | @endpoints = files.delete_if { |str| str.start_with?("_") }
16 | end
17 |
18 | def show
19 | if self.class.endpoints.include?(params[:file])
20 | expires_in(1.day, public: true)
21 | render_asset(params[:file])
22 | else
23 | raise ActionController::RoutingError.new("Not found #{params[:file]}")
24 | end
25 | end
26 |
27 | private
28 |
29 | def render_asset(path)
30 | ext = File.extname(params[:file])
31 | path_without_ext = path.delete_suffix(ext)
32 | mime_type = Mime::Type.lookup_by_extension(ext.delete_prefix("."))
33 | render("#{controller_path}/#{path_without_ext}", mime_type: mime_type)
34 | end
35 | end
36 | end
37 |
--------------------------------------------------------------------------------
/app/controllers/active_analytics/browsers_controller.rb:
--------------------------------------------------------------------------------
1 | require_dependency "active_analytics/application_controller"
2 |
3 | module ActiveAnalytics
4 | class BrowsersController < ApplicationController
5 | def index
6 | @histogram = Histogram.new(current_browsers_per_days.order_by_date.group_by_date, from_date, to_date)
7 | @previous_histogram = Histogram.new(previous_browsers_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date)
8 | @browsers = current_browsers_per_days.group_by_name.top(100)
9 | end
10 |
11 | def show
12 | @histogram = Histogram.new(current_browsers_per_days.order_by_date.group_by_date, from_date, to_date)
13 | @previous_histogram = Histogram.new(previous_browsers_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date)
14 | @browsers = current_browsers_per_days.group_by_version.top(100)
15 | end
16 |
17 | private
18 |
19 | def current_browsers_per_days
20 | BrowsersPerDay.filter_by(params)
21 | end
22 |
23 | def previous_browsers_per_days
24 | BrowsersPerDay.filter_by(params.merge(from: previous_from_date, to: previous_to_date))
25 | end
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/app/controllers/active_analytics/pages_controller.rb:
--------------------------------------------------------------------------------
1 | require_dependency "active_analytics/application_controller"
2 |
3 | module ActiveAnalytics
4 | class PagesController < ApplicationController
5 | include PagesHelper
6 |
7 | before_action :require_date_range
8 |
9 | def index
10 | @histogram = Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date)
11 | @previous_histogram = Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date)
12 | @pages = current_views_per_days.top(100).group_by_page
13 | end
14 |
15 | def show
16 | page_scope = current_views_per_days.where(page: page_from_params)
17 | previous_page_scope = previous_views_per_days.where(page: page_from_params)
18 | @histogram = Histogram.new(page_scope.order_by_date.group_by_date, from_date, to_date)
19 | @previous_histogram = Histogram.new(previous_page_scope.order_by_date.group_by_date, previous_from_date, previous_to_date)
20 | @next_pages = current_views_per_days.where(referrer_host: params[:site], referrer_path: page_from_params).top(100).group_by_page
21 | @previous_pages = page_scope.top(100).group_by_referrer_page
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/controllers/active_analytics/referrers_controller.rb:
--------------------------------------------------------------------------------
1 | require_dependency "active_analytics/application_controller"
2 |
3 | module ActiveAnalytics
4 | class ReferrersController < ApplicationController
5 | before_action :require_date_range
6 |
7 | def index
8 | @referrers = current_views_per_days.top(100).group_by_referrer_site
9 | @histogram = Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date)
10 | @previous_histogram = Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date)
11 | end
12 |
13 | def show
14 | referrer_host, referrer_path = params[:referrer].split("/", 2)
15 | scope = current_views_per_days.where(referrer_host: referrer_host)
16 | scope = scope.where(referrer_path: "/" + referrer_path) if referrer_path.present?
17 | previous_scope = previous_views_per_days.where(referrer_host: params[:referrer])
18 | @histogram = Histogram.new(scope.order_by_date.group_by_date, from_date, to_date)
19 | @previous_histogram = Histogram.new(previous_scope.order_by_date.group_by_date, previous_from_date, previous_to_date)
20 | @previous_pages = scope.top(100).group_by_referrer_page
21 | @next_pages = scope.top(100).group_by_page
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/controllers/active_analytics/sites_controller.rb:
--------------------------------------------------------------------------------
1 | require_dependency "active_analytics/application_controller"
2 |
3 | module ActiveAnalytics
4 | class SitesController < ApplicationController
5 | before_action :require_date_range, only: :show
6 |
7 | def index
8 | @sites = ViewsPerDay.after(30.days.ago).order_by_totals.group_by_site
9 | redirect_to(site_path(@sites.first.host)) if @sites.size == 1
10 | end
11 |
12 | def show
13 | @histogram = Histogram.new(current_views_per_days.order_by_date.group_by_date, from_date, to_date)
14 | @previous_histogram = Histogram.new(previous_views_per_days.order_by_date.group_by_date, previous_from_date, previous_to_date)
15 | @referrers = current_views_per_days.top.group_by_referrer_site
16 | @pages = current_views_per_days.top.group_by_page
17 | @browsers = BrowsersPerDay.filter_by(params).group_by_name.top
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/app/helpers/active_analytics/application_helper.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | module ApplicationHelper
3 | def format_view_count(number)
4 | number_with_delimiter(number.to_i)
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/helpers/active_analytics/browsers_helper.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | module BrowsersHelper
3 | def browser_icon(browser_name)
4 | path = "browsers/#{browser_name.to_s.parameterize(separator: "_")}.svg"
5 | if AssetsController.endpoints.include?(path)
6 | image_tag(asset_url(path, host: request.host), alt: browser_name, class: "referer-favicon", width: 16, height: 16)
7 | else
8 | image_tag(asset_url("browsers/default.svg", host: request.host), alt: browser_name, class: "referer-favicon", width: 16, height: 16)
9 | end
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/app/helpers/active_analytics/pages_helper.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | module PagesHelper
3 | def page_to_params(name)
4 | name == "/" ? "index" : name[1..-1]
5 | end
6 |
7 | def page_from_params
8 | if params[:page] == "index"
9 | "/"
10 | elsif params[:page].present?
11 | "/#{params[:page]}"
12 | end
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/app/helpers/active_analytics/referrers_helper.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | module ReferrersHelper
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/helpers/active_analytics/sites_helper.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | module SitesHelper
3 | def site_icon(host)
4 | image_tag("https://icons.duckduckgo.com/ip3/#{host}.ico", referrerpolicy: "no-referrer", class: "referer-favicon")
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/app/jobs/active_analytics/application_job.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | class ApplicationJob < ActiveJob::Base
3 | end
4 | end
5 |
--------------------------------------------------------------------------------
/app/lib/active_analytics/histogram.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | class Histogram
3 | attr_reader :bars, :from_date, :to_date
4 |
5 | def initialize(scope, from_date, to_date)
6 | @scope = scope
7 | @from_date, @to_date = from_date, to_date
8 | @bars = scope.map { |record| Bar.new(record.date, record.total, self) }
9 | fill_missing_days(@bars, @from_date, @to_date)
10 | end
11 |
12 | def fill_missing_days(bars, from, to)
13 | i = 0
14 | while (day = from + i) <= to
15 | if !@bars[i] || @bars[i].label != day
16 | @bars.insert(i, Bar.new(day, 0, self))
17 | end
18 | i += 1
19 | end
20 | @bars
21 | end
22 |
23 | def max_value
24 | @max_total ||= bars.map(&:value).max
25 | end
26 |
27 | def total
28 | @bars.reduce(0) { |sum, bar| sum += bar.value }
29 | end
30 |
31 | class Bar
32 | attr_reader :label, :value, :histogram
33 |
34 | def initialize(label, value, histogram)
35 | @label, @value, @histogram = label, value, histogram
36 | end
37 |
38 | def height
39 | if histogram.max_value > 0
40 | (value.to_f / histogram.max_value).round(2)
41 | else
42 | 0
43 | end
44 | end
45 | end
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/app/mailers/active_analytics/application_mailer.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | class ApplicationMailer < ActionMailer::Base
3 | default from: 'from@example.com'
4 | layout 'mailer'
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/app/models/active_analytics/application_record.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | class ApplicationRecord < ActiveRecord::Base
3 | self.abstract_class = true
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/app/models/active_analytics/browsers_per_day.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | class BrowsersPerDay < ApplicationRecord
3 | # TODO: Deduplicate
4 | scope :top, -> (n = 10) { order_by_totals.limit(n) }
5 | scope :order_by_date, -> { order(:date) }
6 | scope :order_by_totals, -> { order(Arel.sql("SUM(total) DESC")) }
7 | scope :between_dates, -> (from, to) { where(date: from..to) }
8 |
9 | def self.append(params)
10 | total = params.delete(:total) || 1
11 | params[:site] = params[:site].downcase if params[:site]
12 | where(params).first.try(:increment!, :total, total) || create!(params.merge(total: total))
13 | end
14 |
15 | def self.group_by_name
16 | group(:name).select("name, sum(total) AS total")
17 | end
18 |
19 | def self.group_by_version
20 | group(:name, :version).select("version, sum(total) AS total")
21 | end
22 |
23 | def self.group_by_date
24 | group(:date).select("date, sum(total) as total")
25 | end
26 |
27 | def self.filter_by(params)
28 | scope = all
29 | scope = scope.between_dates(params[:from], params[:to]) if params[:from].present? && params[:to].present?
30 | scope = scope.where(site: params[:site]) if params[:site].present?
31 | scope = scope.where(name: params[:id]) if params[:id].present?
32 | scope = scope.where(version: params[:version]) if params[:version].present?
33 | scope
34 | end
35 |
36 | def to_param
37 | name
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/app/models/active_analytics/views_per_day.rb:
--------------------------------------------------------------------------------
1 | module ActiveAnalytics
2 | class ViewsPerDay < ApplicationRecord
3 | validates_presence_of :site, :page, :date
4 |
5 | scope :between_dates, -> (from, to) { where("date BETWEEN ? AND ?", from, to) }
6 | scope :after, -> (date) { where("date > ?", date) }
7 | scope :order_by_totals, -> { order(Arel.sql("SUM(total) DESC")) }
8 | scope :order_by_date, -> { order(:date) }
9 | scope :top, -> (n = 10) { order_by_totals.limit(n) }
10 |
11 | class Site
12 | attr_reader :host, :total
13 | def initialize(host, total)
14 | @host, @total = host, total
15 | end
16 | end
17 |
18 | class Page
19 | attr_reader :host, :path, :total
20 | def initialize(host, path, total)
21 | @host, @path, @total = host, path, total
22 | end
23 |
24 | def url
25 | host + path
26 | end
27 | end
28 |
29 | def self.group_by_site
30 | group(:site).pluck("site, SUM(total)").map do |row|
31 | Site.new(row[0], row[1])
32 | end
33 | end
34 |
35 | def self.group_by_page
36 | group(:site, :page).pluck("site, page, SUM(total)").map do |row|
37 | Page.new(row[0], row[1], row[2])
38 | end
39 | end
40 |
41 | def self.group_by_referrer_site
42 | group(:referrer_host).pluck("referrer_host, SUM(total)").map do |row|
43 | Site.new(row[0], row[1])
44 | end
45 | end
46 |
47 | def self.group_by_referrer_page
48 | group(:referrer_host, :referrer_path).pluck("referrer_host, referrer_path, SUM(total)").map do |row|
49 | Page.new(row[0], row[1], row[2])
50 | end
51 | end
52 |
53 | def self.group_by_date
54 | group(:date).select("date, sum(total) AS total")
55 | end
56 |
57 | def self.to_histogram
58 | Histogram.new(self)
59 | end
60 |
61 | def self.append(params)
62 | total = params.delete(:total) || 1
63 | params[:site] = params[:site].downcase if params[:site]
64 | params[:referrer_path] = nil if params[:referrer_path].blank?
65 | params[:referrer_path] = params[:referrer_path].downcase if params[:referrer_path]
66 | params[:referrer_host] = params[:referrer_host].downcase if params[:referrer_host]
67 | where(params).first.try(:increment!, :total, total) || create!(params.merge(total: total))
68 | end
69 |
70 | SLASH = "/"
71 |
72 | def self.split_referrer(referrer)
73 | return [nil, nil] if referrer.blank?
74 | if (uri = URI(referrer)).host.present?
75 | [uri.host, uri.path.presence]
76 | else
77 | strings = referrer.split(SLASH, 2)
78 | [strings[0], strings[1] ? SLASH + strings[1] : nil]
79 | end
80 | end
81 | end
82 | end
83 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/_charts.css:
--------------------------------------------------------------------------------
1 | /*
2 | * Charts.css v0.9.0 (https://ChartsCSS.org/)
3 | * Copyright 2020 Rami Yushuvaev
4 | * Licensed under MIT
5 | */
6 | .active-analytics .charts-css {
7 | --chart-bg-color: transparent;
8 | --heading-size: 0px;
9 | --primary-axis-color: rgba(var(--color-grey-100), 1);
10 | --primary-axis-style: solid;
11 | --primary-axis-width: 1px;
12 | --secondary-axes-color: rgba(var(--color-grey-50), 1);
13 | --secondary-axes-style: solid;
14 | --secondary-axes-width: 1px;
15 | --data-axes-color: rgba(var(--color-grey-200), 1);
16 | --data-axes-style: solid;
17 | --data-axes-width: 1px;
18 | --legend-border-color: rgba(var(--color-grey-200), 1);
19 | position: relative;
20 | display: block;
21 | margin: 0;
22 | padding: 0;
23 | border: 0;
24 | }
25 |
26 | /*
27 | * Chart wrapper element
28 | */
29 |
30 | .active-analytics .charts-css,
31 | .active-analytics .charts-css::after,
32 | .active-analytics .charts-css::before,
33 | .active-analytics .charts-css *,
34 | .active-analytics .charts-css *::after,
35 | .active-analytics .charts-css *::before {
36 | box-sizing: border-box;
37 | }
38 |
39 | /*
40 | * Reset table element
41 | */
42 | .active-analytics table.charts-css {
43 | border-collapse: collapse;
44 | border-spacing: 0;
45 | empty-cells: show;
46 | overflow: initial;
47 | background-color: transparent;
48 | }
49 |
50 | .active-analytics table.charts-css caption,
51 | .active-analytics table.charts-css colgroup,
52 | .active-analytics table.charts-css thead,
53 | .active-analytics table.charts-css tbody,
54 | .active-analytics table.charts-css tr,
55 | .active-analytics table.charts-css th,
56 | .active-analytics table.charts-css td {
57 | display: block;
58 | margin: 0;
59 | padding: 0;
60 | border: 0;
61 | background-color: transparent;
62 | }
63 |
64 | .active-analytics table.charts-css colgroup,
65 | .active-analytics table.charts-css thead,
66 | .active-analytics table.charts-css tfoot {
67 | display: none;
68 | }
69 |
70 |
71 | /*
72 | * Chart colors
73 | */
74 |
75 | .active-analytics .charts-css.column tbody tr td {
76 | background: rgba(var(--color-grey-100), 1);
77 | padding: 0;
78 | }
79 |
80 | /*
81 | * Chart data
82 | */
83 | .active-analytics .charts-css.hide-data .data {
84 | opacity: 0;
85 | }
86 |
87 | .active-analytics .charts-css.show-data-on-hover .data {
88 | transition-duration: .3s;
89 | opacity: 0;
90 | }
91 |
92 | .active-analytics .charts-css.show-data-on-hover tr:hover .data {
93 | transition-duration: .3s;
94 | opacity: 1;
95 | }
96 |
97 | /*
98 | * Chart labels
99 | */
100 |
101 | .active-analytics .charts-css.column:not(.show-labels) {
102 | --labels-size: 0;
103 | }
104 |
105 | .active-analytics .charts-css.column:not(.show-labels) tbody tr th {
106 | display: none;
107 | }
108 |
109 | .active-analytics .charts-css.column.show-labels {
110 | --labels-size: 1.5rem;
111 | }
112 |
113 | .active-analytics .charts-css.column.show-labels tbody tr th {
114 | display: flex;
115 | justify-content: var(--labels-align, center);
116 | align-items: center;
117 | flex-direction: column;
118 | }
119 |
120 | @media (max-width: 600px) {
121 | .active-analytics .charts-css.column.show-labels {
122 | --labels-size: 0;
123 | }
124 |
125 | .active-analytics .charts-css.column.show-labels tbody tr th {
126 | display: none;
127 | }
128 | }
129 |
130 | /*
131 | * Chart axes
132 | */
133 | .active-analytics .charts-css.column.show-primary-axis:not(.reverse) tbody tr {
134 | border-block-end: var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);
135 | }
136 |
137 | .active-analytics .charts-css.column.show-primary-axis.reverse tbody tr {
138 | border-block-start: var(--primary-axis-width) var(--primary-axis-style) var(--primary-axis-color);
139 | }
140 |
141 | .active-analytics .charts-css.column.show-5-secondary-axes:not(.reverse) tbody tr {
142 | background-size: 100% 20%;
143 | background-image: linear-gradient(var(--secondary-axes-color) var(--secondary-axes-width), transparent var(--secondary-axes-width));
144 | }
145 |
146 | .active-analytics .charts-css.column.show-5-secondary-axes.reverse tbody tr {
147 | background-size: 100% 20%;
148 | background-image: linear-gradient(0deg, var(--secondary-axes-color) var(--secondary-axes-width), transparent var(--secondary-axes-width));
149 | }
150 |
151 | /*
152 | * Chart tooltips
153 | */
154 | .active-analytics .charts-css .tooltip {
155 | position: absolute;
156 | z-index: 1;
157 | bottom: 50%;
158 | left: 50%;
159 | transform: translateX(-50%);
160 | width: max-content;
161 | padding: 5px 10px;
162 | border-radius: 6px;
163 | visibility: hidden;
164 | opacity: 0;
165 | transition: opacity .3s;
166 | background-color: rgba(var(--color-grey-500), 1);
167 | color: rgba(var(--color-grey-00), 1);
168 | text-align: center;
169 | font-size: .9rem;
170 | }
171 |
172 | .active-analytics .charts-css .tooltip::after {
173 | content: "";
174 | position: absolute;
175 | top: 100%;
176 | left: 50%;
177 | margin-left: -5px;
178 | border-width: 5px;
179 | border-style: solid;
180 | border-color: rgba(var(--color-grey-500), 1) transparent transparent;
181 | }
182 |
183 | .active-analytics .charts-css tr:hover .tooltip {
184 | visibility: visible;
185 | opacity: 1;
186 | }
187 |
188 | /*
189 | * Column Chart
190 | */
191 | .active-analytics .charts-css.column tbody {
192 | display: flex;
193 | justify-content: space-between;
194 | align-items: stretch;
195 | width: 100%;
196 | gap: 1px;
197 | height: calc(100% - var(--heading-size));
198 | }
199 |
200 | .active-analytics .charts-css.column tbody tr {
201 | position: relative;
202 | flex-grow: 1;
203 | flex-shrink: 1;
204 | flex-basis: 0;
205 | overflow-wrap: anywhere;
206 | display: flex;
207 | justify-content: flex-start;
208 | min-width: 0;
209 | }
210 |
211 | .active-analytics .charts-css.column tbody tr th {
212 | position: absolute;
213 | right: 0;
214 | left: 0;
215 | }
216 |
217 | .active-analytics .charts-css.column tbody tr td {
218 | display: flex;
219 | justify-content: center;
220 | width: 100%;
221 | height: calc(100% * var(--size, 1));
222 | position: relative;
223 | }
224 |
225 | .active-analytics .charts-css.column:not(.reverse) tbody tr {
226 | align-items: flex-end;
227 | margin-block-end: var(--labels-size);
228 | }
229 |
230 | .active-analytics .charts-css.column:not(.reverse) tbody tr th {
231 | bottom: calc(-1 * var(--labels-size) - var(--primary-axis-width));
232 | height: var(--labels-size);
233 | color: rgba(var(--color-grey-400), 1);
234 | font-weight: 400;
235 | }
236 |
237 | .active-analytics .charts-css.column:not(.reverse) tbody tr td {
238 | align-items: flex-start;
239 | }
240 |
241 | .active-analytics .charts-css.column:not(.stacked) tbody tr td {
242 | flex-grow: 1;
243 | flex-shrink: 1;
244 | flex-basis: 0;
245 | }
246 |
247 | .active-analytics .charts-css.column:not(.reverse-data) tbody {
248 | flex-direction: row;
249 | }
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/_style.css:
--------------------------------------------------------------------------------
1 | .active-analytics body {
2 | padding: 0;
3 | margin: 0;
4 | width: 100%;
5 | }
6 |
7 | .active-analytics body > header nav {
8 | width: 100%;
9 | max-width: 1280px;
10 | margin: 0 auto;
11 | padding: 24px;
12 | display: flex;
13 | flex-wrap: wrap;
14 | align-items: center;
15 | min-height: 88px;
16 | }
17 |
18 | .active-analytics header nav .menubutton {
19 | margin-left: auto;
20 | }
21 |
22 | .active-analytics header .logo {
23 | margin-right: 8px;
24 | }
25 |
26 | .active-analytics header .logo svg {
27 | width: 24px;
28 | height: 24px;
29 | fill: none;
30 | stroke: rgba(var(--color-blue-500), 1);
31 | stroke-width: 1;
32 | vertical-align: middle;
33 | }
34 |
35 | .active-analytics header .is-link {
36 | text-transform: none;
37 | text-decoration: none;
38 | font-size: 1rem;
39 | letter-spacing: 0;
40 | font-weight: 400;
41 | padding: 0 16px;
42 | margin: 0;
43 | min-height: 24px;
44 | color: rgba(var(--color-grey-700), 1);
45 | }
46 |
47 | .active-analytics header .is-link:hover {
48 | background: rgba(var(--color-grey-50), 1);
49 | }
50 |
51 | .active-analytics main {
52 | width: 100%;
53 | max-width: 1280px;
54 | margin: 0 auto;
55 | padding: 0 24px;
56 | }
57 |
58 | .active-analytics main h2 {
59 | word-break: break-word;
60 | display: flex;
61 | gap: var(--space);
62 | }
63 |
64 | .active-analytics section {
65 | margin-bottom: var(--space-4x);
66 | }
67 |
68 | .active-analytics .tooltip-date {
69 | color: rgba(var(--color-grey-100), 1);
70 | }
71 |
72 | .active-analytics .card h3 {
73 | display: flex;
74 | }
75 |
76 | .active-analytics .card h3 small {
77 | color: rgba(var(--color-red-500), 1);
78 | }
79 |
80 | .active-analytics .card h3 small.is-success {
81 | color: rgba(var(--color-green-500), 1);
82 | }
83 |
84 | .active-analytics .card h3 a {
85 | margin-left: auto;
86 | font-weight: 400;
87 | font-size: 1rem;
88 | }
89 |
90 | .active-analytics .card table {
91 | margin: 0 -24px -24px;
92 | width: calc(100% + 48px);
93 | }
94 |
95 | .active-analytics tr td:first-of-type {
96 | padding: 12px 0px 12px 24px;
97 | }
98 |
99 | .active-analytics tr td:last-of-type {
100 | padding: 12px 24px 12px 0;
101 | }
102 |
103 | .active-analytics td a {
104 | word-break: break-word;
105 | }
106 |
107 | .active-analytics td.number {
108 | text-align: right;
109 | font-variant-numeric: tabular-nums;
110 | }
111 |
112 | .active-analytics .referer-favicon {
113 | width: 16px;
114 | height: 16px;
115 | display: inline-block;
116 | position: relative;
117 | top: 3px;
118 | margin-right: 4px;
119 | }
120 |
121 | .active-analytics div.is-empty {
122 | display: flex;
123 | align-items: center;
124 | justify-content: center;
125 | height: calc(100% - 72px);
126 | width: 100%;
127 | color: rgba(var(--color-grey-300), 1);
128 | }
129 |
130 | .active-analytics body > footer {
131 | max-width: 1280px;
132 | margin: 48px auto 0;
133 | padding: 24px;
134 | }
135 |
136 | .active-analytics body > footer ul {
137 | margin: 0;
138 | list-style-type: none;
139 | padding: 0;
140 | }
141 |
142 | .active-analytics body > footer .card {
143 | margin: 0;
144 | background: rgba(var(--color-grey-50), 1);
145 | }
146 |
147 | .active-analytics body > footer .card p {
148 | padding: 0 0 2px;
149 | color: rgba(var(--color-grey-400), 1);
150 | }
151 |
152 | @media (max-width: 480px) {
153 | .active-analytics header nav .menubutton {
154 | margin-top: 24px;
155 | }
156 |
157 | .active-analytics [role="dialog"] {
158 | min-width: 280px;
159 | }
160 | }
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/application.css.erb:
--------------------------------------------------------------------------------
1 | <%= render "charts" %>
2 | <%= render "style" %>
3 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/application.js:
--------------------------------------------------------------------------------
1 | ActiveAnalytics = {}
2 |
3 | ActiveAnalytics.Header = function() {
4 | }
5 |
6 | ActiveAnalytics.Header.prototype.toggleDateRangeForm = function() {
7 | var form = this.node.querySelector("#dateRangeForm")
8 | if (form.hasAttribute("hidden"))
9 | form.removeAttribute("hidden")
10 | else
11 | form.setAttribute("hidden", "hidden")
12 | }
13 |
14 | Ariato.launchWhenDomIsReady()
15 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/ariato.css:
--------------------------------------------------------------------------------
1 | /*******************************************/
2 | /* ariato css */
3 | /* https://github.com/BaseSecrete/ariato */
4 | /*******************************************/
5 |
6 | .active-analytics {
7 | --color-blue-100: 199,231,236;
8 | --color-blue-200: 147,206,223;
9 | --color-blue-300: 51,152,207;
10 | --color-blue-400: 0,115,198;
11 | --color-blue-500: 0,67,184;
12 | --color-blue-600: 22,50,155;
13 | --color-blue-700: 23,20,98;
14 | --color-red-20: 253,249,248;
15 | --color-red-200: 233,183,173;
16 | --color-red-500: 164,0,27;
17 | --color-red-600: 123,0,15;
18 | --color-green-500: 119,187,118;
19 | --color-grey-00: 255,255,248;
20 | --color-grey-20: 253,253,246;
21 | --color-grey-35: 249,249,242;
22 | --color-grey-50: 244,243,236;
23 | --color-grey-100: 225,224,217;
24 | --color-grey-200: 195,194,187;
25 | --color-grey-300: 142,140,134;
26 | --color-grey-400: 112,109,103;
27 | --color-grey-500: 80,77,72;
28 | --color-grey-600: 59,57,51;
29 | --color-grey-700: 20,21,15;
30 | --color-black: 0,0.2913950572018906,0;
31 | --color-bg: var(--color-grey-20);
32 | --color-body: var(--color-grey-500);
33 | }
34 |
35 | @media (prefers-color-scheme: dark) {
36 | .active-analytics {
37 | --color-blue-100: 34,44,64;
38 | --color-blue-200: 57,73,106;
39 | --color-blue-300: 82,105,152;
40 | --color-blue-400: 108,138,200;
41 | --color-blue-500: 135,173,250;
42 | --color-blue-600: 145,191,252;
43 | --color-blue-700: 161,208,252;
44 | --color-red-20: 47,27,24;
45 | --color-red-200: 103,62,59;
46 | --color-red-500: 244,146,140;
47 | --color-red-600: 248,165,157;
48 | --color-green-500: 119,187,118;
49 | --color-grey-00: 14,18,26;
50 | --color-grey-20: 16,20,28;
51 | --color-grey-35: 21,25,34;
52 | --color-grey-50: 24,28,36;
53 | --color-grey-100: 40,44,54;
54 | --color-grey-200: 69,74,84;
55 | --color-grey-300: 100,106,116;
56 | --color-grey-400: 133,139,150;
57 | --color-grey-500: 168,174,186;
58 | --color-grey-600: 180,189,200;
59 | --color-grey-700: 193,204,214;
60 | --color-grey-800: 206,219,228;
61 | --color-grey-900: 219,234,241;
62 |
63 | --color-black: 0,0,0;
64 | --color-bg: var(--color-grey-20);
65 | --color-body: var(--color-grey-500);
66 | }
67 | }
68 |
69 | .active-analytics body {
70 | background: rgba(var(--color-bg), 1);
71 | color: rgba(var(--color-body), 1);
72 | }
73 |
74 | .active-analytics :focus {
75 | outline: 2px solid rgba(var(--color-blue-200), 1);
76 | }
77 |
78 | /* design_tokens/font.css */
79 | .active-analytics {
80 | --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
81 | --base-font-size: var(--space-2x);
82 | --font-weight-400: 400;
83 | --font-weight-700: 700;
84 | --ratio: 1.2;
85 | --font-size-30: calc(var(--base-font-size) / var(--ratio) / var(--ratio)); /* -2 */
86 | --font-size-40: calc(var(--base-font-size) / var(--ratio)); /* -1 */
87 | --font-size-50: var(--base-font-size);
88 | --font-size-60: calc(var(--base-font-size) * var(--ratio)); /* +1 */
89 | --font-size-70: calc(var(--base-font-size) * var(--ratio) * var(--ratio)); /* +2 */
90 | --font-size-80: calc(var(--base-font-size) * var(--ratio) * var(--ratio) * var(--ratio)); /* +4 */
91 | --font-size-90: calc(var(--base-font-size) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio)); /* +5 */
92 | --font-size-100: calc(var(--base-font-size) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio)); /* +6 */
93 | --font-size-110: calc(var(--base-font-size) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio) * var(--ratio)); /* +7 */
94 | }
95 |
96 | .active-analytics,
97 | .active-analytics body {
98 | font-size: var(--base-font-size);
99 | }
100 |
101 | .active-analytics body {
102 | font-family: var(--font-sans);
103 | font-weight: var(--font-weight-400);
104 | line-height: var(--space-3x);
105 | }
106 |
107 | /* design_tokens/shadow.css */
108 | .active-analytics {
109 | --box-shadow-s:
110 | 0 1px 1px 0 rgba(var(--color-black), 0.05),
111 | 0 1px 2px 0 rgba(var(--color-black), 0.05);
112 | --box-shadow-l:
113 | 0 0px 2px 0 rgba(var(--color-black), 0.05),
114 | 0 1px 2px 0 rgba(var(--color-black), 0.05),
115 | 0 2px 4px -1px rgba(var(--color-black), 0.05),
116 | 0 4px 8px -2px rgba(var(--color-black), 0.05),
117 | 0 8px 16px -4px rgba(var(--color-black), 0.05),
118 | 0 16px 32px -8px rgba(var(--color-black), 0.05);
119 | --box-inset-shadow-s: 0 1px 1px 0 rgba(var(--color-black), 0.05) inset;
120 | --box-inset-shadow-m: 0 1px 1px 0px rgba(var(--color-black), 0.05) inset,
121 | 0 2px 2px 1px rgba(var(--color-black), 0.05) inset;
122 | --text-shadow-s: 0 1px 1px rgba(var(--color-black), 0.05);
123 | }
124 |
125 | /* design_tokens/shape.css */
126 | .active-analytics {
127 | --border-radius-s: calc(var(--space) * 0.25);
128 | --border-radius-m: var(--space-1-2);
129 | }
130 |
131 | /* design_tokens/space.css */
132 | *,
133 | *::before,
134 | *::after {
135 | box-sizing: border-box;
136 | }
137 |
138 | .active-analytics {
139 | --space: 0.5rem;
140 | --space-1-4: calc(var(--space) / 4);
141 | --space-1-2: calc(var(--space) / 2);
142 | --space-2x: calc(var(--space) * 2);
143 | --space-3x: calc(var(--space) * 3);
144 | --space-4x: calc(var(--space) * 4);
145 | --space-5x: calc(var(--space) * 5);
146 | --space-6x: calc(var(--space) * 6);
147 | --space-7x: calc(var(--space) * 7);
148 | }
149 |
150 | /* elements */
151 |
152 | /* text */
153 | .active-analytics ::-moz-selection {
154 | background: rgba(var(--color-blue-100), 1);
155 | text-shadow: none;
156 | }
157 |
158 | .active-analytics ::selection {
159 | background: rgba(var(--color-blue-100), 1);
160 | text-shadow: none;
161 | }
162 |
163 | .active-analytics p {
164 | margin: 0;
165 | padding: calc(var(--space) - 2px) 0 calc(var(--space-2x) + 2px);
166 | }
167 |
168 | .active-analytics p.is-large {
169 | font-size: var(--font-size-70);
170 | line-height: var(--space-4x);
171 | padding: var(--space) 0 var(--space-2x);
172 | }
173 |
174 | .active-analytics p + :not(p) {
175 | margin-top: var(--space-3x);
176 | }
177 |
178 | .active-analytics h1,
179 | .active-analytics .is-h1 {
180 | display: inline-block;
181 | margin: 0;
182 | padding: calc(var(--space-2x) + 2px) 0 calc(var(--space) - 2px);
183 | color: rgba(var(--color-grey-700), 1);
184 | font-size: var(--font-size-110);
185 | font-weight: 700;
186 | letter-spacing: -0.05rem;
187 | line-height: var(--space-7x);
188 | text-transform: none;
189 | }
190 |
191 | .active-analytics h2,
192 | .active-analytics .is-h2 {
193 | margin: 0;
194 | padding: calc(var(--space) + 1px) 0 calc(var(--space-2x) - 1px);
195 | color: rgba(var(--color-grey-700), 1);
196 | font-size: var(--font-size-100);
197 | font-weight: 700;
198 | letter-spacing: -0.05rem;
199 | line-height: var(--space-6x);
200 | text-transform: none;
201 | }
202 |
203 | .active-analytics * + h2,
204 | .active-analytics * + .is-h2 {
205 | margin-top: var(--space-3x);
206 | }
207 |
208 | .active-analytics h3,
209 | .active-analytics .is-h3 {
210 | margin: 0;
211 | padding: calc(var(--space) - 3px) 0 calc(var(--space-2x) + 3px);
212 | color: rgba(var(--color-grey-700), 1);
213 | font-size: var(--font-size-60);
214 | font-weight: 700;
215 | letter-spacing: 0;
216 | line-height: var(--space-3x);
217 | text-transform: none;
218 | }
219 |
220 | .active-analytics small {
221 | font-size: var(--font-size-30);
222 | font-weight: 400;
223 | text-transform: none;
224 | color: rgba(var(--color-grey-400), 1);
225 | }
226 |
227 | /**** BUTTONS *****/
228 | .active-analytics button,
229 | .active-analytics input[type="submit"],
230 | .active-analytics input[type="button"],
231 | .active-analytics input[type="reset"],
232 | .active-analytics [role="button"],
233 | .active-analytics :any-link[role="button"] {
234 | /* Display */
235 | display: inline-flex;
236 | width: auto;
237 | cursor: pointer;
238 | align-items: center;
239 | justify-content: center;
240 | align-self: start;
241 | transition: 0.2s all ease-in-out;
242 | background-color: transparent;
243 | min-height: var(--space-5x);
244 | margin: 0;
245 | padding: var(--space) calc(var(--space) * 2);
246 | border: none;
247 | border-radius: var(--border-radius-s, 0);
248 | box-shadow: 0 0 0 1px rgba(var(--color-grey-200), 1) inset,
249 | var(--box-shadow-s);
250 | outline: 0;
251 | color: rgba(var(--color-grey-700), 1);
252 | font: inherit;
253 | font-family: inherit;
254 | font-size: var(--font-size-40);
255 | font-weight: 700;
256 | letter-spacing: 0.05rem;
257 | line-height: var(--space-3x);
258 | text-align: center;
259 | text-decoration: none;
260 | text-transform: uppercase;
261 | text-shadow: var(--text-shadow-s);
262 | }
263 |
264 | .active-analytics button:hover,
265 | .active-analytics input[type="submit"]:hover,
266 | .active-analytics input[type="button"]:hover,
267 | .active-analytics input[type="reset"]:hover,
268 | .active-analytics [role="button"]:hover,
269 | .active-analytics :any-link[role="button"]:hover,
270 | .active-analytics button:active,
271 | .active-analytics input[type="submit"]:active,
272 | .active-analytics input[type="button"]:active,
273 | .active-analytics input[type="reset"]:active,
274 | [role="button"]:active,
275 | :any-link[role="button"]:active {
276 | color: rgba(var(--color-grey-800), 1);
277 | box-shadow: 0 0 0 1px rgba(var(--color-grey-300), 1) inset,
278 | var(--box-shadow-s);
279 | }
280 |
281 | .active-analytics button:focus,
282 | .active-analytics input[type="submit"]:focus,
283 | .active-analytics input[type="button"]:focus,
284 | .active-analytics input[type="reset"]:focus,
285 | .active-analytics [role="button"]:focus,
286 | .active-analytics :any-link[role="button"]:focus {
287 | outline: 2px solid rgba(var(--color-blue-200), 1);
288 | border: 0;
289 | color: rgba(var(--color-grey-900), 1);
290 | z-index: 1;
291 | }
292 |
293 | .active-analytics button[aria-expanded="true"],
294 | .active-analytics button[type="reset"][aria-expanded="true"],
295 | .active-analytics input[type="submit"][aria-expanded="true"],
296 | .active-analytics input[type="button"][aria-expanded="true"],
297 | .active-analytics input[type="reset"][aria-expanded="true"],
298 | .active-analytics [role="button"][aria-expanded="true"],
299 | .active-analytics :any-link[role="button"][aria-expanded="true"] {
300 | background: rgba(var(--color-grey-100), 1);
301 | color: rgba(var(--color-grey-600), 1);
302 | box-shadow: 0 0 0 1px rgba(var(--color-grey-200), 1) inset,
303 | var(--box-inset-shadow-m);
304 | }
305 |
306 | .active-analytics button[type="reset"],
307 | .active-analytics input[type="reset"],
308 | .active-analytics button.is-reset,
309 | .active-analytics input[type="submit"].is-reset,
310 | .active-analytics input[type="button"].is-reset,
311 | .active-analytics [role="button"].is-reset,
312 | .active-analytics :any-link[role="button"].is-reset {
313 | color: rgba(var(--color-red-500), 1);
314 | }
315 |
316 | .active-analytics button[type="reset"]:hover,
317 | .active-analytics input[type="reset"]:hover,
318 | .active-analytics button[type="reset"]:hover,
319 | .active-analytics input[type="reset"]:hover,
320 | .active-analytics button.is-reset:hover,
321 | .active-analytics input[type="submit"].is-reset:hover,
322 | .active-analytics input[type="button"].is-reset:hover,
323 | .active-analytics [role="button"].is-reset:hover,
324 | .active-analytics :any-link[role="button"].is-reset:hover,
325 | .active-analytics button[type="reset"]:active,
326 | .active-analytics input[type="reset"]:active,
327 | .active-analytics button.is-reset:active,
328 | .active-analytics input[type="submit"].is-reset:active,
329 | .active-analytics input[type="button"].is-reset:active,
330 | .active-analytics [role="button"].is-reset:active,
331 | .active-analytics :any-link[role="button"].is-reset:active {
332 | background: rgba(var(--color-red-500), 1);
333 | color: rgba(var(--color-red-20), 1);
334 | box-shadow: 0 0 0 1px rgba(var(--color-red-600), 1) inset,
335 | var(--box-shadow-s);
336 | }
337 |
338 | .active-analytics button[type="reset"]:focus,
339 | .active-analytics input[type="reset"]:focus,
340 | .active-analytics button.is-reset:focus,
341 | .active-analytics input[type="submit"].is-reset:focus,
342 | .active-analytics input[type="button"].is-reset:focus,
343 | .active-analytics [role="button"].is-reset:focus,
344 | .active-analytics :any-link[role="button"].is-reset:focus {
345 | background: rgba(var(--color-red-500), 1);
346 | color: rgba(var(--color-red-20), 1);
347 | outline: 2px solid rgba(var(--color-red-200), 1);
348 | }
349 |
350 | .active-analytics button.is-link,
351 | .active-analytics button[type="reset"].is-link,
352 | .active-analytics input[type="submit"].is-link,
353 | .active-analytics input[type="button"].is-link,
354 | .active-analytics input[type="reset"].is-link,
355 | .active-analytics [role="button"].is-link,
356 | .active-analytics :any-link[role="button"].is-link {
357 | background: transparent;
358 | color: rgba(var(--color-grey-500), 1);
359 | text-decoration: underline;
360 | border-color: transparent;
361 | box-shadow: none !important;
362 | }
363 |
364 | .active-analytics button.is-link:hover,
365 | .active-analytics button[type="reset"].is-link:hover,
366 | .active-analytics input[type="submit"].is-link:hover,
367 | .active-analytics input[type="button"].is-link:hover,
368 | .active-analytics input[type="reset"].is-link:hover,
369 | .active-analytics [role="button"].is-link:hover,
370 | .active-analytics :any-link[role="button"].is-link:hover,
371 | .active-analytics button.is-link:active,
372 | .active-analytics button[type="reset"].is-link:active,
373 | .active-analytics input[type="submit"].is-link:active,
374 | .active-analytics input[type="button"].is-link:active,
375 | .active-analytics input[type="reset"].is-link:active,
376 | .active-analytics [role="button"].is-link:active,
377 | .active-analytics :any-link[role="button"].is-link:active {
378 | background: transparent;
379 | color: rgba(var(--color-grey-700), 1);
380 | box-shadow: none;
381 | }
382 |
383 | .active-analytics button.is-link:focus,
384 | .active-analytics button[type="reset"].is-link:focus,
385 | .active-analytics input[type="submit"].is-link:focus,
386 | .active-analytics input[type="button"].is-link:focus,
387 | .active-analytics input[type="reset"].is-link:focus,
388 | .active-analytics [role="button"].is-link:focus,
389 | .active-analytics :any-link[role="button"].is-link:focus {
390 | color: rgba(var(--color-grey-700), 1);
391 | }
392 |
393 | /* FORMS */
394 | .active-analytics form {
395 | overflow: visible;
396 | }
397 |
398 | .active-analytics form > * + * {
399 | margin-top: var(--space-3x, 1em);
400 | }
401 |
402 | .active-analytics form *[hidden] {
403 | margin: 0;
404 | }
405 |
406 | .active-analytics label {
407 | display: block;
408 | line-height: var(--space-3x);
409 | padding: 0;
410 | text-align: left;
411 | text-transform: uppercase;
412 | font-size: var(--font-size-30);
413 | font-weight: 400;
414 | letter-spacing: 0.1rem;
415 | color: rgba(var(--color-grey-500), 1);
416 | -webkit-user-select: none;
417 | -moz-user-select: none;
418 | -ms-user-select: none;
419 | }
420 |
421 | .active-analytics input {
422 | display: block;
423 | width: 100%;
424 | background-color: rgba(var(--color-grey-00), 1);
425 | min-height: var(--space-5x);
426 | padding: calc(var(--space) - 1px) calc(var(--space-2x) - 1px);
427 | margin: 0;
428 | border: 1px solid rgba(var(--color-grey-50), 1);
429 | border-radius: var(--border-radius-s, 0);
430 | outline: 0 none;
431 | box-shadow: var(--box-inset-shadow-s);
432 | color: rgba(var(--color-grey-600), 1);
433 | font-family: inherit;
434 | font-size: var(--base-font-size);
435 | line-height: var(--space-3x);
436 | }
437 |
438 | .active-analytics input:focus{
439 | outline: 0 none;
440 | border-color: rgba(var(--color-blue-400), 1);
441 | box-shadow:
442 | 0 0 0 2px rgba(var(--color-blue-200), 1),
443 | var(--box-shadow-s);
444 | }
445 |
446 | .active-analytics label + input {
447 | margin-top: 0;
448 | }
449 |
450 | .active-analytics ::placeholder {
451 | color: rgba(var(--color-grey-300), 1);
452 | }
453 |
454 | .active-analytics a:any-link {
455 | color: rgba(var(--color-blue-500, inherit), 1);
456 | text-decoration: underline;
457 | }
458 |
459 | .active-analytics a:any-link:hover {
460 | color: rgba(var(--color-blue-600), 1);
461 | }
462 |
463 | .active-analytics a:any-link > * + * {
464 | margin-left: var(--space);
465 | text-decoration: none;
466 | }
467 |
468 | .active-analytics a:any-link[target="_blank"] {
469 | padding-right: 16px;
470 | position: relative;
471 | display: inline-block;
472 | }
473 |
474 | /* Arrow to signify external links */
475 | .active-analytics a:any-link[target="_blank"]::after,
476 | .active-analytics a:any-link[target="_blank"]::before {
477 | content: "";
478 | display: block;
479 | position: absolute;
480 | right: 0;
481 | }
482 |
483 | .active-analytics a:any-link[target="_blank"]::before {
484 | background: currentColor;
485 | transform: rotate(-45deg);
486 | top: 50%;
487 | height: 2px;
488 | width: 12px;
489 | }
490 |
491 | .active-analytics a:any-link[target="_blank"]::after {
492 | width: 8px;
493 | height: 8px;
494 | border-right: 2px solid;
495 | border-top: 2px solid;
496 | top: calc(50% - 5px);
497 | }
498 |
499 | .active-analytics table {
500 | width: 100%;
501 | padding: 0;
502 | border-spacing: 0;
503 | }
504 |
505 | .active-analytics th {
506 | text-transform: uppercase;
507 | font-weight: var(--font-weight-700);
508 | font-size: calc(var(--base-font-size) / var(--ratio) / var(--ratio));
509 | letter-spacing: 0.03em;
510 | transform: translateY(2px);
511 | padding: 12px 24px;
512 | text-align: left;
513 | color: rgba(var(--color-grey-500), 1);
514 | }
515 |
516 | .active-analytics tbody tr {
517 | box-shadow: 0 -1px 0 0 rgba(var(--color-grey-50), 1) inset;
518 | }
519 |
520 | .active-analytics tbody tr:hover {
521 | background: rgba(var(--color-grey-50), 1);
522 | }
523 |
524 | .active-analytics td {
525 | color: rgba(var(--color-grey-500), 1);
526 | padding: 12px 24px;
527 | }
528 |
529 | /* components/breadcrumb.css */
530 |
531 | .active-analytics .breadcrumb ol {
532 | margin: 0;
533 | padding-left: 0;
534 | list-style: none;
535 | }
536 |
537 | .active-analytics .breadcrumb li {
538 | display: inline;
539 | }
540 |
541 | .active-analytics .breadcrumb li + li::before {
542 | content: '';
543 | display: inline-block;
544 | margin: 0 var(--space);
545 | transform: rotate(20deg);
546 | border-right: 1px solid rgba(var(--color-grey-400), 1);
547 | height: 0.75em;
548 | }
549 |
550 | .active-analytics .breadcrumb li:last-child {
551 | color: rgba(var(--color-grey-700), 1);
552 | font-weight: var(--font-weight-700);
553 | }
554 |
555 | /* components/dialog.css */
556 |
557 | .active-analytics .dialog-backdrop {
558 | height: 100vh;
559 | width: 100%;
560 | position: fixed;
561 | top: 0;
562 | left: 0;
563 | background: rgba(var(--color-grey-300), .7);
564 | z-index: var(--z-40, 90);
565 | animation: backdrop 0.1s cubic-bezier(0.165, 0.840, 0.440, 1.000);
566 | display: flex;
567 | align-items: center;
568 | justify-content: center;
569 | }
570 |
571 | @supports (-webkit-backdrop-filter: none) or (backdrop-filter: none) {
572 | .active-analytics .dialog-backdrop {
573 | -webkit-backdrop-filter: blur(4px);
574 | backdrop-filter: blur(4px);
575 | background: rgba(var(--color-grey-300), .7);
576 | }
577 | }
578 |
579 | /* Modale */
580 |
581 | .active-analytics [role="dialog"] {
582 | position: relative;
583 | padding: var(--space-3x);
584 | background-color: rgba(var(--color-bg), 1);
585 | border: 1px solid rgba(var(--color-grey-35), 1);
586 | border-radius: var(--border-radius-m, 0);
587 | min-width: 480px;
588 | max-width: 100%;
589 | max-height: 100vh;
590 | overflow-y: auto;
591 | animation: modale 0.5s cubic-bezier(0.165, 0.840, 0.440, 1.000);
592 | }
593 |
594 | .active-analytics [role="dialog"] > header {
595 | width: calc(100% + var(--space-6x));
596 | display: flex;
597 | align-items: center;
598 | border-radius: var(--border-radius-m, 0) var(--border-radius-m, 0) 0 0;
599 | margin: calc(var(--space-3x) * -1) calc(var(--space-3x) * -1) calc(var(--space-3x) * 1);
600 | padding: calc(var(--space) * 1.5 - 1px) calc(var(--space-3x) - 1px) calc(var(--space) * 1.5 - 1px);
601 | border-bottom: 1px solid rgba(var(--color-grey-50), 1);
602 | color: rgba(var(--color-grey-500), 1);
603 | }
604 |
605 | .active-analytics [role="dialog"] > header button {
606 | margin-left: auto;
607 | }
608 |
609 | .active-analytics [role="dialog"] > footer {
610 | background: rgba(var(--color-grey-50), 1);
611 | width: calc(100% + var(--space-6x));
612 | border-radius: 0 0 var(--border-radius-m, 0) var(--border-radius-m, 0);
613 | margin: var(--space-3x) calc(var(--space-3x) * -1) calc(var(--space-3x) * -1);
614 | padding: calc(var(--space) * 1.5 - 1px) calc(var(--space-3x) - 1px) calc(var(--space) * 1.5 - 1px);
615 | border-top: 1px solid rgba(var(--color-grey-50), 1);
616 | }
617 |
618 | @media screen and (min-width: 640px) {
619 | .active-analytics [role="dialog"] {
620 | min-width: 640px;
621 | min-height: auto;
622 | box-shadow: var(--box-shadow-l);
623 | background-color: rgba(var(--color-bg), 1);
624 | }
625 | }
626 |
627 | @keyframes modale {
628 | 0% {
629 | top: -48px;
630 | opacity: 0;
631 | }
632 | 100% {
633 | top: 0;
634 | opacity: 1;
635 | }
636 | }
637 |
638 | @keyframes backdrop {
639 | 0% {
640 | opacity: 0;
641 | }
642 | 100% {
643 | opacity: 1;
644 | }
645 | }
646 | /* components/grid_auto.css */
647 |
648 | .active-analytics .grid-auto {
649 | --gap: var(--space-3x);
650 | --col-min-width: calc(var(--space) * 38);
651 | display: grid;
652 | grid-gap: var(--gap, 0);
653 | grid-template-columns: repeat(auto-fit, minmax(var(--col-min-width), 1fr));
654 | width: 100%;
655 | padding: 0;
656 | }
657 |
658 | .active-analytics .grid-auto > * {
659 | list-style-type: none;
660 | margin: 0;
661 | }
662 |
663 | .active-analytics .grid-auto * + *,
664 | .active-analytics .grid-auto * + .card {
665 | margin-top: 0;
666 | }
667 |
668 | .active-analytics .grid-auto + * {
669 | margin-top: var(--space-3x);
670 | }
671 |
672 | /* components/group.css */
673 |
674 | .active-analytics [role="group"] {
675 | display: flex;
676 | align-items: flex-end;
677 | border-radius: var(--border-radius-s, 0);
678 | border-radius: 4px;
679 | max-width: 100%;
680 | }
681 |
682 | .active-analytics [role="group"] > * {
683 | border-radius: 0;
684 | margin: auto 0 0;
685 | }
686 |
687 | .active-analytics [role="group"] > *:first-child {
688 | border-radius: var(--border-radius-s, 0) 0 0 var(--border-radius-s, 0);
689 | }
690 |
691 | .active-analytics [role="group"] > *:last-child {
692 | border-radius: 0 var(--border-radius-s, 0) var(--border-radius-s, 0) 0;
693 | }
694 |
695 | .active-analytics [role="group"] > .input {
696 | margin: auto var(--space) 0 0;
697 | }
698 |
699 | .active-analytics [role="group"] > .input:last-child {
700 | margin: auto 0 0 0;
701 | }
702 |
703 | .active-analytics [role="group"] > label {
704 | margin: 0;
705 | }
706 |
707 | .active-analytics [role="group"].is-block {
708 | width: 100%;
709 | }
710 |
711 | .active-analytics [role="group"].is-block > * {
712 | flex: 1;
713 | }
714 |
715 | /* components/card.css */
716 | .active-analytics .card {
717 | background: rgba(var(--color-grey-20), 1);
718 | border: 0;
719 | border-radius: var(--border-radius-s, 0);
720 | box-shadow: 0 0 0 1px inset rgba(var(--color-grey-50), 1), var(--box-shadow-s);
721 | padding: var(--space-3x);
722 | list-style-type: none;
723 | position: relative;
724 | overflow: visible;
725 | }
726 |
727 | .active-analytics .card > header {
728 | display: flex;
729 | align-items: center;
730 | min-height: var(--space-6x);
731 | width: calc(100% + var(--space-6x));
732 | border-radius: var(--border-radius-m, 0) var(--border-radius-m, 0) 0 0;
733 | padding: var(--space) var(--space-3x);
734 | margin: calc(var(--space-3x) * -1) calc(var(--space-3x) * -1) calc(var(--space-3x) * 1);
735 | box-shadow: 0 1px 0 0 rgba(var(--color-grey-50), 1);
736 | color: rgba(var(--color-grey-500), 1);
737 | }
738 |
739 | .active-analytics .card > header > * {
740 | margin: 0 var(--space-1-2);
741 | padding: 0;
742 | text-transform: none;
743 | letter-spacing: 0;
744 | font-size: var(--font-size-50);
745 | line-height: var(--space-3x);
746 | }
747 |
748 | .active-analytics .card > footer {
749 | background: rgba(var(--color-grey-50), 1);
750 | box-shadow: 0 -1px 0 0 rgba(var(--color-grey-100), .8);
751 | border-radius: 0 0 var(--border-radius-m, 0) var(--border-radius-m, 0);
752 | margin: var(--space-3x) calc(var(--space-3x) * -1) calc(var(--space-3x) * -1);
753 | padding: var(--space-2x) calc(var(--space-3x) - 1px);
754 | width: calc(100% + var(--space-6x));
755 | }
756 |
757 | /* components/menu.css */
758 |
759 | .active-analytics [role="menu"] {
760 | text-align: left;
761 | color: rgba(var(--color-grey-500), 1);
762 | position: relative;
763 | list-style-type: none;
764 | margin: 0;
765 | padding: 6px 0 18px;
766 | text-transform: none;
767 | font-size: var(--font-size-50);
768 | letter-spacing: 0.03em;
769 | }
770 |
771 | .active-analytics li[role="menuitem"] {
772 | display: flex;
773 | flex-direction: column;
774 | }
775 |
776 | .active-analytics a[role="menuitem"]:not(.button) {
777 | padding: 0 24px;
778 | margin: 0;
779 | font-size: inherit;
780 | text-decoration: none;
781 | color: rgba(var(--color-grey-700), 1);
782 | display: flex;
783 | align-items: center;
784 | }
785 |
786 | .active-analytics a[role="menuitem"]:not(.button):hover {
787 | color: rgba(var(--color-grey-800), 1);
788 | }
789 |
790 | .active-analytics a[role="menuitem"]:not(.button)[aria-current="true"] {
791 | background: rgba(var(--color-grey-100), 1);
792 | color: rgba(var(--color-blue-700), 1);
793 | }
794 |
795 | .active-analytics .menubutton {
796 | position: relative;
797 | display: inline-block;
798 | }
799 |
800 | .active-analytics .menubutton > button,
801 | .active-analytics .menubutton > input[type="submit"],
802 | .active-analytics .menubutton > input[type="button"],
803 | .active-analytics .menubutton > input[type="reset"],
804 | .active-analytics .menubutton > [role="button"],
805 | .active-analytics .menubutton > :any-link[role="button"] {
806 | position: relative;
807 | margin: 0;
808 | }
809 |
810 | .active-analytics .menubutton > button:after,
811 | .active-analytics .menubutton > input[type="submit"]:after,
812 | .active-analytics .menubutton > input[type="button"]:after,
813 | .active-analytics .menubutton > input[type="reset"]:after,
814 | .active-analytics .menubutton > [role="button"]:after,
815 | .active-analytics .menubutton > :any-link[role="button"]:after {
816 | content: "…";
817 | margin-left: var(--space);
818 | }
819 |
820 | .active-analytics .menubutton > [role="menu"] {
821 | display: none;
822 | position: absolute;
823 | z-index: 1000;
824 | top: var(--space-7x);
825 | left: 0;
826 | margin: 0;
827 | padding: var(--space, 0) 0;
828 | border-radius: var(--border-radius-m, 0);
829 | width: auto;
830 | background: rgba(var(--color-grey-00), 1);
831 | box-shadow: var(--box-shadow-l);
832 | border: 1px solid rgba(var(--color-grey-50), 1);
833 | }
834 |
835 | .active-analytics .menubutton > [role="menu"]:after,
836 | .active-analytics .menubutton > [role="menu"]:before {
837 | bottom: 100%;
838 | left: var(--space-3x);
839 | border: solid transparent;
840 | content: "";
841 | height: 0;
842 | width: 0;
843 | position: absolute;
844 | pointer-events: none;
845 | }
846 |
847 | .active-analytics .menubutton > [role="menu"]:after {
848 | border-color: transparent;
849 | border-bottom-color: rgba(var(--color-grey-00), 1);
850 | border-width: 6px;
851 | margin-left: -6px;
852 | }
853 |
854 | .active-analytics .menubutton > [role="menu"]:before {
855 | border-color: transparent;
856 | border-bottom-color: rgba(var(--color-grey-50), 1);
857 | border-width: 8px;
858 | margin-left: -8px;
859 | }
860 |
861 | .active-analytics .menubutton > [role="menu"] [role="menuitem"] {
862 | display: block;
863 | padding: 0px 16px;
864 | background: rgba(var(--color-grey-00), 1);
865 | }
866 |
867 | .active-analytics .menubutton > [role="menu"] [role="menuitem"]:hover,
868 | .active-analytics .menubutton > [role="menu"] [role="menuitem"]:focus {
869 | background: rgba(var(--color-grey-50), 1);
870 | color: rgba(var(--color-grey-700), 1);
871 | }
872 |
873 | .active-analytics *[hidden=true] {
874 | display: none;
875 | }
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/ariato.js:
--------------------------------------------------------------------------------
1 | Ariato = {}
2 |
3 | Ariato.launchWhenDomIsReady = function(root) {
4 | if (document.readyState != "loading") {
5 | Ariato.launch()
6 | Ariato.launch(document, "aria-roledescription")
7 | Ariato.launch(document, "data-ariato")
8 | }
9 | else
10 | document.addEventListener("DOMContentLoaded", function() { Ariato.launchWhenDomIsReady(root) } )
11 | }
12 |
13 | Ariato.launch = function(root, attribute, parent) {
14 | attribute || (attribute = "role")
15 | var elements = (root || document).querySelectorAll("[" + attribute + "]")
16 | for (var i = 0; i < elements.length; i++)
17 | Ariato.start(elements[i], attribute, parent)
18 | }
19 |
20 | Ariato.mount = function() {
21 | }
22 |
23 | Ariato.start = function(element, attribute, parent) {
24 | var names = element.getAttribute(attribute).split(" ")
25 | for (var i = 0; i < names.length; i++) {
26 | var name = names[i].charAt(0).toUpperCase() + names[i].slice(1) // Capitalize
27 | var func = Ariato.stringToFunction("Ariato." + name) || Ariato.stringToFunction(name)
28 | if (func instanceof Function)
29 | Ariato.instanciate(func, element, parent)
30 | }
31 | }
32 |
33 | Ariato.instanciate = function(func, element, parent) {
34 | try {
35 | controller = Object.create(func.prototype)
36 | controller.parent = parent
37 | controller.node = element
38 | Ariato.initialize(controller, element)
39 | func.call(controller, element)
40 | } catch (ex) {
41 | console.error(ex)
42 | }
43 | }
44 |
45 | Ariato.stringToFunction = function(fullName) {
46 | var func = window, names = fullName.split(".")
47 | for (var i = 0; i < names.length; i++)
48 | if (!(func = func[names[i]]))
49 | return null
50 | return func
51 | }
52 |
53 | Ariato.initialize = function(controller, container) {
54 | Ariato.listenEvents(container, controller)
55 | Ariato.assignRoles(container, controller)
56 | }
57 |
58 | Ariato.listenEvents = function(root, controller) {
59 | var elements = root.querySelectorAll("[data-event]")
60 | for (var i = 0; i < elements.length; i++) {
61 | elements[i].getAttribute("data-event").split(" ").forEach(function(eventAndAction) {
62 | var array = eventAndAction.split("->")
63 | Ariato.listenEvent(controller, elements[i], array[0], array[1])
64 | })
65 | }
66 | }
67 |
68 | Ariato.listenEvent = function(controller, element, event, action) {
69 | if (controller[action] instanceof Function)
70 | element.addEventListener(event, controller[action].bind(controller))
71 | }
72 |
73 | Ariato.findRoles = function(container) {
74 | var roles = {}, elements = container.querySelectorAll("[data-role]")
75 | for (var i = 0; i < elements.length; i++) {
76 | var name = elements[i].getAttribute("data-role")
77 | roles[name] ? roles[name].push(elements[i]) : roles[name] = [elements[i]]
78 | }
79 | return roles
80 | }
81 |
82 | Ariato.assignRoles = function(container, controller) {
83 | controller.roles = Ariato.findRoles(container)
84 | for (var name in controller.roles)
85 | if (controller.roles[name].length == 1)
86 | controller[name] = controller.roles[name][0]
87 | }
88 |
89 | Ariato.Dialog = function(node) {
90 | node.setAttribute("hidden", true)
91 | node.addEventListener("open", this.open.bind(this))
92 | node.addEventListener("close", this.close.bind(this))
93 | node.addEventListener("keydown", this.keydown.bind(this))
94 | }
95 |
96 | Ariato.Dialog.open = function(elementOrId) {
97 | var dialog = elementOrId instanceof Element ? elementOrId : document.getElementById(elementOrId)
98 | dialog && dialog.dispatchEvent(new CustomEvent("open"))
99 | }
100 |
101 | Ariato.Dialog.close = function(button) {
102 | var dialog = Ariato.Dialog.current()
103 | if (dialog && dialog.node.contains(button))
104 | dialog.close()
105 | }
106 |
107 | Ariato.Dialog.closeCurrent = function() {
108 | var dialog = Ariato.Dialog.current()
109 | dialog && dialog.close()
110 | }
111 |
112 | Ariato.Dialog.replace = function(elementOrId) {
113 | Ariato.Dialog.closeCurrent()
114 | Ariato.Dialog.open(elementOrId)
115 | }
116 |
117 | Ariato.Dialog.close = function(button) {
118 | var dialog = Ariato.Dialog.current()
119 | if (dialog && dialog.node.contains(button))
120 | dialog.close()
121 | }
122 |
123 | Ariato.Dialog.list = []
124 |
125 | Ariato.Dialog.current = function() {
126 | return this.list[this.list.length - 1]
127 | }
128 |
129 | Ariato.Dialog.prototype.open = function(event) {
130 | Ariato.Dialog.list.push(this)
131 | document.addEventListener("focus", this.bindedLimitFocusScope = this.limitFocusScope.bind(this), true)
132 | this.initiator = document.activeElement
133 | this.node.removeAttribute("hidden")
134 |
135 | this.lockScrolling()
136 | this.createBackdrop()
137 | this.createFocusStoppers()
138 | this.focusFirstDescendant(this.node)
139 | }
140 |
141 | Ariato.Dialog.prototype.close = function(event) {
142 | document.removeEventListener("focus", this.bindedLimitFocusScope, true)
143 | this.node.setAttribute("hidden", true)
144 | this.removeFocusStoppers()
145 | this.removeBackdrop()
146 | this.unlockScrolling()
147 | this.initiator.focus()
148 | Ariato.Dialog.list.pop()
149 | }
150 |
151 | Ariato.Dialog.prototype.keydown = function(event) {
152 | if (event.key == "Escape")
153 | this.close()
154 | }
155 |
156 | Ariato.Dialog.prototype.focusFirstDescendant = function(parent) {
157 | var focusable = ["A", "BUTTON", "INPUT", "SELECT", "TEXTAREA"]
158 |
159 | for (var i = 0; i < parent.children.length; i++) {
160 | var child = parent.children[i]
161 | if (focusable.indexOf(child.nodeName) != -1 && !child.disabled && child.type != "hidden") {
162 | child.focus()
163 | return child
164 | }
165 | else {
166 | var focus = this.focusFirstDescendant(child)
167 | if (focus) return focus
168 | }
169 | }
170 | }
171 |
172 | Ariato.Dialog.prototype.limitFocusScope = function(event) {
173 | if (this == Ariato.Dialog.current())
174 | if (!this.node.contains(event.target))
175 | this.focusFirstDescendant(this.node)
176 | }
177 |
178 | Ariato.Dialog.prototype.lockScrolling = function() {
179 | document.body.style.position = "fixed";
180 | document.body.style.top = "-" + window.scrollY + "px";
181 | }
182 |
183 | Ariato.Dialog.prototype.unlockScrolling = function() {
184 | var scrollY = document.body.style.top
185 | document.body.style.position = ""
186 | document.body.style.top = ""
187 | window.scrollTo(0, parseInt(scrollY || "0") * -1)
188 | }
189 |
190 | Ariato.Dialog.prototype.createFocusStoppers = function() {
191 | this.node.parentNode.insertBefore(this.focusStopper1 = document.createElement("div"), this.node)
192 | this.focusStopper1.tabIndex = 0
193 |
194 | this.node.parentNode.insertBefore(this.focusStopper2 = document.createElement("div"), this.node.nextSibling)
195 | this.focusStopper2.tabIndex = 0
196 | }
197 |
198 | Ariato.Dialog.prototype.removeFocusStoppers = function() {
199 | this.focusStopper1 && this.focusStopper1.parentNode.removeChild(this.focusStopper1)
200 | this.focusStopper2 && this.focusStopper2.parentNode.removeChild(this.focusStopper2)
201 | }
202 |
203 | Ariato.Dialog.prototype.createBackdrop = function() {
204 | this.backdrop = document.createElement("div")
205 | this.backdrop.classList.add("dialog-backdrop")
206 | this.node.parentNode.insertBefore(this.backdrop, this.node)
207 | this.backdrop.appendChild(this.node)
208 | }
209 |
210 | Ariato.Dialog.prototype.removeBackdrop = function() {
211 | this.backdrop.parentNode.insertBefore(this.node, this.backdrop)
212 | this.backdrop.parentNode.removeChild(this.backdrop)
213 | this.backdrop = null
214 | }
215 |
216 | Ariato.Alertdialog = Ariato.Dialog
217 |
218 | Ariato.MenuButton = function(node) {
219 | this.node = this.button = node
220 | this.menu = document.getElementById(this.button.getAttribute("aria-controls"))
221 |
222 | this.menu.addEventListener("keydown", this.keydown.bind(this))
223 | this.button.addEventListener("keydown", this.keydown.bind(this))
224 |
225 | this.button.addEventListener("click", this.clicked.bind(this))
226 | window.addEventListener("click", this.windowClicked.bind(this), true)
227 | }
228 |
229 | Ariato.MenuButton.prototype.clicked = function(event) {
230 | this.node.getAttribute("aria-expanded") == "true" ? this.close() : this.open()
231 | }
232 |
233 | Ariato.MenuButton.prototype.windowClicked = function() {
234 | if (!this.node.contains(event.target) && this.node.getAttribute("aria-expanded") == "true")
235 | this.close()
236 | }
237 |
238 | Ariato.MenuButton.prototype.open = function() {
239 | this.button.setAttribute("aria-expanded", "true")
240 | this.menu.style.display = "block"
241 | }
242 |
243 | Ariato.MenuButton.prototype.close = function() {
244 | this.button.setAttribute("aria-expanded", "false")
245 | this.menu.style.display = null
246 | }
247 |
248 | Ariato.MenuButton.prototype.keydown = function(event) {
249 | switch(event.key) {
250 | case "Escape":
251 | this.close()
252 | break
253 | case "ArrowDown":
254 | event.preventDefault()
255 | this.focusNextItem()
256 | break
257 | case "ArrowUp":
258 | event.preventDefault()
259 | this.focusPreviousItem()
260 | break
261 | case "Tab":
262 | this.close()
263 | case "Home":
264 | case "PageUp":
265 | event.preventDefault()
266 | this.items()[0].focus()
267 | break
268 | case "End":
269 | case "PageDown":
270 | event.preventDefault()
271 | var items = this.items()
272 | items[items.length-1].focus()
273 | break
274 | }
275 | }
276 |
277 | Ariato.MenuButton.prototype.items = function() {
278 | return this.menu.querySelectorAll("[role=menuitem]")
279 | }
280 |
281 | Ariato.MenuButton.prototype.currentItem = function() {
282 | return this.menu.querySelector("[role=menuitem]:focus")
283 | }
284 |
285 | Ariato.MenuButton.prototype.nextItem = function() {
286 | var items = this.items()
287 | var current = this.currentItem()
288 | if (!current) return items[0]
289 | for (var i = 0; i < items.length; i++) {
290 | if (items[i] == current)
291 | return items[i+1]
292 | }
293 | }
294 |
295 | Ariato.MenuButton.prototype.previousItem = function() {
296 | var items = this.items()
297 | var current = this.currentItem()
298 | if (!current) return items[0]
299 | for (var i = 0; i < items.length; i++) {
300 | if (items[i] == current)
301 | return items[i-1]
302 | }
303 | }
304 |
305 | Ariato.MenuButton.prototype.focusNextItem = function() {
306 | var item = this.nextItem()
307 | item && item.focus()
308 | }
309 |
310 | Ariato.MenuButton.prototype.focusPreviousItem = function() {
311 | var item = this.previousItem()
312 | item && item.focus()
313 | }
314 |
315 | Ariato.Menu = function(node) {
316 | var button = this.labelledBy()
317 | button && new Ariato.MenuButton(button)
318 | }
319 |
320 | Ariato.Menu.prototype.labelledBy = function() {
321 | return document.getElementById(this.node.getAttribute("aria-labelledby"))
322 | }
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/arc.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/brave.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/chrome.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/default.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/firefox.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/internet_explorer.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/microsoft_edge.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/opera.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/safari.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/samsung_internet.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/vivaldi.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/active_analytics/assets/browsers/yandex.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/active_analytics/browsers/_table.html.erb:
--------------------------------------------------------------------------------
1 | <% if browsers.empty? %>
2 |
10 | <%= browser_icon(browser.name)%> 11 | <%= link_to browser.name, browser_path(site: params[:site], id: browser, from: params[:from], to: params[:to]) %> 12 | | 13 |<%= format_view_count browser.total %> | 14 |
10 | <%= link_to browser.version, browser_path(site: params[:site], id: params[:id], version: browser.version, from: params[:from], to: params[:to]) %> 11 | | 12 |<%= format_view_count browser.total %> | 13 |
10 | <% if page.path.present? %> 11 | <%= link_to page.path, page_path(site: page.host, page: page_to_params(page.path), from: params[:from], to: params[:to]) %> 12 | <% else %> 13 | <%= site_icon page.host %> 14 | <%= link_to page.host, site_path(site: page.host, from: params[:from], to: params[:to]) %> 15 | (page not provided ?) 16 | <% end %> 17 | | 18 |<%= format_view_count page.total %> | 19 |
10 | <% if referrer.try(:path) %> 11 | <%= site_icon referrer.host %> 12 | <% if referrer.host == params[:site] %> 13 | <%= link_to referrer.path, page_path(site: referrer.host, page: page_to_params(referrer.path), from: params[:from], to: params[:to]) %> 14 | <% else %> 15 | <%= link_to referrer.url, referrer_path(site: params[:site], referrer: referrer.url.chomp("/"), from: params[:from], to: params[:to]) %> 16 | <% end %> 17 | <% elsif referrer.host %> 18 | <%= site_icon referrer.host %> 19 | <%= link_to referrer.host, referrer_path(site: params[:site], referrer: referrer.host, from: params[:from], to: params[:to]) %> 20 | <% else %> 21 | (None or direct) 22 | <% end %> 23 | | 24 |<%= format_view_count referrer.total %> | 25 |
<%= link_to site.host, site_path(site: site.host) %> | 8 |<%= format_view_count site.total %> | 9 |
You may have mistyped the address or the page may have moved.
63 |If you are the application owner check the logs for more information.
65 |Maybe you tried to change something you didn't have access to.
63 |If you are the application owner check the logs for more information.
65 |If you are the application owner check the logs for more information.
64 |