lookout-ahoy 0.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 (223) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +99 -0
  4. data/Rakefile +24 -0
  5. data/app/assets/images/lookout/apple-touch-icon.png +0 -0
  6. data/app/assets/images/lookout/favicon-16x16.png +0 -0
  7. data/app/assets/images/lookout/favicon-32x32.png +0 -0
  8. data/app/assets/images/lookout/logo.png +0 -0
  9. data/app/assets/images/lookout/safari-pinned-tab.png +0 -0
  10. data/app/assets/images/lookout/safari-pinned-tab.svg +199 -0
  11. data/app/assets/javascript/lookout/application.js +2 -0
  12. data/app/assets/javascript/lookout/controllers/application.js +9 -0
  13. data/app/assets/javascript/lookout/controllers/application_controller.js +33 -0
  14. data/app/assets/javascript/lookout/controllers/combobox_controller.js +371 -0
  15. data/app/assets/javascript/lookout/controllers/details_modal_controller.js +18 -0
  16. data/app/assets/javascript/lookout/controllers/dropdown_label_controller.js +39 -0
  17. data/app/assets/javascript/lookout/controllers/filter/item_controller.js +12 -0
  18. data/app/assets/javascript/lookout/controllers/filter_form_controller.js +13 -0
  19. data/app/assets/javascript/lookout/controllers/filter_modal_controller.js +45 -0
  20. data/app/assets/javascript/lookout/controllers/frame_link_controller.js +20 -0
  21. data/app/assets/javascript/lookout/controllers/funnel_chart_controller.js +159 -0
  22. data/app/assets/javascript/lookout/controllers/index.js +4 -0
  23. data/app/assets/javascript/lookout/controllers/interval_controller.js +15 -0
  24. data/app/assets/javascript/lookout/controllers/line_chart_controller.js +251 -0
  25. data/app/assets/javascript/lookout/controllers/predicate_select_controller.js +10 -0
  26. data/app/assets/javascript/lookout/controllers/properties_controller.js +8 -0
  27. data/app/assets/javascript/lookout/controllers/property_filter_controller.js +45 -0
  28. data/app/assets/javascript/lookout/controllers/realtime_controller.js +30 -0
  29. data/app/assets/javascript/lookout/controllers/sparkline_controller.js +64 -0
  30. data/app/assets/javascript/lookout/controllers/tile_controller.js +33 -0
  31. data/app/assets/javascript/lookout/controllers/toggle_controller.js +17 -0
  32. data/app/assets/javascript/lookout/helpers/chart_utils.js +156 -0
  33. data/app/assets/javascript/lookout/helpers/countries.js +2261 -0
  34. data/app/assets/javascript/lookout/helpers/number_formatters.js +55 -0
  35. data/app/assets/manifest/lookout/manifest.js +2 -0
  36. data/app/components/lookout/combobox_component.html.erb +33 -0
  37. data/app/components/lookout/combobox_component.rb +13 -0
  38. data/app/components/lookout/comparison_link_component.html.erb +17 -0
  39. data/app/components/lookout/comparison_link_component.rb +44 -0
  40. data/app/components/lookout/dropdown_button_component.html.erb +16 -0
  41. data/app/components/lookout/dropdown_button_component.rb +14 -0
  42. data/app/components/lookout/dropdown_link_component.html.erb +17 -0
  43. data/app/components/lookout/dropdown_link_component.rb +19 -0
  44. data/app/components/lookout/filter/dropdown_component.html.erb +50 -0
  45. data/app/components/lookout/filter/dropdown_component.rb +51 -0
  46. data/app/components/lookout/filter/modal_component.html.erb +16 -0
  47. data/app/components/lookout/filter/modal_component.rb +13 -0
  48. data/app/components/lookout/filter/select_component.html.erb +25 -0
  49. data/app/components/lookout/filter/select_component.rb +64 -0
  50. data/app/components/lookout/filter/tag_component.html.erb +13 -0
  51. data/app/components/lookout/filter/tag_component.rb +14 -0
  52. data/app/components/lookout/filter/tag_container_component.html.erb +4 -0
  53. data/app/components/lookout/filter/tag_container_component.rb +6 -0
  54. data/app/components/lookout/previous_next_component.html.erb +8 -0
  55. data/app/components/lookout/previous_next_component.rb +11 -0
  56. data/app/components/lookout/stats/comparable_container_component.html.erb +25 -0
  57. data/app/components/lookout/stats/comparable_container_component.rb +103 -0
  58. data/app/components/lookout/stats/container_component.html.erb +23 -0
  59. data/app/components/lookout/stats/container_component.rb +28 -0
  60. data/app/components/lookout/sticky_nav_component.html.erb +32 -0
  61. data/app/components/lookout/sticky_nav_component.rb +24 -0
  62. data/app/components/lookout/table_component.html.erb +16 -0
  63. data/app/components/lookout/table_component.rb +48 -0
  64. data/app/components/lookout/tables/devices_table_component.rb +11 -0
  65. data/app/components/lookout/tables/dynamic_table.rb +13 -0
  66. data/app/components/lookout/tables/dynamic_table_component.rb +207 -0
  67. data/app/components/lookout/tables/goals_table_component.rb +17 -0
  68. data/app/components/lookout/tables/header_component.html.erb +6 -0
  69. data/app/components/lookout/tables/header_component.rb +18 -0
  70. data/app/components/lookout/tables/headers/header_component.html.erb +5 -0
  71. data/app/components/lookout/tables/headers/header_component.rb +16 -0
  72. data/app/components/lookout/tables/properties_table_component.rb +27 -0
  73. data/app/components/lookout/tables/row_component.html.erb +4 -0
  74. data/app/components/lookout/tables/rows/row_component.html.erb +6 -0
  75. data/app/components/lookout/tables/rows/row_component.rb +40 -0
  76. data/app/components/lookout/tile_component.html.erb +24 -0
  77. data/app/components/lookout/tile_component.rb +24 -0
  78. data/app/components/lookout/tooltip_component.html.erb +3 -0
  79. data/app/components/lookout/tooltip_component.rb +18 -0
  80. data/app/controllers/lookout/application_controller.rb +83 -0
  81. data/app/controllers/lookout/campaigns_controller.rb +19 -0
  82. data/app/controllers/lookout/devices_controller.rb +20 -0
  83. data/app/controllers/lookout/entry_pages_controller.rb +19 -0
  84. data/app/controllers/lookout/exit_pages_controller.rb +19 -0
  85. data/app/controllers/lookout/exports_controller.rb +14 -0
  86. data/app/controllers/lookout/filters/base_controller.rb +15 -0
  87. data/app/controllers/lookout/filters/goals_controller.rb +9 -0
  88. data/app/controllers/lookout/filters/locations_controller.rb +11 -0
  89. data/app/controllers/lookout/filters/operating_systems/names_controller.rb +13 -0
  90. data/app/controllers/lookout/filters/operating_systems/versions_controller.rb +13 -0
  91. data/app/controllers/lookout/filters/pages/actions_controller.rb +13 -0
  92. data/app/controllers/lookout/filters/pages/entry_pages_controller.rb +14 -0
  93. data/app/controllers/lookout/filters/pages/exit_pages_controller.rb +15 -0
  94. data/app/controllers/lookout/filters/properties/names_controller.rb +29 -0
  95. data/app/controllers/lookout/filters/properties/values_controller.rb +15 -0
  96. data/app/controllers/lookout/filters/screens_controller.rb +11 -0
  97. data/app/controllers/lookout/filters/sources_controller.rb +11 -0
  98. data/app/controllers/lookout/filters/utms_controller.rb +10 -0
  99. data/app/controllers/lookout/funnels_controller.rb +8 -0
  100. data/app/controllers/lookout/goals_controller.rb +7 -0
  101. data/app/controllers/lookout/locations/cities_controller.rb +22 -0
  102. data/app/controllers/lookout/locations/countries_controller.rb +22 -0
  103. data/app/controllers/lookout/locations/maps_controller.rb +24 -0
  104. data/app/controllers/lookout/locations/regions_controller.rb +22 -0
  105. data/app/controllers/lookout/properties_controller.rb +73 -0
  106. data/app/controllers/lookout/realtimes_controller.rb +7 -0
  107. data/app/controllers/lookout/roots_controller.rb +6 -0
  108. data/app/controllers/lookout/sources_controller.rb +21 -0
  109. data/app/controllers/lookout/stats/base_controller.rb +148 -0
  110. data/app/controllers/lookout/stats/bounce_rates_controller.rb +12 -0
  111. data/app/controllers/lookout/stats/total_pageviews_controller.rb +10 -0
  112. data/app/controllers/lookout/stats/total_visits_controller.rb +10 -0
  113. data/app/controllers/lookout/stats/unique_visitors_controller.rb +11 -0
  114. data/app/controllers/lookout/stats/views_per_visits_controller.rb +11 -0
  115. data/app/controllers/lookout/stats/visit_durations_controller.rb +10 -0
  116. data/app/controllers/lookout/stats_controller.rb +7 -0
  117. data/app/controllers/lookout/top_pages_controller.rb +20 -0
  118. data/app/decorators/lookout/application_decorator.rb +58 -0
  119. data/app/decorators/lookout/campaign_decorator.rb +27 -0
  120. data/app/decorators/lookout/city_decorator.rb +24 -0
  121. data/app/decorators/lookout/country_decorator.rb +38 -0
  122. data/app/decorators/lookout/device_decorator.rb +27 -0
  123. data/app/decorators/lookout/entry_page_decorator.rb +7 -0
  124. data/app/decorators/lookout/exit_page_decorator.rb +7 -0
  125. data/app/decorators/lookout/page_decorator.rb +27 -0
  126. data/app/decorators/lookout/region_decorator.rb +28 -0
  127. data/app/decorators/lookout/source_decorator.rb +27 -0
  128. data/app/decorators/lookout/top_page_decorator.rb +7 -0
  129. data/app/helpers/lookout/application_helper.rb +124 -0
  130. data/app/models/concerns/lookout/compare_mode.rb +19 -0
  131. data/app/models/concerns/lookout/limitable.rb +17 -0
  132. data/app/models/concerns/lookout/range_options.rb +8 -0
  133. data/app/models/lookout/comparison_mode.rb +72 -0
  134. data/app/models/lookout/export.rb +48 -0
  135. data/app/models/lookout/filter_parser.rb +82 -0
  136. data/app/models/lookout/range_from_params.rb +78 -0
  137. data/app/models/lookout/rangeable.rb +7 -0
  138. data/app/models/lookout/widget.rb +15 -0
  139. data/app/presenters/lookout/dashboard_presenter.rb +53 -0
  140. data/app/presenters/lookout/funnel_presenter.rb +75 -0
  141. data/app/presenters/lookout/goals_presenter.rb +72 -0
  142. data/app/queries/concerns/lookout/comparable_queries.rb +25 -0
  143. data/app/queries/concerns/lookout/comparable_query.rb +152 -0
  144. data/app/queries/concerns/lookout/lazy_comparable_query.rb +42 -0
  145. data/app/queries/lookout/application_query.rb +186 -0
  146. data/app/queries/lookout/campaign_query.rb +14 -0
  147. data/app/queries/lookout/city_query.rb +14 -0
  148. data/app/queries/lookout/country_query.rb +10 -0
  149. data/app/queries/lookout/device_query.rb +10 -0
  150. data/app/queries/lookout/entry_pages_query.rb +18 -0
  151. data/app/queries/lookout/event_query.rb +42 -0
  152. data/app/queries/lookout/exit_pages_query.rb +19 -0
  153. data/app/queries/lookout/region_query.rb +14 -0
  154. data/app/queries/lookout/source_query.rb +11 -0
  155. data/app/queries/lookout/stats/average_views_per_visit_query.rb +20 -0
  156. data/app/queries/lookout/stats/average_visit_duration_query.rb +34 -0
  157. data/app/queries/lookout/stats/base_query.rb +18 -0
  158. data/app/queries/lookout/stats/bounce_rates_query.rb +33 -0
  159. data/app/queries/lookout/stats/total_pageviews_query.rb +9 -0
  160. data/app/queries/lookout/stats/total_visitors_query.rb +9 -0
  161. data/app/queries/lookout/stats/unique_visitors_query.rb +9 -0
  162. data/app/queries/lookout/stats/views_per_visit_query.rb +17 -0
  163. data/app/queries/lookout/stats/visit_duration_query.rb +19 -0
  164. data/app/queries/lookout/top_page_query.rb +13 -0
  165. data/app/queries/lookout/visit_query.rb +42 -0
  166. data/app/views/lookout/campaigns/index.html+details.erb +4 -0
  167. data/app/views/lookout/campaigns/index.html.erb +3 -0
  168. data/app/views/lookout/devices/_table.html.erb +2 -0
  169. data/app/views/lookout/devices/index.html+details.erb +4 -0
  170. data/app/views/lookout/devices/index.html.erb +3 -0
  171. data/app/views/lookout/entry_pages/index.html+details.erb +4 -0
  172. data/app/views/lookout/entry_pages/index.html.erb +3 -0
  173. data/app/views/lookout/exit_pages/index.html+details.erb +4 -0
  174. data/app/views/lookout/exit_pages/index.html.erb +3 -0
  175. data/app/views/lookout/funnels/index.html.erb +7 -0
  176. data/app/views/lookout/funnels/show.html.erb +15 -0
  177. data/app/views/lookout/goals/index.html.erb +4 -0
  178. data/app/views/lookout/layouts/application.html.erb +144 -0
  179. data/app/views/lookout/layouts/shared/_tile_loader.html.erb +5 -0
  180. data/app/views/lookout/layouts/shared/_widget_disabled.html+details.erb +3 -0
  181. data/app/views/lookout/layouts/shared/_widget_disabled.html.erb +3 -0
  182. data/app/views/lookout/locations/cities/index.html+details.erb +4 -0
  183. data/app/views/lookout/locations/cities/index.html.erb +3 -0
  184. data/app/views/lookout/locations/countries/index.html+details.erb +5 -0
  185. data/app/views/lookout/locations/countries/index.html.erb +3 -0
  186. data/app/views/lookout/locations/maps/_simple_map.html.erb +26 -0
  187. data/app/views/lookout/locations/maps/show.html.erb +106 -0
  188. data/app/views/lookout/locations/regions/index.html+details.erb +4 -0
  189. data/app/views/lookout/locations/regions/index.html.erb +3 -0
  190. data/app/views/lookout/properties/_form.html.erb +6 -0
  191. data/app/views/lookout/properties/index.html.erb +3 -0
  192. data/app/views/lookout/properties/show.html.erb +6 -0
  193. data/app/views/lookout/realtimes/show.html.erb +9 -0
  194. data/app/views/lookout/roots/_filters.html.erb +80 -0
  195. data/app/views/lookout/roots/show.html.erb +191 -0
  196. data/app/views/lookout/sources/index.html+details.erb +4 -0
  197. data/app/views/lookout/sources/index.html.erb +3 -0
  198. data/app/views/lookout/stats/base/index.html.erb +40 -0
  199. data/app/views/lookout/stats/show.html.erb +15 -0
  200. data/app/views/lookout/top_pages/index.html+details.erb +4 -0
  201. data/app/views/lookout/top_pages/index.html.erb +3 -0
  202. data/config/routes.rb +69 -0
  203. data/lib/generators/lookout/install_generator.rb +31 -0
  204. data/lib/generators/lookout/migration_generator.rb +21 -0
  205. data/lib/generators/lookout/templates/config.rb.tt +185 -0
  206. data/lib/generators/lookout/templates/migration.rb.tt +7 -0
  207. data/lib/lookout/active_record.rb +108 -0
  208. data/lib/lookout/ahoy/event_methods.rb +75 -0
  209. data/lib/lookout/ahoy/visit_methods.rb +24 -0
  210. data/lib/lookout/configuration.rb +58 -0
  211. data/lib/lookout/database_adapter.rb +168 -0
  212. data/lib/lookout/engine.rb +47 -0
  213. data/lib/lookout/filter_configuration/filter.rb +16 -0
  214. data/lib/lookout/filter_configuration/filter_collection.rb +48 -0
  215. data/lib/lookout/filters_configuration.rb +77 -0
  216. data/lib/lookout/funnels.rb +44 -0
  217. data/lib/lookout/goals.rb +51 -0
  218. data/lib/lookout/period_collection.rb +115 -0
  219. data/lib/lookout/predicate_label.rb +7 -0
  220. data/lib/lookout/railtie.rb +9 -0
  221. data/lib/lookout/version.rb +3 -0
  222. data/lib/lookout.rb +78 -0
  223. metadata +673 -0
@@ -0,0 +1,55 @@
1
+ const THOUSAND = 1000
2
+ const HUNDRED_THOUSAND = 100000
3
+ const MILLION = 1000000
4
+ const HUNDRED_MILLION = 100000000
5
+ const BILLION = 1000000000
6
+ const HUNDRED_BILLION = 100000000000
7
+ const TRILLION = 1000000000000
8
+
9
+ export function numberFormatter(num) {
10
+ if (num >= THOUSAND && num < MILLION) {
11
+ const thousands = num / THOUSAND
12
+ if (thousands === Math.floor(thousands) || num >= HUNDRED_THOUSAND) {
13
+ return Math.floor(thousands) + 'k'
14
+ } else {
15
+ return (Math.floor(thousands * 10) / 10) + 'k'
16
+ }
17
+ } else if (num >= MILLION && num < BILLION) {
18
+ const millions = num / MILLION
19
+ if (millions === Math.floor(millions) || num >= HUNDRED_MILLION) {
20
+ return Math.floor(millions) + 'M'
21
+ } else {
22
+ return (Math.floor(millions * 10) / 10) + 'M'
23
+ }
24
+ } else if (num >= BILLION && num < TRILLION) {
25
+ const billions = num / BILLION
26
+ if (billions === Math.floor(billions) || num >= HUNDRED_BILLION) {
27
+ return Math.floor(billions) + 'B'
28
+ } else {
29
+ return (Math.floor(billions * 10) / 10) + 'B'
30
+ }
31
+ } else {
32
+ return num
33
+ }
34
+ }
35
+
36
+ function pad(num, size) {
37
+ return ('000' + num).slice(size * -1);
38
+ }
39
+
40
+ export function durationFormatter(duration) {
41
+ const hours = Math.floor(duration / 60 / 60)
42
+ const minutes = Math.floor(duration / 60) % 60
43
+ const seconds = Math.floor(duration - (minutes * 60) - (hours * 60 * 60))
44
+ if (hours > 0) {
45
+ return `${hours}h ${minutes}m ${seconds}s`
46
+ } else if (minutes > 0) {
47
+ return `${minutes}m ${pad(seconds, 2)}s`
48
+ } else {
49
+ return `${seconds}s`
50
+ }
51
+ }
52
+
53
+ export function percentageFormatter(float) {
54
+ return Number(float/100).toLocaleString(undefined,{style: 'percent', minimumFractionDigits:2});
55
+ }
@@ -0,0 +1,2 @@
1
+ //= link_tree ../../javascript/lookout .js
2
+ //= link_tree ../../images/lookout
@@ -0,0 +1,33 @@
1
+ <div data-controller="combobox"
2
+ data-combobox-single-option-value="<%= !@multiple %>"
3
+ data-combobox-placeholder-value="Select an option..."
4
+ data-combobox-is-disabled-value="false"
5
+ data-combobox-url-value="<%= @url %>"
6
+ data-combobox-disabled-value="<%= @disabled %>"
7
+ data-combobox-query-value="q[<%= @column %>_i_cont]"
8
+ data-combobox-selected-value="<%= @value.map { |value| { text: value, value: value } }.to_json %>"
9
+ class=" w-full "
10
+ >
11
+ <div data-action="click->combobox#toggleOpen" data-combobox-target="box"
12
+ class="
13
+ bg-gray-900 ring-0 focus-within:ring-0 focus-within:ring-0 focus:ring-0 focus:outline-none w-full rounded-md py-2 text-sm
14
+ w-full"
15
+ data-combobox-box-open-class="border-secondary-500 ring-1 ring-secondary-500">
16
+ <select data-combobox-target="select" style="display:none;"
17
+ data-predicate-select-target="select"
18
+ name="<%= @name %>"
19
+ id="<%= @select_html[:id] || "filter_#{@name}" %>"
20
+ <% @select_html.each do |k,v| %><%=k %>="<%=v %>"<% end %>
21
+ ><% @value.each do |value| %><option value="<%= value %>" selected><%= value %></option><% end %></select>
22
+ <div data-combobox-target="selected" class="px-2" style="display:none;"></div>
23
+ <input data-combobox-target="input"
24
+ data-action="input->combobox#onInput"
25
+ class="input input-sm input-ghost w-full inline-block rounded-md focus:outline-none focus:ring-0 focus:bg-gray-900"
26
+ type="text"
27
+ placeholder="Select an option...">
28
+ </div>
29
+ <ul data-combobox-target="list"
30
+ class="z-50 absolute mt-1 max-h-60 w-full overflow-auto rounded-md py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm bg-gray-800 text-gray-350"
31
+ style="display: none;">
32
+ </ul>
33
+ </div>
@@ -0,0 +1,13 @@
1
+ module Lookout
2
+ class ComboboxComponent < ViewComponent::Base
3
+ def initialize(name:, multiple: false, disabled: false, column:, url:, value: [], select_html: {})
4
+ @name = name
5
+ @multiple = multiple
6
+ @column = column
7
+ @url = url
8
+ @value = Array(value)
9
+ @select_html = select_html
10
+ @disabled = disabled
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ <div class="dropdown dropdown-end" data-controller='dropdown-label'>
2
+ <label
3
+ tabindex="0"
4
+ class="cursor-pointer flex <%= classes %>"
5
+ data-action='click->dropdown-label#removeHidden'
6
+ >
7
+ <span data-dropdown-label-target="label"><%= title %></span>
8
+
9
+ </label>
10
+ <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" data-dropdown-label-target="close">
11
+ <% links.each do |link| %>
12
+ <li data-action="click->dropdown-label#setLabel">
13
+ <%= link %>
14
+ <li>
15
+ <% end %>
16
+ </ul>
17
+ </div>
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lookout::ComparisonLinkComponent < ViewComponent::Base
4
+ include ::Lookout::CompareMode
5
+ include ::Lookout::RangeOptions
6
+ include ::Lookout::Rangeable
7
+
8
+ renders_many :links
9
+ renders_one :header
10
+
11
+ attr_reader :title, :classes
12
+ def initialize(title: "", classes: "btn btn-sm btn-base-100 no-underline hover:bg-base-100")
13
+ @classes = classes
14
+ end
15
+
16
+ # cheating
17
+ def title
18
+ self.with_link_content(options_for_option)
19
+
20
+ comparison_mode.label
21
+ end
22
+
23
+ def render?
24
+ comparison_mode.enabled?
25
+ end
26
+
27
+ def options_for_option
28
+ [
29
+ (link_to "Custom period", "javascript:customComparisonModal.showModal()", class: selected(:custom)),
30
+ (link_to "Year-over-year", Lookout::Engine.routes.url_helpers.root_path(**helpers.search_params.merge(comparison: :year)), class: selected(:year)),
31
+ (link_to "Previous period", Lookout::Engine.routes.url_helpers.root_path(**helpers.search_params.merge(comparison: :previous)), class: selected(:previous, :true)),
32
+ (link_to "Disable Comparison", Lookout::Engine.routes.url_helpers.root_path(**helpers.search_params.merge(comparison: false))),
33
+
34
+ ].reverse.join.html_safe
35
+ end
36
+
37
+ private
38
+
39
+ def selected(*types)
40
+ return "font-bold" if comparison_mode.type.in?(types)
41
+
42
+ nil
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ <div class="dropdown dropdown-end">
2
+ <label
3
+ tabindex="0"
4
+ class="btn btn-ghost dark:hover:bg-neutral flex"
5
+ >
6
+ <%= header_icon %>
7
+ <span><%= title %></span>
8
+ </label>
9
+ <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box">
10
+ <% options.each do |option| %>
11
+ <li>
12
+ <%= option %>
13
+ <li>
14
+ <% end %>
15
+ </ul>
16
+ </div>
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lookout::DropdownButtonComponent < ViewComponent::Base
4
+ renders_many :options
5
+ renders_one :header_icon
6
+
7
+ def initialize(title:)
8
+ @title = title
9
+ end
10
+
11
+ private
12
+
13
+ attr_reader :title
14
+ end
@@ -0,0 +1,17 @@
1
+ <div class="dropdown dropdown-end" data-controller='dropdown-label'>
2
+ <label
3
+ tabindex="0"
4
+ class="cursor-pointer flex <%= classes %>"
5
+ data-action='click->dropdown-label#removeHidden'
6
+ >
7
+ <span data-dropdown-label-target="label"><%= title %></span>
8
+
9
+ </label>
10
+ <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52" data-dropdown-label-target="close">
11
+ <% options.each do |option| %>
12
+ <li data-action="click->dropdown-label#setLabel">
13
+ <%= option %>
14
+ <li>
15
+ <% end %>
16
+ </ul>
17
+ </div>
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lookout::DropdownLinkComponent < ViewComponent::Base
4
+ renders_many :options
5
+ renders_one :header
6
+
7
+ def initialize(title:, classes: nil)
8
+ @title = title
9
+ @classes = classes
10
+ end
11
+
12
+ def link_to(name, url, **options)
13
+ self.with_option_content view_context.link_to name, url, options
14
+ end
15
+
16
+ private
17
+
18
+ attr_reader :title, :classes
19
+ end
@@ -0,0 +1,50 @@
1
+ <div class="dropdown dropdown-end">
2
+ <label
3
+ tabindex="0"
4
+ class="btn btn-sm btn-ghost dark:hover:bg-neutral flex"
5
+ >
6
+ <%= header_icon %>
7
+ <span><%= title %></span>
8
+ </label>
9
+
10
+ <ul class="w-72 dropdown-content z-[1] p-2 shadow bg-base-100 rounded-box" data-controller='toggle' data-toggle-enable-value="<%= advanced_filter_menu? %>">
11
+ <% if advanced_filter_menu? %>
12
+ <div id="advanced-filters" class="w-full text-sm leading-tight" >
13
+ <button class='w-full cursor-pointer block pl-4 pt-1 text-left hover:text-primary' data-action="click->toggle#trigger">+ Add filter</button>
14
+ <div class="divider my-1"></div>
15
+ <% filters.each do |_, filter| %>
16
+ <li class='flex flex-inline px-4 items-center' data-toggle-target='toggleable'>
17
+ <button title="Edit filter: <%= filter.title %>"
18
+ class="flex w-full justify-between link no-underline items-center group <% if filter.modal %>cursor-pointer<% else %>cursor-text<% end %> text-left"
19
+ onclick="<% if filter.modal %><%= filter.modal %>.showModal() <% end %>">
20
+ <span class="truncate w-48 ">
21
+ <%= filter.column.titleize %> <%= filter.predicate.titleize %>
22
+ <%= filter.values.to_sentence %>
23
+ </span>
24
+ <% if filter.modal %>
25
+ <span class="group-hover:text-primary hover:text-primary ">
26
+ <%= edit_icon %>
27
+ </span>
28
+ <% end %>
29
+ </button>
30
+ <a title="Remove filter: <%= filter.title %>"
31
+ class="hover:text-primary link no-underline pl-2" href="<%= filter.url %>">
32
+ <%= remove_icon %>
33
+ </a>
34
+ </li>
35
+ <div class="divider my-1" data-toggle-target='toggleable'></div>
36
+ <% end %>
37
+ <li data-toggle-target='toggleable'>
38
+ <a class="block mx-auto pl-4 pb-1 " href="<%= Lookout::Engine.app.url_helpers.root_path %>">Clear all filters</a>
39
+ </li>
40
+ </div>
41
+ <% end %>
42
+
43
+ <ul id="core-filters" class="menu <%= 'hidden' if advanced_filter_menu? %> pt-0" data-toggle-target='toggleable'>
44
+ <% Lookout.config.filters.each do |label, filter_group| %>
45
+ <li><button onClick="<%= filter_group.modal_name %>.showModal()" class='link no-underline' data-action="click->toggle#trigger"><%= label %></button></li>
46
+ <% end %>
47
+ <li><button onClick="customPropertyFilterModal.showModal()" class='link no-underline' data-action="click->toggle#trigger">Property</button></li>
48
+ </ul>
49
+ </ul>
50
+ </div>
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lookout::Filter::DropdownComponent < ViewComponent::Base
4
+ def initialize(filters:)
5
+ @filters = filters
6
+ end
7
+
8
+ private
9
+
10
+ attr_reader :filters
11
+
12
+ def header_icon
13
+ advanced_filter_menu? ? filters_icon : magnifier_icon
14
+ end
15
+
16
+ def title
17
+ advanced_filter_menu? ? "#{filters.size} Filters" : 'Filter'
18
+ end
19
+
20
+ def advanced_filter_menu?
21
+ filter_categories.count >= ::Lookout::FilterParser::FILTER_MENU_MAX_SIZE
22
+ end
23
+
24
+ def filter_categories
25
+ filters.values.map(&:values).flatten
26
+ end
27
+
28
+ def magnifier_icon
29
+ %Q(
30
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="-ml-1 mr-1 h-4 w-4 md:h-4 md:w-4"><path fill-rule="evenodd" d="M9 3.5a5.5 5.5 0 100 11 5.5 5.5 0 000-11zM2 9a7 7 0 1112.452 4.391l3.328 3.329a.75.75 0 11-1.06 1.06l-3.329-3.328A7 7 0 012 9z" clip-rule="evenodd"></path></svg>
31
+ ).html_safe
32
+ end
33
+
34
+ def filters_icon
35
+ %Q(
36
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="-ml-1 mr-1 h-4 w-4"><path d="M17 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM17 15.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM3.75 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5a.75.75 0 01.75-.75zM4.5 2.75a.75.75 0 00-1.5 0v5.5a.75.75 0 001.5 0v-5.5zM10 11a.75.75 0 01.75.75v5.5a.75.75 0 01-1.5 0v-5.5A.75.75 0 0110 11zM10.75 2.75a.75.75 0 00-1.5 0v1.5a.75.75 0 001.5 0v-1.5zM10 6a2 2 0 100 4 2 2 0 000-4zM3.75 10a2 2 0 100 4 2 2 0 000-4zM16.25 10a2 2 0 100 4 2 2 0 000-4z"></path></svg>
37
+ ).html_safe
38
+ end
39
+
40
+ def edit_icon
41
+ %Q(
42
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4 ml-1 cursor-pointer"><path d="M5.433 13.917l1.262-3.155A4 4 0 017.58 9.42l6.92-6.918a2.121 2.121 0 013 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 01-.65-.65z"></path><path d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0010 3H4.75A2.75 2.75 0 002 5.75v9.5A2.75 2.75 0 004.75 18h9.5A2.75 2.75 0 0017 15.25V10a.75.75 0 00-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5z"></path></svg>
43
+ ).html_safe
44
+ end
45
+
46
+ def remove_icon
47
+ %Q(
48
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4"><path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"></path></svg>
49
+ ).html_safe
50
+ end
51
+ end
@@ -0,0 +1,16 @@
1
+ <dialog id="<%= id %>" class="modal" data-controller="filter-modal">
2
+ <div class="modal-box h-5/6 max-w-5xl">
3
+ <h1 class="text-xl mb-4"><%= title %></h1>
4
+ <fieldset>
5
+ <%= modal_display %>
6
+ </fieldset>
7
+ <div class="mt-10">
8
+ <button class="btn btn-active btn-primary" type="submit">Apply</button>
9
+ <button class="btn btn-active btn-primary" type="reset">Reset</button>
10
+ </div>
11
+ </div>
12
+ <label class="modal-backdrop">
13
+ <button onclick="event.preventDefault(); <%=id %>.close();">Close</button>
14
+ </label>
15
+ </dialog>
16
+
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lookout::Filter::ModalComponent < ViewComponent::Base
4
+ renders_one :modal_display
5
+
6
+ def initialize(title: nil, id:)
7
+ @title = title
8
+ @id = id
9
+ end
10
+
11
+ private
12
+ attr_reader :title, :id
13
+ end
@@ -0,0 +1,25 @@
1
+ <div class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"><%= label %></div>
2
+ <fieldset class="grid grid-cols-11 mb-3" data-controller="predicate-select">
3
+ <div class="col-span-3 mr-2">
4
+ <div class="w-full">
5
+ <div class="relative inline-block text-left w-full">
6
+ <div class="w-full">
7
+ <% if @predicates.any? %>
8
+ <select id="<%= column %>_predicate" class='select border-0 select-primary bg-gray-900 ring-0 focus-within:ring-0 focus-within:ring-0 focus:ring-0 focus:outline-none inline-flex justify-between items-center w-full rounded-md ' data-action='change->predicate-select#handleChange'>
9
+ <% @predicates.each do |predicate| %>
10
+ <option value="<%= option_value(predicate) %>" <%= 'selected' if selected_predicate?(predicate) %>>
11
+ <%= predicate_label(predicate) %>
12
+ </option>
13
+ <% end %>
14
+ </select>
15
+ <% end %>
16
+ </div>
17
+ </div>
18
+ </div>
19
+ </div>
20
+ <div class="col-span-8">
21
+ <div class="relative w-full">
22
+ <%= render ::Lookout::ComboboxComponent.new(name: column_name_with_predicate, multiple: multiple, column: column, url: url, value: values) %>
23
+ </div>
24
+ </div>
25
+ </fieldset>
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lookout::Filter::SelectComponent < ViewComponent::Base
4
+ def initialize(label:, column:, url:, predicates:, form:, multiple: true, input_html: {})
5
+ @label = label
6
+ @column = column
7
+ @url = url
8
+ @predicates = predicates
9
+ @form = form
10
+ @multiple = multiple
11
+ @input_html_options = input_html
12
+ end
13
+
14
+ private
15
+
16
+ def selected_predicate?(predicate)
17
+ params.dig(:q, predicate_name(predicate)).present?
18
+ end
19
+
20
+ def option_value(predicate)
21
+ name = "q[#{predicate_name(predicate)}]"
22
+ name += "[]" if multiple
23
+ name
24
+ end
25
+
26
+ def predicate_name(predicate)
27
+ "#{@column}_#{predicate}"
28
+ end
29
+
30
+ def selected_predicate
31
+ @predicates.each do |predicate|
32
+ if params.dig(:q, predicate_name(predicate))
33
+ return predicate
34
+ end
35
+ end
36
+
37
+ nil
38
+ end
39
+
40
+ def column_name_with_predicate
41
+ if selected_predicate
42
+ option_value(selected_predicate)
43
+ else
44
+ option_value(@predicates.first)
45
+ end
46
+ end
47
+
48
+ def values
49
+ @predicates.each do |predicate|
50
+ option = params.dig(:q, predicate_name(predicate))
51
+ if option
52
+ return option
53
+ end
54
+ end
55
+
56
+ []
57
+ end
58
+
59
+ def predicate_label(predicate)
60
+ Lookout::PredicateLabel[predicate]
61
+ end
62
+
63
+ attr_reader :label, :column, :url, :predicates, :form, :multiple
64
+ end
@@ -0,0 +1,13 @@
1
+ <div class="bg-base-200 py-2 px-4 mx-2 flex items-center text-base-content" data-controller="filter--item" data-filter--item-modal-value="<%= tag_item.modal %>">
2
+ <span data-action='click->filter--item#openModal'>
3
+ <%= tag_item.column.titleize %> <%= tag_item.predicate.titleize %>
4
+ <span class="font-bold">
5
+ <%= tag_item.label %>
6
+ </span>
7
+ </span>
8
+ <a class="hover:text-primary" href="<%= tag_item.url %>">
9
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true" class="w-4 h-4">
10
+ <path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z"></path>
11
+ </svg>
12
+ </a>
13
+ </div>
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lookout::Filter::TagComponent < ViewComponent::Base
4
+ def initialize(tag_item:)
5
+ @tag_item = tag_item
6
+ end
7
+
8
+ private
9
+ attr_reader :tag_item
10
+
11
+ def modal
12
+ tag_item.modal
13
+ end
14
+ end
@@ -0,0 +1,4 @@
1
+ <% ::Lookout::FilterParser.parse(request).each do |_, filter| %>
2
+ <%= render Lookout::Filter::TagComponent.new(tag_item: filter) %>
3
+ <% end %>
4
+
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lookout::Filter::TagContainerComponent < ViewComponent::Base
4
+ def initialize
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ <div class="rounded shadow bg-white cursor-pointer dark:bg-gray-800 flex h-8">
2
+ <button class="flex items-center px-1 sm:px-2 border-r border-gray-300 rounded-l dark:border-gray-500 dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-900" type="button">
3
+ <svg class="feather h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
4
+ </button>
5
+ <button class="flex items-center px-1 sm:px-2 rounded-r dark:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-900" type="button">
6
+ <svg class="feather h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
7
+ </button>
8
+ </div>
@@ -0,0 +1,11 @@
1
+ module Lookout
2
+ class PreviousNextComponent < ViewComponent::Base
3
+ def initialize(range)
4
+ @range = range
5
+ end
6
+
7
+ def render?
8
+ false
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,25 @@
1
+ <a href="<%= @url %>" class="relative px-4 md:px-6 w-1/2 my-4 w-auto group cursor-pointer" data-controller="frame-link" data-turbo-frame="chart">
2
+ <div>
3
+ <h5 class="text-sm font-bold uppercase whitespace-nowrap flex w-content border-transparent tooltip tooltip-bottom "
4
+ data-active-links-target="link" data-tip="<%= tooltip %>">
5
+ <%= @label %>
6
+ <div class="<%= klass %> ms-2"><%= arrow %></div>
7
+ </h5>
8
+ <div class="md:block flex gap-4">
9
+ <div>
10
+ <span class="flex items-center justify-between whitespace-nowrap">
11
+ <p class="font-bold text-xl"><%= formatted(value.current) %></p>
12
+ </span>
13
+ <% if compare_mode? %>
14
+ <p class="text-xs"><%= range_string %></p>
15
+ <% end %>
16
+ </div>
17
+ <% if compare_mode? %>
18
+ <div>
19
+ <p class="font-bold text-xl text-gray-500"><%= formatted(value.compared_to) %></p>
20
+ <p class="text-xs text-gray-500"><%= compare_range_string %></p>
21
+ </div>
22
+ <% end %>
23
+ </div>
24
+ </div>
25
+ </a>
@@ -0,0 +1,103 @@
1
+ module Lookout
2
+ module Stats
3
+ class ComparableContainerComponent < ViewComponent::Base
4
+ include CompareMode
5
+ include Rangeable
6
+
7
+ def initialize(url, label, comparable, formatter = nil, selected = false, compare = false)
8
+ @url = url
9
+ @label = label
10
+ @comparable = comparable
11
+ @formatter = formatter
12
+ @selected = selected
13
+ @compare = compare
14
+ end
15
+
16
+ def compare?
17
+ @compare
18
+ end
19
+
20
+ def klass
21
+ if percentage.negative?
22
+ "text-red-400"
23
+ else
24
+ "text-green-400"
25
+ end
26
+ end
27
+
28
+ # 〰 ↓ ↑
29
+ def arrow
30
+ if percentage.negative?
31
+ "↓"
32
+ elsif percentage.positive?
33
+ "↑"
34
+ else
35
+ "〰"
36
+ end
37
+ end
38
+
39
+ def percentage
40
+ begin
41
+ diff = value.current - value.compared_to
42
+ if diff.zero?
43
+ return 0
44
+ end
45
+ (value.current / diff).round(2) * 100
46
+ rescue ZeroDivisionError
47
+ 0
48
+ end
49
+ end
50
+
51
+ def number_to_duration(duration)
52
+ seconds =
53
+ case duration
54
+ when nil
55
+ nil
56
+ when ActiveSupport::Duration
57
+ duration.in_seconds
58
+ else
59
+ duration.to_f
60
+ end
61
+
62
+ if seconds && seconds > 0
63
+ minutes = (seconds / 60).to_i
64
+ seconds = (seconds % 60).to_i
65
+ "#{minutes}m #{seconds}s"
66
+ else
67
+ "0m 0s"
68
+ end
69
+ end
70
+
71
+ def number_to_percentage(number, options = {})
72
+ precision = options.fetch(:precision, 2)
73
+ "#{number.round(precision)}%"
74
+ end
75
+
76
+ def compare_range_string
77
+ range_to_string(@comparable.compare_range)
78
+ end
79
+
80
+ def range_string
81
+ range_to_string(@comparable.range)
82
+ end
83
+
84
+ def value
85
+ @comparable.result
86
+ end
87
+
88
+ def tooltip
89
+ "#{formatted(value.current)} vs #{formatted(value.compared_to)} — #{number_to_percentage percentage} (#{arrow}) "
90
+ end
91
+
92
+ def formatted(value)
93
+ public_send(@formatter, value)
94
+ end
95
+
96
+ private
97
+
98
+ def range_to_string(range)
99
+ [range[0], range[1]].map { |item| item.strftime('%m %B') }.join(' - ')
100
+ end
101
+ end
102
+ end
103
+ end