lesli_audit 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/lesli_audit/users_controller.rb +1 -2
  3. data/app/controllers/lesli_audit/visitors_controller.rb +14 -0
  4. data/app/interfaces/lesli_audit/logger_interface.rb +22 -29
  5. data/app/services/lesli_audit/user_service.rb +105 -12
  6. data/app/services/lesli_audit/{analytic_service.rb → visitor_service.rb} +121 -24
  7. data/app/views/lesli_audit/partials/_navigation.html.erb +1 -1
  8. data/app/views/lesli_audit/requests/index.html.erb +1 -1
  9. data/app/views/lesli_audit/users/index.html.erb +36 -4
  10. data/app/views/lesli_audit/visitors/index.html.erb +118 -0
  11. data/config/routes.rb +1 -1
  12. data/db/migrate/v1.0/0501110210_create_lesli_audit_account_devices.rb +2 -1
  13. data/db/seed/devices.rb +23 -15
  14. data/db/seed/users.rb +117 -0
  15. data/db/seeds.rb +2 -2
  16. data/lib/lesli_audit/version.rb +2 -2
  17. metadata +10 -50
  18. data/app/assets/config/lesli_audit_manifest.js +0 -37
  19. data/app/controllers/lesli_audit/analytics_controller.rb +0 -23
  20. data/app/controllers/lesli_audit/user_journals_controller.rb +0 -60
  21. data/app/helpers/lesli_audit/analytics_helper.rb +0 -4
  22. data/app/views/lesli_audit/analytics/index.html.erb +0 -44
  23. data/app/views/lesli_audit/user_journals/_form.html.erb +0 -17
  24. data/app/views/lesli_audit/user_journals/_user_journal.html.erb +0 -2
  25. data/app/views/lesli_audit/user_journals/edit.html.erb +0 -12
  26. data/app/views/lesli_audit/user_journals/index.html.erb +0 -16
  27. data/app/views/lesli_audit/user_journals/new.html.erb +0 -11
  28. data/app/views/lesli_audit/user_journals/show.html.erb +0 -10
  29. data/app/views/lesli_audit/users/show.html.erb +0 -10
  30. data/lib/scss/application.scss +0 -31
  31. data/lib/vue/application.js +0 -146
  32. data/lib/vue/apps/accounts/activities.vue +0 -48
  33. data/lib/vue/apps/analytics/index.vue +0 -56
  34. data/lib/vue/apps/analytics/trends.vue +0 -148
  35. data/lib/vue/apps/dashboards/components/roles.vue +0 -71
  36. data/lib/vue/apps/dashboards/components/users.vue +0 -71
  37. data/lib/vue/apps/dashboards/show.vue +0 -8
  38. data/lib/vue/apps/requests/index.vue +0 -125
  39. data/lib/vue/apps/security/passwords.vue +0 -52
  40. data/lib/vue/apps/security/sessions.vue +0 -63
  41. data/lib/vue/apps/users/activities.vue +0 -49
  42. data/lib/vue/apps/users/index.vue +0 -89
  43. data/lib/vue/apps/users/logs.vue +0 -49
  44. data/lib/vue/apps/users/registrations.vue +0 -73
  45. data/lib/vue/apps/users/roles.vue +0 -70
  46. data/lib/vue/apps/users/workingHours.vue +0 -52
  47. data/lib/vue/components/requests.vue +0 -63
  48. data/lib/vue/components/visitors.vue +0 -76
  49. data/lib/vue/stores/accounts/activities.js +0 -87
  50. data/lib/vue/stores/analytics.js +0 -127
  51. data/lib/vue/stores/request.js +0 -70
  52. data/lib/vue/stores/security/password.js +0 -75
  53. data/lib/vue/stores/security/session.js +0 -78
  54. data/lib/vue/stores/translations.json +0 -162
  55. data/lib/vue/stores/users/activities.js +0 -76
  56. data/lib/vue/stores/users/logs.js +0 -74
  57. data/lib/vue/stores/users/registrations.js +0 -61
  58. data/lib/vue/stores/users/roles.js +0 -52
  59. data/lib/vue/stores/users/working_hours.js +0 -92
  60. data/lib/vue/stores/users.js +0 -56
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff9465d4d130455ae1ef9804d86e17e54754708857234684a0d99237f8545770
4
- data.tar.gz: 3455cb58ed202920a07a0faacf21ced66b421201a2832206e22c0495bff8d05c
3
+ metadata.gz: 6a84c6870b0006a5609054e1c8a311a0b9d57a5220dc8c75c7841eae7ffd1e8c
4
+ data.tar.gz: 882412cf089844f7c36bb58d492396c521e673374e2a13da8dcd466c23a5dc7f
5
5
  SHA512:
6
- metadata.gz: 32ea636b6528c49201ba30eb2e972deaeca0ea47bf625f5e8815a81fec5dbb83bdb3c4f450c014cb33f3eef2b9e5b040acaf8a887410d3117880e89a3736dfc6
7
- data.tar.gz: 21ee15b705797e0f6871a7432b7044b85bdaee377b376b55f568d77a329e9be51bcfc6426c80290c4ec0c08dab47fa2b8da11024c59751d3af23e7ce0e8c9256
6
+ metadata.gz: f3d60b2ebcd66549072ace0988c2c4ca40b832c2c50173960b9db99b9c4184d4c65488197633718aacaa568b10144d592d842f667430732cc57d8665e1055fc9
7
+ data.tar.gz: 7f2c3f56961cb0aaf60dab530de6a7fd30eaf29a64cfafbc84623ee96c50e7162581c9728adf5894fd68f1d290946f49e1136456a913c00cf80cd8ebe4128fd4
@@ -34,8 +34,7 @@ module LesliAudit
34
34
 
35
35
  # GET /users
36
36
  def index
37
- @requests = UserService.new(current_user, query).requests
38
- @registrations = UserService.new(current_user, query).registrations
37
+ @users = UserService.new(current_user, query).users
39
38
  @working_hours = UserService.new(current_user, query).working_hours
40
39
  end
41
40
  end
@@ -0,0 +1,14 @@
1
+ module LesliAudit
2
+ class VisitorsController < ApplicationController
3
+
4
+ # GET /analytics
5
+ def index
6
+ @visits = VisitorService.new(current_user, query).visits
7
+ @visitors = VisitorService.new(current_user, query).visitors
8
+ @requests = VisitorService.new(current_user, query).requests
9
+ @browsers = VisitorService.new(current_user, query).browsers
10
+ @devices = VisitorService.new(current_user, query).devices
11
+ @os = VisitorService.new(current_user, query).os
12
+ end
13
+ end
14
+ end
@@ -29,35 +29,28 @@ Building a better future, one line of code at a time.
29
29
  // · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
30
30
  // ·
31
31
  =end
32
-
32
+ require 'device_detector'
33
33
  module LesliAudit
34
34
  module LoggerInterface
35
35
  def get_user_agent(as_string=true)
36
36
 
37
- http_user_agent = request.env["HTTP_USER_AGENT"]
38
-
39
37
  # parse user agent
40
- user_agent = UserAgent.parse(http_user_agent)
38
+ user_agent = DeviceDetector.new(request.env["HTTP_USER_AGENT"])
41
39
 
42
- user_agent_version = user_agent.version.to_a.first(2).join(".")
40
+ #user_agent_version = user_agent.version.to_a.first(2).join(".")
43
41
 
44
42
  # return user agent as object
45
- if as_string == false
46
- return {
47
- platform: user_agent.platform,
48
- os: user_agent.os,
49
- browser: user_agent.browser,
50
- version: user_agent_version
51
- }
52
- end
53
-
54
- # return user agent info as string
55
- "#{user_agent.platform} #{user_agent.os} - #{user_agent.browser} #{user_agent_version}"
43
+ {
44
+ platform: user_agent.os_name,
45
+ browser: user_agent.name,
46
+ device: user_agent.device_type
47
+ }
56
48
  end
57
49
 
58
50
  def log_requests
59
51
  log_account_requests
60
52
  log_user_requests
53
+ log_devices
61
54
  end
62
55
 
63
56
  def log_account_requests
@@ -86,17 +79,7 @@ module LesliAudit
86
79
  def log_user_requests
87
80
  return unless current_user
88
81
  return unless session[:user_session_id]
89
-
90
- current_user.account.audit.user_journals.create({
91
- request_controller: controller_path,
92
- request_action: action_name,
93
- session_id: session[:user_session_id],
94
- user_id: current_user.id,
95
- date: Date2.new.date.to_s
96
- }) if Lesli.config.audit.dig(:enable_journals)
97
-
98
- # Determine the correct SQL "now" keyword based on the database connection
99
- now_func = ActiveRecord::Base.connection.adapter_name =~ /sqlite/i ? 'CURRENT_TIMESTAMP' : 'NOW()'
82
+ return unless Lesli.config.audit.dig(:enable_analytics)
100
83
 
101
84
  # Try to save a unique record for this request configuration
102
85
  current_user.account.audit.user_requests.upsert(
@@ -118,7 +101,16 @@ module LesliAudit
118
101
  on_duplicate: Arel.sql(
119
102
  "request_count = lesli_audit_user_requests.request_count + 1,updated_at = #{LesliDate::Compatibility.db_now}"
120
103
  )
121
- ) if Lesli.config.audit.dig(:enable_analytics)
104
+ )
105
+
106
+ return unless Lesli.config.audit.dig(:enable_journals)
107
+ current_user.account.audit.user_journals.create({
108
+ request_controller: controller_path,
109
+ request_action: action_name,
110
+ session_id: session[:user_session_id],
111
+ user_id: current_user.id,
112
+ date: Date2.new.date.to_s
113
+ })
122
114
  end
123
115
 
124
116
  def log_devices
@@ -133,11 +125,12 @@ module LesliAudit
133
125
  :created_at => Date2.new.date.to_s,
134
126
  :agent_platform => user_agent&.dig(:platform) || "unknown",
135
127
  :agent_browser => user_agent&.dig(:browser) || "unknown",
128
+ :agent_device => user_agent&.dig(:device) || "unknown",
136
129
  :agent_count => 1
137
130
  },
138
131
 
139
132
  # group of columns to consider a request as unique
140
- unique_by: %i[agent_platform agent_browser created_at account_id],
133
+ unique_by: %i[agent_platform agent_browser agent_device created_at account_id],
141
134
 
142
135
  # if request id is not unique
143
136
  # - increase the counter for this configuration
@@ -33,17 +33,104 @@ Building a better future, one line of code at a time.
33
33
  module LesliAudit
34
34
  class UserService < Lesli::ApplicationLesliService
35
35
 
36
- def requests
37
- current_user.account.audit.user_requests
38
- .joins(:user)
39
- .group(:email)
40
- .limit(5)
41
- .order("requests DESC")
42
- .select(
43
- :email,
44
- "count(lesli_audit_user_requests.id) resources",
45
- "sum(lesli_audit_user_requests.request_count) requests"
36
+ def users
37
+ users = current_user.account.users
38
+
39
+ now = Time.current
40
+ last_30_days = 30.days.ago
41
+
42
+ stats = users.pick(
43
+ # total users
44
+ Arel.sql("COUNT(*)"),
45
+
46
+ # total deactivated users
47
+ Arel.sql("SUM(CASE WHEN active = 0 AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
48
+
49
+ # total locked users
50
+ Arel.sql(
51
+ "SUM(CASE WHEN (
52
+ locked_at IS NOT NULL
53
+ OR locked_until > #{ActiveRecord::Base.connection.quote(Time.current)}
54
+ ) AND deleted_at IS NULL THEN 1 ELSE 0 END)"
55
+ ),
56
+
57
+ # total not confirmed users
58
+ Arel.sql("SUM(CASE WHEN confirmed_at IS NULL AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
59
+
60
+ # total deleted users
61
+ Arel.sql("SUM(CASE WHEN deleted_at IS NOT NULL THEN 1 ELSE 0 END)"),
62
+
63
+ # users that never signed in
64
+ Arel.sql("SUM(CASE WHEN last_sign_in_at IS NULL AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
65
+
66
+ # total active users for the last 30 days
67
+ Arel.sql("SUM(CASE WHEN last_sign_in_at >= #{ActiveRecord::Base.connection.quote(last_30_days)} AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
68
+
69
+ # users with failed logins
70
+ Arel.sql("SUM(CASE WHEN failed_attempts > 0 AND deleted_at IS NULL THEN 1 ELSE 0 END)"),
46
71
  )
72
+
73
+ summary = {
74
+ total_users: stats[0].to_i,
75
+ total_users_deactivated: stats[1].to_i,
76
+ total_users_locked: stats[2].to_i,
77
+ total_users_unconfirmed: stats[3].to_i,
78
+ total_users_deleted: stats[4].to_i,
79
+
80
+ total_users_never_signed_in: stats[5].to_i,
81
+ total_users_active_last_30_days: stats[6].to_i,
82
+ total_users_with_failed_attempts: stats[7].to_i
83
+ }
84
+
85
+ {
86
+ overview: [{
87
+ label: "Total users",
88
+ value: summary[:total_users],
89
+ percentage: 100,
90
+ color: "info"
91
+ }, {
92
+ label: "Deactivated users",
93
+ value: summary[:total_users_deactivated],
94
+ percentage: percentage(summary[:total_users_deactivated], summary[:total_users]),
95
+ color: "warning"
96
+ }, {
97
+ label: "Locked users",
98
+ value: summary[:total_users_locked],
99
+ percentage: percentage(summary[:total_users_locked], summary[:total_users]),
100
+ color: "danger"
101
+ }, {
102
+ label: "Unconfirmed users",
103
+ value: summary[:total_users_unconfirmed],
104
+ percentage: percentage(summary[:total_users_unconfirmed], summary[:total_users]),
105
+ color: "warning",
106
+ }, {
107
+ label: "Deleted users",
108
+ value: summary[:total_users_deleted],
109
+ percentage: percentage(summary[:total_users_deleted], summary[:total_users]),
110
+ color: "danger"
111
+ }],
112
+ attention_needed: [{
113
+ label: "Unconfirmed users",
114
+ value: summary[:total_users_unconfirmed],
115
+ color: "info",
116
+ icon: "user-add-line"
117
+ }, {
118
+ label: "Never signed in",
119
+ value: summary[:total_users_never_signed_in],
120
+ color: "warning",
121
+ icon: "time-line"
122
+ }, {
123
+ label: "Failed login attempts",
124
+ value: summary[:total_users_with_failed_attempts],
125
+ color: "danger",
126
+ icon: "shield-keyhole-line"
127
+ }, {
128
+ label: "Locked users",
129
+ value: summary[:total_users_locked],
130
+ color: "danger",
131
+ icon: "lock-line"
132
+ }]
133
+ }
47
134
  end
48
135
 
49
136
  def working_hours
@@ -72,8 +159,7 @@ module LesliAudit
72
159
  "sum(lesli_audit_user_requests.request_count) requests"
73
160
  )
74
161
 
75
- user_requests = user_requests
76
- .group(:user_id, group_by)
162
+ user_requests = user_requests.group(:user_id, group_by)
77
163
 
78
164
  user_requests.map do |request|
79
165
  request[:first_activity] = Date2.new(request[:first_activity]).time
@@ -118,5 +204,12 @@ module LesliAudit
118
204
 
119
205
  registrations
120
206
  end
207
+
208
+ private
209
+
210
+ def percentage(value, total)
211
+ return 0 if total.to_i.zero?
212
+ ((value.to_f / total) * 100).round
213
+ end
121
214
  end
122
215
  end
@@ -31,15 +31,22 @@ Building a better future, one line of code at a time.
31
31
  =end
32
32
 
33
33
  module LesliAudit
34
- class AnalyticService < Lesli::ApplicationLesliService
34
+ class VisitorService < Lesli::ApplicationLesliService
35
35
 
36
- LIMIT=5
36
+ def visits
37
+ requests = current_user.account.audit.account_requests
38
+
39
+ controllers, total_requests = requests.pick(
40
+ Arel.sql("COUNT(DISTINCT request_controller)"),
41
+ Arel.sql("SUM(request_count)")
42
+ )
43
+
44
+ {
45
+ controllers: controllers.to_i,
46
+ requests: total_requests.to_i
47
+ }
48
+ end
37
49
 
38
- # @overwrite
39
- # @return {Hash} Paginated list of the records
40
- # @param {query} Has of the formated queries/filters that will be applied to filter data
41
- # @description
42
- # @example
43
50
  def visitors
44
51
  #Rails.cache.fetch(cache_key_for_account(__method__), expires_in: 1.hour) do
45
52
  group = 'day'
@@ -70,7 +77,7 @@ module LesliAudit
70
77
 
71
78
  def requests
72
79
  requests = current_user.account.audit.account_requests
73
- .group("request_controller").limit(30)
80
+ .group("request_controller")
74
81
 
75
82
  requests = apply_filters(requests, query)
76
83
 
@@ -80,32 +87,122 @@ module LesliAudit
80
87
  ).as_json
81
88
  end
82
89
 
90
+ def os
91
+ device_counts = current_user.account.audit.account_devices
92
+ .group(:agent_platform)
93
+ .sum(:agent_count)
94
+
95
+ total = device_counts.values.sum
96
+
97
+ device_counts.map do |device, count|
98
+ {
99
+ device: device,
100
+ count: count,
101
+ icon: platform_icon(device),
102
+ percentage: total.zero? ? 0 : ((count.to_f / total) * 100).round()
103
+ }
104
+ end
105
+ end
106
+
83
107
  def devices
84
- #Rails.cache.fetch(cache_key_for_account(__method__), expires_in: 4.hour) do
85
- current_user.account.audit.account_devices
86
- .group(:agent_platform, :created_at)
87
- .select(
88
- 'created_at as xaxiskey',
89
- 'agent_platform as dataname',
90
- 'sum(agent_count) as yaxiskey'
91
- ).as_json
92
- #end
108
+ device_counts = current_user.account.audit.account_devices
109
+ .group(:agent_device)
110
+ .sum(:agent_count)
111
+
112
+ total = device_counts.values.sum
113
+
114
+ device_counts.map do |device, count|
115
+ {
116
+ device: device,
117
+ count: count,
118
+ icon: device_icon(device),
119
+ percentage: total.zero? ? 0 : ((count.to_f / total) * 100).round()
120
+ }
121
+ end
93
122
  end
94
123
 
95
124
  def browsers
125
+ browser_counts = current_user.account.audit.account_devices
126
+ .group(:agent_browser)
127
+ .sum(:agent_count)
128
+
129
+ total = browser_counts.values.sum
130
+
131
+ browser_counts.map do |browser, count|
132
+ {
133
+ browser: browser,
134
+ count: count,
135
+ icon: browser_icon(browser),
136
+ percentage: total.zero? ? 0 : ((count.to_f / total) * 100).round()
137
+ }
138
+ end
96
139
  #Rails.cache.fetch(cache_key_for_account(__method__), expires_in: 4.hour) do
97
- current_user.account.audit.account_devices
98
- .group(:agent_browser, :created_at)
99
- .select(
100
- 'created_at as xaxiskey',
101
- 'agent_browser as dataname',
102
- 'sum(agent_count) as yaxiskey'
103
- ).as_json
140
+ # current_user.account.audit.account_devices
141
+ # .group(:agent_browser, :created_at)
142
+ # .select(
143
+ # 'created_at as xaxiskey',
144
+ # 'agent_browser as dataname',
145
+ # 'sum(agent_count) as yaxiskey'
146
+ # ).as_json
104
147
  #end
105
148
  end
106
149
 
107
150
  private
108
151
 
152
+ LIMIT=15
153
+
154
+ DEFAULT_PLATFORM_ICON_CLASS = "computer-fill"
155
+ PLATFORM_ICON_CLASSES = {
156
+ /chrome os|cros/i => "chrome-fill",
157
+ /ios|ipad|iphone|mac/i => "apple-fill",
158
+ /windows/i => "windows-fill",
159
+ /ubuntu/i => "ubuntu-fill",
160
+ /debian|fedora|linux/i => "linux-fill",
161
+ /android/i => "android-fill"
162
+ }.freeze
163
+
164
+ DEFAULT_BROWSER_ICON_CLASS = "global-fill"
165
+ BROWSER_ICON_CLASSES = {
166
+ /chrome|chromium/i => "chrome-fill",
167
+ /firefox/i => "firefox-fill",
168
+ /safari/i => "safari-fill",
169
+ /edge/i => "edge-fill",
170
+ /opera|opr/i => "opera-fill",
171
+ /internet explorer|msie/i => "ie-fill",
172
+ /brave/i => "brave-fill"
173
+ }.freeze
174
+
175
+ DEVICE_ICON_CLASSES = {
176
+ "desktop" => "mac-line",
177
+ "tablet" => "tablet-line",
178
+ "smartphone" => "smartphone-line",
179
+ "unknown" => "file-unknow-line"
180
+ }.freeze
181
+
182
+ def platform_icon(platform)
183
+ return DEFAULT_PLATFORM_ICON_CLASS if platform.blank?
184
+
185
+ match = PLATFORM_ICON_CLASSES.find do |pattern, _icon_class|
186
+ platform.match?(pattern)
187
+ end
188
+
189
+ match ? match.last : DEFAULT_PLATFORM_ICON_CLASS
190
+ end
191
+
192
+ def browser_icon(browser)
193
+ return DEFAULT_BROWSER_ICON_CLASS if browser.blank?
194
+
195
+ match = BROWSER_ICON_CLASSES.find do |pattern, _icon_class|
196
+ browser.match?(pattern)
197
+ end
198
+
199
+ match ? match.last : DEFAULT_BROWSER_ICON_CLASS
200
+ end
201
+
202
+ def device_icon(device)
203
+ DEVICE_ICON_CLASSES.fetch(device.to_s, DEVICE_ICON_CLASSES["unknown"])
204
+ end
205
+
109
206
  def apply_filters requests, params
110
207
 
111
208
  return requests
@@ -33,6 +33,6 @@ Building a better future, one line of code at a time.
33
33
  %>
34
34
 
35
35
  <%= navigation_item(lesli_audit.users_path, 'Users', 'ri-team-line'); %>
36
- <%= navigation_item(lesli_audit.analytics_path, 'Analytics', 'ri-pie-chart-line'); %>
36
+ <%= navigation_item(lesli_audit.visitors_path, 'Visitors', 'ri-line-chart-fill'); %>
37
37
  <%= navigation_item(lesli_audit.requests_path, 'Requests', 'ri-arrow-left-right-fill'); %>
38
38
  <%= navigation_item(lesli_audit.logs_path, 'Logs', 'ri-history-line'); %>
@@ -1,6 +1,6 @@
1
1
  <%= render(LesliView::Layout::Container.new("audit-requests")) do %>
2
2
  <%= render(LesliView::Components::Header.new("Requests")) %>
3
- <%= render LesliView::Components::Toolbar.new() %>
3
+ <%= render(LesliView::Components::Toolbar.new()) %>
4
4
  <%= render(LesliView::Elements::Table.new(
5
5
  columns: [{
6
6
  label: "ID",
@@ -3,11 +3,41 @@
3
3
  <%= render(LesliView::Components::Header.new("Users")) %>
4
4
 
5
5
  <div class="columns">
6
+ <% @users[:overview].each do |user| %>
7
+ <div class="column">
8
+ <div class="box">
9
+ <h4><%= user[:label] %></h4>
10
+ <p class="is-size-3 has-text-<%= user[:color] %>"><%= user[:value] %></p>
11
+ </div>
12
+ </div>
13
+ <% end %>
14
+ </div>
15
+
16
+ <div class="columns">
6
17
  <div class="column">
7
- <%= render(LesliView::Charts::Bar.new(
8
- title:"Registered users",
9
- database_to_dataset: @registrations.as_json
10
- ))%>
18
+ <div class="box">
19
+ <%= render(LesliView::Elements::Table.new(columns: [])) do |table| %>
20
+ <% @users[:attention_needed].each do |user| %>
21
+ <% table.with_row(css_class:"is-white") do |row| %>
22
+ <% row.with_cell(css_class:"") do %>
23
+ <span class="icon has-text-<%= user[:color] %>">
24
+ <span class="is-size-4 ri-<%= user[:icon] %>"></span>
25
+ </span>
26
+ <% end %>
27
+ <% row.with_cell(css_class:"is-fullwidth") do %>
28
+ <%= user[:label] %>
29
+ <% end %>
30
+ <% row.with_cell(css_class:"has-text-right") do %>
31
+ <span class="is-size-6 has-text-weight-bold">
32
+ <%= user[:value] %>
33
+ </span>
34
+ <% end %>
35
+ <% end %>
36
+ <% end %>
37
+ <% end %>
38
+ </div>
39
+ </div>
40
+ <div class="column is-three-fifths">
11
41
  </div>
12
42
  </div>
13
43
 
@@ -40,4 +70,6 @@
40
70
  )) %>
41
71
  </div>
42
72
  </div>
73
+
74
+ <br>
43
75
  <% end %>
@@ -0,0 +1,118 @@
1
+
2
+ <% @visitors = @visitors.reverse %>
3
+ <% top_days = (@visitors.last(3)) %>
4
+
5
+ <%= render(LesliView::Layout::Container.new("audit-analytics")) do %>
6
+ <%= render(LesliView::Components::Header.new("Analytics")) %>
7
+
8
+ <div class="columns">
9
+ <div class="column">
10
+ <div class="box">
11
+ <h4>Controllers</h4>
12
+ <p class="is-size-3"><%= @visits[:controllers] %></p>
13
+ </div>
14
+ </div>
15
+ <div class="column">
16
+ <div class="box">
17
+ <h4>Requests</h4>
18
+ <p class="is-size-3"><%= @visits[:requests] %></p>
19
+ </div>
20
+ </div>
21
+ <% top_days.each do |day| %>
22
+ <div class="column">
23
+ <div class="box">
24
+ <h4><%= day['xaxiskey'] %></h4>
25
+ <p class="is-size-3"><%= day['yaxiskey'] %></p>
26
+ </div>
27
+ </div>
28
+ <% end %>
29
+ </div>
30
+
31
+ <div class="box">
32
+ <%= render(LesliView::Charts::Line.new(
33
+ title:"Visitors",
34
+ database_to_dataset: @visitors
35
+ ))%>
36
+ </div>
37
+
38
+ <div class="columns">
39
+ <div class="column">
40
+ <%= render(LesliView::Elements::Table.new(columns: [{label:'Browsers', align:'left'},{label:'Visitors', align:'center'}])) do |table| %>
41
+ <% @browsers.each do |browser| %>
42
+ <% table.with_row(css_class:"") do |row| %>
43
+ <% row.with_cell(css_class:"p-0") do %>
44
+ <p style="padding:8px;background: linear-gradient(to right, #E5EAF0 <%= browser[:percentage] %>%, transparent 0%);">
45
+ <span class="icon-text">
46
+ <span class="icon">
47
+ <span class="ri-<%= browser[:icon] %>"></span>
48
+ </span>
49
+ <span><%= browser[:browser] %></span>
50
+ </span>
51
+ </p>
52
+ <% end %>
53
+ <% row.with_cell(css_class:"has-text-centered") do %>
54
+ <%= browser[:count] %>
55
+ <% end %>
56
+ <% end %>
57
+ <% end %>
58
+ <% end %>
59
+ </div>
60
+ <div class="column">
61
+ <%= render(LesliView::Elements::Table.new(columns: [{label:'OS', align:'left'},{label:'Visitors', align:'center'}])) do |table| %>
62
+ <% @os.each do |device| %>
63
+ <% table.with_row(css_class:"") do |row| %>
64
+ <% row.with_cell(css_class:"p-0") do %>
65
+ <p style="padding:8px;background: linear-gradient(to right, #E5EAF0 <%= device[:percentage] %>%, transparent 0%);">
66
+ <span class="icon-text">
67
+ <span class="icon">
68
+ <span class="ri-<%= device[:icon].downcase %>"></span>
69
+ </span>
70
+ <span><%= device[:device] %></span>
71
+ </span>
72
+ </p>
73
+ <% end %>
74
+ <% row.with_cell(css_class:"has-text-centered") do %>
75
+ <%= device[:count] %>
76
+ <% end %>
77
+ <% end %>
78
+ <% end %>
79
+ <% end %>
80
+ </div>
81
+ <div class="column">
82
+ <%= render(LesliView::Elements::Table.new(columns: [{label:'Device', align:'left'},{label:'Visitors', align:'center'}])) do |table| %>
83
+ <% @devices.each do |device| %>
84
+ <% table.with_row(css_class:"") do |row| %>
85
+ <% row.with_cell(css_class:"p-0") do %>
86
+ <p style="padding:8px;background: linear-gradient(to right, #E5EAF0 <%= device[:percentage] %>%, transparent 0%);">
87
+ <span class="icon-text">
88
+ <span class="icon">
89
+ <span class="ri-<%= device[:icon] %>"></span>
90
+ </span>
91
+ <span><%= device[:device] %></span>
92
+ </span>
93
+ </p>
94
+ <% end %>
95
+ <% row.with_cell(css_class:"has-text-centered") do %>
96
+ <%= device[:count] %>
97
+ <% end %>
98
+ <% end %>
99
+ <% end %>
100
+ <% end %>
101
+ </div>
102
+ </div>
103
+
104
+ <div class="columns">
105
+ <div class="column">
106
+ <%= render(LesliView::Elements::Table.new(
107
+ columns: [{
108
+ label: "Top controllers",
109
+ field: "request_controller"
110
+ },{
111
+ label:"Requests",
112
+ field:"requests"
113
+ }],
114
+ records: @requests
115
+ )) %>
116
+ </div>
117
+ </div>
118
+ <% end %>
data/config/routes.rb CHANGED
@@ -47,7 +47,7 @@ LesliAudit::Engine.routes.draw do
47
47
  # Trends by date
48
48
  # Most active users
49
49
  # Most active controllers
50
- resources :analytics, only: [:index]
50
+ resources :visitors, only: [:index]
51
51
 
52
52
  resources :logs, only: [:index, :show]
53
53
 
@@ -35,11 +35,12 @@ class CreateLesliAuditAccountDevices < ActiveRecord::Migration[8.1]
35
35
  create_table :lesli_audit_account_devices do |t|
36
36
  t.string :agent_platform
37
37
  t.string :agent_browser
38
+ t.string :agent_device
38
39
  t.integer :agent_count
39
40
  t.date :created_at
40
41
  end
41
42
 
42
43
  add_reference(:lesli_audit_account_devices, :account, foreign_key: { to_table: :lesli_audit_accounts })
43
- add_index(:lesli_audit_account_devices, %i[agent_platform agent_browser created_at account_id], unique: true, name: "lesli_audit_devices_index")
44
+ add_index(:lesli_audit_account_devices, %i[agent_platform agent_browser agent_device created_at account_id], unique: true, name: "lesli_audit_devices_index")
44
45
  end
45
46
  end