ahoy_captain 0.8 → 0.9

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 (130) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -13
  3. data/app/assets/javascript/ahoy_captain/application.js +4 -4
  4. data/app/assets/javascript/ahoy_captain/controllers/active_links_controller.js +14 -0
  5. data/app/assets/javascript/ahoy_captain/controllers/application.js +5 -5
  6. data/app/assets/javascript/ahoy_captain/controllers/application_controller.js +9 -10
  7. data/app/assets/javascript/ahoy_captain/controllers/details_modal_controller.js +5 -5
  8. data/app/assets/javascript/ahoy_captain/controllers/dropdown_label_controller.js +2 -2
  9. data/app/assets/javascript/ahoy_captain/controllers/filter/item_controller.js +12 -0
  10. data/app/assets/javascript/ahoy_captain/controllers/filter_form_controller.js +13 -0
  11. data/app/assets/javascript/ahoy_captain/controllers/filter_tag_controller.js +11 -8
  12. data/app/assets/javascript/ahoy_captain/controllers/funnel_chart_controller.js +77 -107
  13. data/app/assets/javascript/ahoy_captain/controllers/index.js +4 -3
  14. data/app/assets/javascript/ahoy_captain/controllers/interval_controller.js +3 -3
  15. data/app/assets/javascript/ahoy_captain/controllers/line_chart_controller.js +37 -0
  16. data/app/assets/javascript/ahoy_captain/controllers/predicate_select_controller.js +10 -0
  17. data/app/assets/javascript/ahoy_captain/controllers/realtime_controller.js +9 -8
  18. data/app/assets/javascript/ahoy_captain/controllers/search_select_controller.js +65 -0
  19. data/app/assets/javascript/ahoy_captain/controllers/toggle_controller.js +17 -0
  20. data/app/components/ahoy_captain/dropdown_button_component.html.erb +5 -5
  21. data/app/components/ahoy_captain/dropdown_link_component.html.erb +5 -5
  22. data/app/components/ahoy_captain/filter/dropdown_component.html.erb +48 -0
  23. data/app/components/ahoy_captain/filter/dropdown_component.rb +51 -0
  24. data/app/components/ahoy_captain/filter/modal_component.html.erb +8 -7
  25. data/app/components/ahoy_captain/filter/select_component.html.erb +14 -12
  26. data/app/components/ahoy_captain/filter/select_component.rb +40 -9
  27. data/app/components/ahoy_captain/filter/tag_component.html.erb +8 -4
  28. data/app/components/ahoy_captain/filter/tag_component.rb +6 -30
  29. data/app/components/ahoy_captain/filter/tag_container_component.html.erb +2 -3
  30. data/app/components/ahoy_captain/filter/tag_container_component.rb +1 -8
  31. data/app/components/ahoy_captain/stats/container_component.html.erb +8 -0
  32. data/app/components/ahoy_captain/stats/container_component.rb +11 -0
  33. data/app/components/ahoy_captain/sticky_nav_component.html.erb +7 -19
  34. data/app/components/ahoy_captain/sticky_nav_component.rb +8 -0
  35. data/app/components/ahoy_captain/table_component.html.erb +4 -37
  36. data/app/components/ahoy_captain/table_component.rb +15 -4
  37. data/app/components/ahoy_captain/tables/headers/devices_header_component.html.erb +3 -0
  38. data/app/components/ahoy_captain/tables/headers/devices_header_component.rb +9 -0
  39. data/app/components/ahoy_captain/tables/headers/goals_header_component.html.erb +6 -0
  40. data/app/components/ahoy_captain/tables/headers/goals_header_component.rb +9 -0
  41. data/app/components/ahoy_captain/tables/headers/header_component.html.erb +5 -0
  42. data/app/components/ahoy_captain/tables/headers/header_component.rb +12 -0
  43. data/app/components/ahoy_captain/tables/rows/devices_row_component.html.erb +5 -0
  44. data/app/components/ahoy_captain/tables/rows/devices_row_component.rb +12 -0
  45. data/app/components/ahoy_captain/tables/rows/goals_row_component.html.erb +11 -0
  46. data/app/components/ahoy_captain/tables/rows/goals_row_component.rb +20 -0
  47. data/app/components/ahoy_captain/tables/rows/row_component.html.erb +6 -0
  48. data/app/components/ahoy_captain/tables/rows/row_component.rb +41 -0
  49. data/app/components/ahoy_captain/tile_component.html.erb +4 -3
  50. data/app/components/ahoy_captain/tile_component.rb +1 -1
  51. data/app/components/ahoy_captain/tooltip_component.html.erb +2 -2
  52. data/app/controllers/ahoy_captain/application_controller.rb +13 -14
  53. data/app/controllers/ahoy_captain/campaigns_controller.rb +2 -10
  54. data/app/controllers/ahoy_captain/cities_controller.rb +2 -6
  55. data/app/controllers/ahoy_captain/countries_controller.rb +2 -6
  56. data/app/controllers/ahoy_captain/devices_controller.rb +3 -6
  57. data/app/controllers/ahoy_captain/entry_pages_controller.rb +2 -4
  58. data/app/controllers/ahoy_captain/exit_pages_controller.rb +3 -4
  59. data/app/controllers/ahoy_captain/exports_controller.rb +15 -0
  60. data/app/controllers/ahoy_captain/filters/pages/entry_pages_controller.rb +2 -2
  61. data/app/controllers/ahoy_captain/filters/pages/exit_pages_controller.rb +1 -2
  62. data/app/controllers/ahoy_captain/filters/properties/names_controller.rb +11 -0
  63. data/app/controllers/ahoy_captain/filters/properties/values_controller.rb +15 -0
  64. data/app/controllers/ahoy_captain/filters/sources_controller.rb +1 -1
  65. data/app/controllers/ahoy_captain/regions_controller.rb +3 -7
  66. data/app/controllers/ahoy_captain/sources_controller.rb +2 -5
  67. data/app/controllers/ahoy_captain/stats/base_controller.rb +3 -3
  68. data/app/controllers/ahoy_captain/stats/bounce_rates_controller.rb +1 -0
  69. data/app/controllers/ahoy_captain/stats/total_pageviews_controller.rb +1 -0
  70. data/app/controllers/ahoy_captain/stats/total_visits_controller.rb +1 -0
  71. data/app/controllers/ahoy_captain/stats/unique_visitors_controller.rb +1 -0
  72. data/app/controllers/ahoy_captain/stats/views_per_visits_controller.rb +3 -2
  73. data/app/controllers/ahoy_captain/stats/visit_durations_controller.rb +1 -0
  74. data/app/controllers/ahoy_captain/top_pages_controller.rb +2 -8
  75. data/app/decorators/ahoy_captain/application_decorator.rb +27 -3
  76. data/app/decorators/ahoy_captain/campaign_decorator.rb +8 -0
  77. data/app/decorators/ahoy_captain/city_decorator.rb +12 -0
  78. data/app/decorators/ahoy_captain/country_decorator.rb +10 -0
  79. data/app/decorators/ahoy_captain/device_decorator.rb +13 -2
  80. data/app/decorators/ahoy_captain/page_decorator.rb +11 -0
  81. data/app/decorators/ahoy_captain/region_decorator.rb +16 -0
  82. data/app/decorators/ahoy_captain/source_decorator.rb +7 -0
  83. data/app/helpers/ahoy_captain/application_helper.rb +33 -0
  84. data/app/models/ahoy_captain/export.rb +48 -0
  85. data/app/models/ahoy_captain/filter_parser.rb +67 -0
  86. data/app/presenters/ahoy_captain/dashboard_presenter.rb +6 -1
  87. data/app/presenters/ahoy_captain/goals_presenter.rb +3 -2
  88. data/app/queries/ahoy_captain/application_query.rb +4 -3
  89. data/app/queries/ahoy_captain/campaign_query.rb +14 -0
  90. data/app/queries/ahoy_captain/city_query.rb +11 -0
  91. data/app/queries/ahoy_captain/country_query.rb +10 -0
  92. data/app/queries/ahoy_captain/device_query.rb +10 -0
  93. data/app/queries/ahoy_captain/entry_pages_query.rb +3 -2
  94. data/app/queries/ahoy_captain/event_query.rb +17 -15
  95. data/app/queries/ahoy_captain/exit_pages_query.rb +6 -4
  96. data/app/queries/ahoy_captain/region_query.rb +11 -0
  97. data/app/queries/ahoy_captain/source_query.rb +10 -0
  98. data/app/queries/ahoy_captain/top_page_query.rb +13 -0
  99. data/app/queries/ahoy_captain/visit_query.rb +1 -1
  100. data/app/views/ahoy_captain/devices/_table.html.erb +5 -0
  101. data/app/views/ahoy_captain/devices/index.html+details.erb +1 -1
  102. data/app/views/ahoy_captain/devices/index.html.erb +2 -2
  103. data/app/views/ahoy_captain/funnels/show.html.erb +5 -2
  104. data/app/views/ahoy_captain/goals/index.html.erb +3 -35
  105. data/app/views/ahoy_captain/layouts/application.html.erb +3 -3
  106. data/app/views/ahoy_captain/realtimes/show.html.erb +1 -1
  107. data/app/views/ahoy_captain/roots/_filters.html.erb +34 -0
  108. data/app/views/ahoy_captain/roots/show.html.erb +33 -95
  109. data/app/views/ahoy_captain/stats/base/index.html.erb +4 -7
  110. data/app/views/ahoy_captain/stats/show.html.erb +6 -51
  111. data/config/routes.rb +7 -0
  112. data/lib/ahoy_captain/ahoy/event_methods.rb +36 -73
  113. data/lib/ahoy_captain/ahoy/visit_methods.rb +1 -1
  114. data/lib/ahoy_captain/configuration.rb +13 -3
  115. data/lib/ahoy_captain/engine.rb +18 -0
  116. data/lib/ahoy_captain/filter_configuration/filter.rb +16 -0
  117. data/lib/ahoy_captain/filter_configuration/filter_collection.rb +48 -0
  118. data/lib/ahoy_captain/filters_configuration.rb +73 -0
  119. data/lib/ahoy_captain/goals.rb +10 -2
  120. data/lib/ahoy_captain/period_collection.rb +1 -1
  121. data/lib/ahoy_captain/predicate_label.rb +7 -0
  122. data/lib/ahoy_captain/version.rb +1 -1
  123. data/lib/ahoy_captain.rb +1 -0
  124. data/lib/generators/ahoy_captain/templates/config.rb.tt +25 -0
  125. metadata +56 -20
  126. data/app/assets/javascript/ahoy_captain/controllers/filter_controller.js +0 -145
  127. data/app/assets/javascript/ahoy_captain/controllers/link_controller.js +0 -43
  128. data/app/assets/javascript/ahoy_captain/controllers/navigation_controller.js +0 -25
  129. data/app/models/ahoy_captain/current.rb +0 -9
  130. data/app/models/ahoy_captain/url_helpers.rb +0 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42069eea6d1d5b65765678fa1d352cf462048c09ad5bbe69715f0f147f0a3856
4
- data.tar.gz: cd0fab7a2a262b89f5688374cb566b482f0a9909b5358db5b02c9d3480f4ed21
3
+ metadata.gz: c7334ddf143eb3d28e42766a6cc39f1acc5ee733679e5824399d668f349e1f68
4
+ data.tar.gz: 98735c61175fe289a4ffc73c6ac395c540c603d1a05ca95e1369eb5619081a99
5
5
  SHA512:
6
- metadata.gz: 464affe93d6d8ebfec8baf55f989a8402a6e265247259894c4f01a22b32047bc3cda4238c3e22ea06a7e6dae088353f23ed1a94cc65899ea24c7096b49a0d031
7
- data.tar.gz: 22b2352c2a7c9a21b8999292ee1bff64b87bc3306dd6ecb9f7e6f0c787306a3ec2132850c8954f15bac01e830f624a961bee868d7e3c8c5d3718d3b0b6946954
6
+ metadata.gz: 737b08e21d4ea18f634ff5c19e10d0f9bc21f73eba746c93cc1f74f6459ebb9f8d95d7ca7ee82236bcb163cfbf6642acb30a547cc710c3d5e63abcaa4d49c1f6
7
+ data.tar.gz: 00dd0823f703ce906e201dd227b33a4cc0606f6d67680c63c1a1c6231110557e18ccfbb81a6964b601ba30a81fd8bd72a618b3da407f40c7f91181ed3cc6472a
data/README.md CHANGED
@@ -1,17 +1,12 @@
1
- # AhoyCaptain
1
+ # <img src="logo.png" style="max-height:100px" /> AhoyCaptain
2
2
 
3
- <img src="logo.png" style="max-width:100px" />
4
3
 
5
4
  A full-featured, mountable analytics dashboard for your Rails app, shamelessly inspired by Plausible Analytics, powered by the Ahoy gem.
6
5
 
7
- <a href="https://github.com/joshmn/ahoy_captain/blob/main/ss.jpg"><img src="ss.jpg" style="max-width:300px" /></a>
6
+ <a href="https://github.com/joshmn/ahoy_captain/blob/main/ss.png"><img src="ss.png" style="max-width:300px" /></a>
8
7
  ## Notice
9
8
 
10
- While this is fine to use in production, it was only built against a PostgreSQL instance. Some of the queries are certainly broken.
11
-
12
- ## Some assumptions
13
-
14
- Some hardcoded stuff as of writing; this will be more fully-featured in due time.
9
+ Currently requires using PG and a JSONB column for your data.
15
10
 
16
11
  ## Installation
17
12
 
@@ -29,11 +24,24 @@ $ bundle add ahoy_captain
29
24
  $ rails g ahoy_captain:install
30
25
  ```
31
26
 
32
- ### 3. Star this repo
27
+ ### 3. Make sure your events are setup correctly
28
+
29
+ By default, AhoyCaptain assumes you're tracking `controller` and `action` in your `Ahoy::Event` properties, and a page view event is named `$view`.
30
+
31
+ For a quick sanity check:
32
+
33
+ ```ruby
34
+ AhoyCaptain.event.where(name: AhoyCaptain.config.event[:view_name]).count
35
+ AhoyCaptain.event.with_routes.count
36
+ ```
37
+
38
+ See the initializer `config/initializers/ahoy_captain.rb` for further customization.
39
+
40
+ ### 4. Star this repo
33
41
 
34
42
  No, seriously, I need all the internet clout I can get.
35
43
 
36
- ### 4. Analyze your nightmares
44
+ ### 5. Analyze your nightmares
37
45
 
38
46
  If you have a large dataset (> 1GB) you probably want some indexes. `rails g ahoy_captain:migration`
39
47
 
@@ -52,12 +60,12 @@ If you have a large dataset (> 1GB) you probably want some indexes. `rails g aho
52
60
  * Device type
53
61
  * OS
54
62
  * UTM tags
63
+ * Goal
64
+ * CSV exports
55
65
 
56
66
  ## Coming soon ™️
57
67
 
58
68
  * Date comparison
59
- * More filters
60
- * CSV exports
61
69
 
62
70
  ## Contributors
63
71
 
@@ -65,7 +73,7 @@ This was built during the Rails Hackathon in July 2023 with [afogel](https://git
65
73
 
66
74
  ## Contributions
67
75
 
68
- Please and thank you in advance!
76
+ Do your worst; please and thank you in advance! :)
69
77
 
70
78
  ## License
71
79
 
@@ -1,4 +1,4 @@
1
- import "@hotwired/turbo-rails"
2
- import "controllers"
3
- import "chartkick"
4
- import "Chart.bundle"
1
+ import '@hotwired/turbo-rails';
2
+ import 'controllers';
3
+ import 'chartkick';
4
+ import 'Chart.bundle';
@@ -0,0 +1,14 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static targets = ["link"]
5
+ connect() {
6
+ this.handleLinkClick = (event) => {
7
+ this.linkTargets.forEach(link => link.classList.remove('text-primary', 'font-semibold'))
8
+ event.target.classList.add('text-primary', 'font-semibold')
9
+ }
10
+ this.linkTargets.forEach(link => {
11
+ link.addEventListener('click', this.handleLinkClick)
12
+ })
13
+ }
14
+ }
@@ -1,9 +1,9 @@
1
- import { Application } from "@hotwired/stimulus"
1
+ import { Application } from '@hotwired/stimulus';
2
2
 
3
- const application = Application.start()
3
+ const application = Application.start();
4
4
 
5
5
  // Configure Stimulus development experience
6
- application.debug = false
7
- window.Stimulus = application
6
+ application.debug = false;
7
+ window.Stimulus = application;
8
8
 
9
- export { application }
9
+ export { application };
@@ -1,14 +1,13 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- connect() {
5
- if(new URLSearchParams(window.location.search).get("period") === 'realtime') {
6
- this.element.querySelectorAll('turbo-frame').forEach(frame => {
7
- setInterval(() => {
8
- frame.reload()
9
- }, 1000 * 30);
10
- })
11
- }
4
+ connect() {
5
+ if (new URLSearchParams(window.location.search).get('period') === 'realtime') {
6
+ this.element.querySelectorAll('turbo-frame').forEach((frame) => {
7
+ setInterval(() => {
8
+ frame.reload();
9
+ }, 1000 * 30);
10
+ });
12
11
  }
13
-
12
+ }
14
13
  }
@@ -1,15 +1,15 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
4
  static values = {
5
- target: String
6
- }
7
-
5
+ target: String,
6
+ };
7
+
8
8
  connect() {
9
9
  this.modal = document.querySelector('#detailsModal');
10
10
  this.turboFrame = document.querySelector('#detailsModal turbo-frame');
11
11
  }
12
-
12
+
13
13
  openModal(e) {
14
14
  e.preventDefault();
15
15
  this.modal.showModal();
@@ -1,7 +1,7 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["label", "close"]
4
+ static targets = ['label', 'close'];
5
5
 
6
6
  setLabel(event) {
7
7
  this.labelTarget.innerText = event.target.innerText;
@@ -0,0 +1,12 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="filter--item"
4
+ export default class extends Controller {
5
+ static values = {
6
+ modal: String
7
+ };
8
+
9
+ openModal() {
10
+ document.getElementById(this.modalValue).showModal()
11
+ }
12
+ }
@@ -0,0 +1,13 @@
1
+ import {Controller} from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ handleReset(event) {
5
+ event.preventDefault();
6
+ const openModal = document.querySelector('dialog.modal[open]');
7
+ openModal.querySelectorAll('input, select').forEach(element => {
8
+ element.value = ""
9
+ });
10
+ openModal.close()
11
+ this.element.requestSubmit()
12
+ }
13
+ }
@@ -1,17 +1,20 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["delete"];
4
+ static targets = ['delete'];
5
+
5
6
  static values = {
6
7
  columnPredicate: String,
7
- category: String
8
- }
8
+ category: String,
9
+ };
9
10
 
10
11
  remove(event) {
11
12
  event.preventDefault();
12
- this.dispatch('remove', {detail: {
13
- paramKey: `q[${this.columnPredicateValue}]`,
14
- paramValue: this.categoryValue
15
- }})
13
+ this.dispatch('remove', {
14
+ detail: {
15
+ paramKey: `q[${this.columnPredicateValue}]`,
16
+ paramValue: this.categoryValue,
17
+ },
18
+ });
16
19
  }
17
20
  }
@@ -1,117 +1,87 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
  import 'chartjs-plugin-datalabels';
3
- const THOUSAND = 1000
4
- const HUNDRED_THOUSAND = 100000
5
- const MILLION = 1000000
6
- const HUNDRED_MILLION = 100000000
7
- const BILLION = 1000000000
8
- const HUNDRED_BILLION = 100000000000
9
- const TRILLION = 1000000000000
10
-
11
- function numberFormatter(num) {
12
- if (num >= THOUSAND && num < MILLION) {
13
- const thousands = num / THOUSAND
14
- if (thousands === Math.floor(thousands) || num >= HUNDRED_THOUSAND) {
15
- return Math.floor(thousands) + 'k'
16
- } else {
17
- return (Math.floor(thousands * 10) / 10) + 'k'
18
- }
19
- } else if (num >= MILLION && num < BILLION) {
20
- const millions = num / MILLION
21
- if (millions === Math.floor(millions) || num >= HUNDRED_MILLION) {
22
- return Math.floor(millions) + 'M'
23
- } else {
24
- return (Math.floor(millions * 10) / 10) + 'M'
25
- }
26
- } else if (num >= BILLION && num < TRILLION) {
27
- const billions = num / BILLION
28
- if (billions === Math.floor(billions) || num >= HUNDRED_BILLION) {
29
- return Math.floor(billions) + 'B'
30
- } else {
31
- return (Math.floor(billions * 10) / 10) + 'B'
32
- }
33
- } else {
34
- return num
35
- }
36
- }
37
3
 
38
4
  export default class extends Controller {
39
- connect() {
40
- const funnel = JSON.parse(this.element.dataset.data);
5
+ connect() {
6
+ const funnel = JSON.parse(this.element.dataset.data);
41
7
 
42
- const fontFamily = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"'
43
- const labels = funnel.steps.map((step) => step.name)
44
- const stepData = funnel.steps.map((step) => step.total_events)
45
- const dropOffData = funnel.steps.map((step) => step.drop_off * 100)
8
+ const fontFamily = 'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"';
9
+ const labels = funnel.steps.map((step) => step.name);
10
+ const stepData = funnel.steps.map((step) => step.total_events);
11
+ const dropOffData = funnel.steps.map((step) => step.drop_off * 100);
46
12
 
47
- const data = {
48
- labels: labels,
49
- datasets: [
50
- {
51
- label: 'Visitors',
52
- data: stepData,
53
- borderRadius: 4,
54
- stack: 'Stack 0',
55
- },
56
- {
57
- label: 'Dropoff',
58
- data: dropOffData,
59
- borderRadius: 4,
60
- stack: 'Stack 0',
61
- },
62
- ],
63
- }
13
+ const data = {
14
+ labels,
15
+ datasets: [
16
+ {
17
+ label: 'Visitors',
18
+ data: stepData,
19
+ borderRadius: 4,
20
+ stack: 'Stack 0',
21
+ },
22
+ {
23
+ label: 'Dropoff',
24
+ data: dropOffData,
25
+ borderRadius: 4,
26
+ stack: 'Stack 0',
27
+ },
28
+ ],
29
+ };
64
30
 
65
- const config = {
66
- responsive:true,
67
- maintainAspectRatio: false,
68
- plugins: [ChartDataLabels],
69
- type: 'bar',
70
- data: data,
71
- options: {
72
- layout: {
73
- padding: 100,
74
- },
75
- plugins: {
76
- legend: {
77
- display: false,
78
- },
79
- tooltip: {
80
- mode: 'index',
81
- intersect: true,
82
- position: 'average',
83
- },
84
- datalabels: {
85
- anchor: 'end',
86
- align: 'end',
87
- borderRadius: 4,
88
- padding: { top: 8, bottom: 8, right: 8, left: 8 },
89
- font: { size: 12, weight: 'normal', lineHeight: 1.6, family: fontFamily },
90
- textAlign: 'center',
91
- },
92
- },
93
- scales: {
94
- y: { display: false },
95
- x: {
96
- position: 'bottom',
97
- display: true,
98
- border: { display: false },
99
- grid: { drawBorder: false, display: false },
100
- ticks: {
101
- padding: 8,
102
- font: { weight: 'bold', family: fontFamily, size: 14 },
103
- color: 'rgb(228, 228, 231)'
104
- },
105
- },
106
- },
31
+ const config = {
32
+ responsive: true,
33
+ maintainAspectRatio: false,
34
+ plugins: [ChartDataLabels],
35
+ type: 'bar',
36
+ data,
37
+ options: {
38
+ layout: {
39
+ padding: 100,
40
+ },
41
+ plugins: {
42
+ legend: {
43
+ display: false,
44
+ },
45
+ tooltip: {
46
+ mode: 'index',
47
+ intersect: true,
48
+ position: 'average',
49
+ },
50
+ datalabels: {
51
+ anchor: 'end',
52
+ align: 'end',
53
+ borderRadius: 4,
54
+ padding: {
55
+ top: 8, bottom: 8, right: 8, left: 8,
56
+ },
57
+ font: {
58
+ size: 12, weight: 'normal', lineHeight: 1.6, family: fontFamily,
59
+ },
60
+ textAlign: 'center',
61
+ },
62
+ },
63
+ scales: {
64
+ y: { display: false },
65
+ x: {
66
+ position: 'bottom',
67
+ display: true,
68
+ border: { display: false },
69
+ grid: { drawBorder: false, display: false },
70
+ ticks: {
71
+ padding: 8,
72
+ font: { weight: 'bold', family: fontFamily, size: 14 },
73
+ color: 'rgb(228, 228, 231)',
107
74
  },
108
- }
75
+ },
76
+ },
77
+ },
78
+ };
109
79
 
110
- const visitorsData = []
80
+ const visitorsData = [];
111
81
 
112
- new Chart(
113
- this.element,
114
- config
115
- );
116
- }
82
+ new Chart(
83
+ this.element,
84
+ config,
85
+ );
86
+ }
117
87
  }
@@ -1,3 +1,4 @@
1
- import {application} from "controllers/application"
2
- import {eagerLoadControllersFrom} from "@hotwired/stimulus-loading"
3
- eagerLoadControllersFrom("controllers", application)
1
+ import { application } from 'controllers/application';
2
+ import { eagerLoadControllersFrom } from '@hotwired/stimulus-loading';
3
+
4
+ eagerLoadControllersFrom('controllers', application);
@@ -1,10 +1,10 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
4
  handleChange(event) {
5
5
  const url = new URL(event.target.form.action);
6
6
  const interval = event.target.value;
7
- url.searchParams.set("interval", interval)
8
- event.target.closest('turbo-frame').src = url.href
7
+ url.searchParams.set('interval', interval);
8
+ event.target.closest('turbo-frame').src = url.href;
9
9
  }
10
10
  }
@@ -0,0 +1,37 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ data: Object,
6
+ label: String
7
+ }
8
+ connect() {
9
+ const getCSS = (varname) => {
10
+ return `hsl(${getComputedStyle(document.documentElement).getPropertyValue(varname)})`
11
+ }
12
+
13
+ new Chart(this.element,
14
+ {
15
+ type: 'line',
16
+ data: {
17
+ labels: Object.keys(this.dataValue),
18
+ datasets: [
19
+ {
20
+ label: this.labelValue,
21
+ data: Object.values(this.dataValue),
22
+ borderColor: getCSS('--s'),
23
+ backgroundColor: getCSS('--sc'),
24
+ color: getCSS('--sf')
25
+ }
26
+ ]
27
+ },
28
+ plugins: {
29
+ colors: {
30
+ forceOverride: true
31
+ }
32
+ }
33
+ },
34
+ )
35
+
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ import {Controller} from "@hotwired/stimulus"
2
+ import SlimSelect from 'slim-select'
3
+
4
+ export default class extends Controller {
5
+ static targets = ["select"]
6
+
7
+ handleChange(event) {
8
+ this.selectTarget.name = event.target.value
9
+ }
10
+ }
@@ -1,13 +1,14 @@
1
- import {Controller} from "@hotwired/stimulus"
1
+ import { Controller } from '@hotwired/stimulus';
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ["label"]
4
+ static targets = ['label'];
5
5
 
6
6
  connect() {
7
- this.reload = this.reload.bind(this)
8
- this.setLabel = this.setLabel.bind(this)
7
+ this.reload = this.reload.bind(this);
8
+ this.setLabel = this.setLabel.bind(this);
9
9
  this.labelCount = 0;
10
-
10
+ this.reloadInterval = setInterval(this.reload, 1000 * 30);
11
+ this.labelInterval = setInterval(this.setLabel, 1000);
11
12
  }
12
13
 
13
14
  reload() {
@@ -17,11 +18,11 @@ export default class extends Controller {
17
18
 
18
19
  setLabel() {
19
20
  this.labelTarget.title = `Last updated ${this.labelCount} seconds ago`;
20
- this.labelCount += 1
21
+ this.labelCount += 1;
21
22
  }
22
23
 
23
24
  disconnect() {
24
- clearInterval(this.labelInterval)
25
- clearInterval(this.reloadInterval)
25
+ clearInterval(this.labelInterval);
26
+ clearInterval(this.reloadInterval);
26
27
  }
27
28
  }
@@ -0,0 +1,65 @@
1
+ import {Controller} from "@hotwired/stimulus"
2
+ import SlimSelect from 'slim-select'
3
+
4
+ export default class extends Controller {
5
+ static values = {
6
+ query: String,
7
+ url: String,
8
+ selected: Array
9
+ }
10
+
11
+ connect() {
12
+ this.loadedInitialData = false;
13
+ this.search = this.search.bind(this)
14
+ this.select = new SlimSelect({
15
+ select: this.element,
16
+ data: [],
17
+ settings: {
18
+ contentPosition: 'relative',
19
+ contentLocation: this.element.closest('fieldset'),
20
+ searchText: 'Sorry, no results found',
21
+ searchPlaceholder: 'Type to populate results',
22
+ placeholderText: `Search`,
23
+ searchHighlight: true
24
+ },
25
+ events: {
26
+ beforeOpen: async () => {
27
+ if (!this.loadedInitialData) {
28
+ const data = await this.search("");
29
+ this.select.setData(data);
30
+ this.loadedInitialData = true
31
+ }
32
+ },
33
+ search: this.search
34
+ }
35
+ });
36
+
37
+ if(this.selectedValue.length) {
38
+ this.select.setData(this.selectedValue.map(item => ({ "text": item, "value": item })))
39
+ this.select.setSelected(this.selectedValue)
40
+ }
41
+ }
42
+
43
+ async search(query) {
44
+ const searchParams = new URLSearchParams(window.location.search);
45
+ const formData = new FormData(this.element.form);
46
+
47
+ let deleted = [];
48
+ for (const [key, value] of formData) {
49
+ if(!deleted.includes(key)) {
50
+ searchParams.delete(key)
51
+ deleted.push(key)
52
+ }
53
+
54
+ searchParams.append(key, value)
55
+ }
56
+
57
+ searchParams.delete(this.element.name);
58
+ searchParams.set(this.queryValue, query);
59
+
60
+ const response = await fetch(`${this.urlValue}?${searchParams.toString()}`);
61
+ const data = await response.json();
62
+ return data;
63
+ }
64
+
65
+ }
@@ -0,0 +1,17 @@
1
+ import { Controller } from '@hotwired/stimulus';
2
+
3
+ // Connects to data-controller="toggle"
4
+ export default class extends Controller {
5
+ static targets = ['toggleable'];
6
+ static values = {
7
+ enable: Boolean
8
+ }
9
+
10
+ trigger() {
11
+ if (this.enableValue) {
12
+ this.toggleableTargets.forEach(element => {
13
+ element.classList.toggle('hidden');
14
+ });
15
+ }
16
+ }
17
+ }
@@ -1,16 +1,16 @@
1
1
  <div class="dropdown dropdown-end">
2
- <label
3
- tabindex="0"
4
- class="btn btn-ghost dark:hover:bg-neutral flex"
2
+ <label
3
+ tabindex="0"
4
+ class="btn btn-ghost dark:hover:bg-neutral flex"
5
5
  >
6
6
  <%= header_icon %>
7
7
  <span><%= title %></span>
8
8
  </label>
9
- <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
9
+ <ul class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box">
10
10
  <% options.each do |option| %>
11
11
  <li>
12
12
  <%= option %>
13
13
  <li>
14
14
  <% end %>
15
15
  </ul>
16
- </div>
16
+ </div>