kanaui 5.0.0 → 5.0.2

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.
@@ -103,11 +103,40 @@
103
103
 
104
104
  .kenui-analytics-dashboard-index .well ul {
105
105
  list-style-type: none;
106
- margin-top: 0.625rem;
106
+ margin: 0.625rem 0 0;
107
107
  background: #fafafa;
108
- width: fit-content;
109
- padding: 0.625rem;
108
+ width: 100%;
109
+ padding: 1rem 1.25rem;
110
110
  border-radius: 0.5rem;
111
+ display: flex;
112
+ flex-wrap: wrap;
113
+ gap: 0.5rem 1.25rem;
114
+ align-items: flex-start;
115
+ }
116
+
117
+ /* Smoothing option links (Weekly/Monthly average/sum) render as inline pills */
118
+ .kenui-analytics-dashboard-index .well > ul > li.smoothing-option {
119
+ display: inline-flex;
120
+ }
121
+
122
+ .kenui-analytics-dashboard-index .well > ul > li.smoothing-option > a {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ background: #ffffff;
126
+ border: 0.0625rem solid #d5d7da;
127
+ border-radius: 999px;
128
+ padding: 0.375rem 0.75rem;
129
+ text-decoration: none;
130
+ color: #414651;
131
+ font-weight: 500;
132
+ font-size: 0.8125rem;
133
+ line-height: 1.25rem;
134
+ transition: all 0.15s ease-in-out;
135
+ }
136
+
137
+ .kenui-analytics-dashboard-index .well > ul > li.smoothing-option > a:hover {
138
+ border-color: #1570ef;
139
+ color: #1570ef;
111
140
  }
112
141
 
113
142
  .kenui-analytics-dashboard-index .well ul li a {
@@ -168,6 +197,26 @@
168
197
  background: #ffffff;
169
198
  }
170
199
 
200
+ .kenui-analytics-dashboard-index .chart-title-container {
201
+ width: auto;
202
+ height: auto;
203
+ display: block;
204
+ justify-content: unset;
205
+ align-items: unset;
206
+ position: static;
207
+ margin-top: 15px;
208
+ }
209
+
210
+ .kenui-analytics-dashboard-index .chart-title {
211
+ font-weight: 600;
212
+ font-size: 1.25rem;
213
+ line-height: 1.75rem;
214
+ color: #111827;
215
+ white-space: normal;
216
+ transform: none;
217
+ text-align: left;
218
+ }
219
+
171
220
  .kenui-analytics-dashboard-index #chartAnchor svg {
172
221
  background: #ffffff;
173
222
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
@@ -329,11 +378,25 @@
329
378
 
330
379
  /* app/views/kanaui/reports/index.html.erb */
331
380
 
381
+ .kanaui-flash-container {
382
+ margin: 1rem 0 1.5rem;
383
+ position: relative;
384
+ z-index: 1;
385
+ }
386
+
387
+ .kanaui-flash-container .alert {
388
+ margin-bottom: 0.75rem;
389
+ }
390
+
332
391
  .kanaui-reports-index .configured-reports {
333
392
  max-width: 80rem;
334
393
  width: 100%;
335
394
  }
336
395
 
396
+ .kanaui-reports-index .kanaui-report-notice {
397
+ margin-bottom: 1rem;
398
+ }
399
+
337
400
  .kanaui-reports-index .configured-reports .configured-reports-header {
338
401
  display: flex;
339
402
  align-items: start;
@@ -393,6 +456,11 @@
393
456
  color: #d92d20 !important;
394
457
  }
395
458
 
459
+ .kanaui-reports-index .disabled-table-button {
460
+ color: #98a2b3 !important;
461
+ cursor: not-allowed;
462
+ }
463
+
396
464
  .kanaui-reports-index .edit-button {
397
465
  color: #535862 !important;
398
466
  }
@@ -600,20 +668,32 @@
600
668
  .kenui-analytics-dashboard-index .advanced-controls {
601
669
  margin: 0.625rem 0;
602
670
  padding: 0;
671
+ flex: 1 1 100%;
603
672
  }
604
673
 
605
674
  .kenui-analytics-dashboard-index .advanced-controls .form-horizontal {
606
- background: #fafafa;
675
+ background: #ffffff;
607
676
  border: 0.0625rem solid #e9eaeb;
608
677
  border-radius: 0.5rem;
609
- padding: 0.625rem;
678
+ padding: 1rem 1.25rem;
610
679
  margin-top: 0.625rem;
680
+ display: grid;
681
+ grid-template-columns: repeat(2, minmax(0, 1fr));
682
+ gap: 1rem 1.5rem;
683
+ align-items: start;
684
+ }
685
+
686
+ .kenui-analytics-dashboard-index .advanced-controls .form-horizontal > .form-group:last-of-type {
687
+ grid-column: 1 / -1;
688
+ justify-self: end;
689
+ margin-top: 0;
611
690
  }
612
691
 
613
692
  .kenui-analytics-dashboard-index .advanced-controls fieldset {
614
693
  border: none;
615
694
  margin: 0;
616
695
  padding: 0;
696
+ min-width: 0;
617
697
  }
618
698
 
619
699
  .kenui-analytics-dashboard-index .advanced-controls legend {
@@ -628,10 +708,10 @@
628
708
  }
629
709
 
630
710
  .kenui-analytics-dashboard-index .advanced-controls .form-group {
631
- margin-bottom: 1rem;
711
+ margin-bottom: 0.75rem;
632
712
  display: flex;
633
713
  flex-direction: column;
634
- align-items: center;
714
+ align-items: stretch;
635
715
  }
636
716
 
637
717
  .kenui-analytics-dashboard-index .advanced-controls .control-label {
@@ -656,7 +736,7 @@
656
736
 
657
737
  .kenui-analytics-dashboard-index .advanced-controls select.form-control {
658
738
  height: 6rem;
659
- min-width: 16rem;
739
+ min-width: 0;
660
740
  width: 100%;
661
741
  }
662
742
 
@@ -696,18 +776,19 @@
696
776
  }
697
777
 
698
778
  /* Current Analytics Query Section */
699
- .kenui-analytics-dashboard-index .well ul li:has(.query-label) {
700
- background: #f8f9fa;
779
+ .kenui-analytics-dashboard-index .well ul li.query-block {
780
+ background: #ffffff;
701
781
  border: 0.0625rem solid #e9eaeb;
702
782
  border-radius: 0.5rem;
703
- padding: 0.625rem;
704
- margin: 0.625rem 0;
783
+ padding: 0.75rem 1rem;
784
+ margin: 0;
705
785
  display: flex;
706
786
  flex-direction: column;
707
- gap: 0.75rem;
787
+ gap: 0.5rem;
788
+ flex: 1 1 100%;
708
789
  }
709
790
 
710
- .kenui-analytics-dashboard-index .well ul li:has(.query-label) a {
791
+ .kenui-analytics-dashboard-index .well ul li.query-block a {
711
792
  color: #1570ef;
712
793
  text-decoration: none;
713
794
  font-weight: 500;
@@ -720,12 +801,12 @@
720
801
  transition: color 0.15s ease-in-out;
721
802
  }
722
803
 
723
- .kenui-analytics-dashboard-index .well ul li:has(.query-label) a:hover {
804
+ .kenui-analytics-dashboard-index .well ul li.query-block a:hover {
724
805
  color: #0d5bb8;
725
806
  text-decoration: underline;
726
807
  }
727
808
 
728
- .kenui-analytics-dashboard-index .well ul li:has(.query-label) pre {
809
+ .kenui-analytics-dashboard-index .well ul li.query-block pre {
729
810
  background: #ffffff;
730
811
  border: 0.0625rem solid #d5d7da;
731
812
  border-radius: 0.375rem;
@@ -741,7 +822,7 @@
741
822
  box-shadow: 0 0.0625rem 0.1875rem rgba(0, 0, 0, 0.1);
742
823
  }
743
824
 
744
- .kenui-analytics-dashboard-index .well ul li:has(.query-label) .query-label {
825
+ .kenui-analytics-dashboard-index .well ul li.query-block .query-label {
745
826
  font-weight: 500;
746
827
  font-size: 0.875rem;
747
828
  line-height: 1.25rem;
@@ -751,6 +832,10 @@
751
832
 
752
833
  /* Responsive adjustments for smaller screens */
753
834
  @media (max-width: 768px) {
835
+ .kenui-analytics-dashboard-index .advanced-controls .form-horizontal {
836
+ grid-template-columns: 1fr;
837
+ }
838
+
754
839
  .kenui-analytics-dashboard-index .advanced-controls .form-group {
755
840
  flex-direction: column;
756
841
  align-items: flex-start;
@@ -766,7 +851,7 @@
766
851
  width: 100%;
767
852
  }
768
853
 
769
- .kenui-analytics-dashboard-index .well ul li:has(.query-label) pre {
854
+ .kenui-analytics-dashboard-index .well ul li.query-block pre {
770
855
  font-size: 0.75rem;
771
856
  padding: 0.5rem 0.75rem;
772
857
  }
@@ -3,6 +3,91 @@
3
3
  white-space: nowrap;
4
4
  }
5
5
 
6
+ .kanaui-field-control {
7
+ display: flex;
8
+ align-items: center;
9
+ gap: 8px;
10
+ }
11
+
12
+ .kanaui-field-control-top {
13
+ align-items: flex-start;
14
+ }
15
+
16
+ .kanaui-field-control .form-control {
17
+ flex: 1 1 auto;
18
+ }
19
+
20
+ .kanaui-field-help {
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ flex: 0 0 auto;
25
+ width: 22px;
26
+ height: 22px;
27
+ padding: 0;
28
+ color: #6b7280;
29
+ background: transparent;
30
+ border: 0;
31
+ box-shadow: none;
32
+ font-size: 16px;
33
+ font-weight: 600;
34
+ line-height: 1;
35
+ text-align: center;
36
+ cursor: pointer;
37
+ }
38
+
39
+ .kanaui-field-help:hover,
40
+ .kanaui-field-help:focus {
41
+ color: #111827;
42
+ background: transparent;
43
+ outline: none;
44
+ }
45
+
46
+ .kanaui-field-help:focus {
47
+ box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.24);
48
+ border-radius: 3px;
49
+ }
50
+
51
+ .kanaui-field-tooltip {
52
+ position: absolute;
53
+ z-index: 1070;
54
+ display: none;
55
+ max-width: 360px;
56
+ padding: 10px 12px;
57
+ color: #1f2937;
58
+ background-color: #fff;
59
+ border: 1px solid #d1d5db;
60
+ border-radius: 6px;
61
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
62
+ font-size: 14px;
63
+ font-weight: 400;
64
+ line-height: 1.5;
65
+ pointer-events: none;
66
+ }
67
+
68
+ .kanaui-field-tooltip::before {
69
+ content: "";
70
+ position: absolute;
71
+ top: 12px;
72
+ left: -6px;
73
+ width: 0;
74
+ height: 0;
75
+ border-top: 6px solid transparent;
76
+ border-bottom: 6px solid transparent;
77
+ border-right: 6px solid #fff;
78
+ }
79
+
80
+ .kanaui-field-tooltip.is-flipped::before {
81
+ left: auto;
82
+ right: -6px;
83
+ border-right: 0;
84
+ border-left: 6px solid #fff;
85
+ }
86
+
87
+ .kanaui-field-tooltip.is-visible {
88
+ display: block;
89
+ }
90
+
6
91
  .sql-query {
7
92
  word-break: break-word;
8
93
  }
@@ -11,7 +11,7 @@ module Kanaui
11
11
 
12
12
  @raw_name = (params[:name] || '').split('^')[0]
13
13
 
14
- @end_date = params[:end_date] || Date.today.to_s
14
+ @end_date = params[:end_date] || Time.zone.today.to_s
15
15
 
16
16
  @available_start_dates = start_date_options
17
17
  @start_date = params[:start_date] || (params[:delta_days].present? ? (@end_date.to_date - params[:delta_days].to_i.day).to_s : @available_start_dates['Last 6 months'])
@@ -19,9 +19,13 @@ module Kanaui
19
19
  @reports = JSON.parse(raw_reports)
20
20
  @report = current_report(@reports) || {}
21
21
 
22
- # If no report name is provided, redirect to the default (second) report
23
- if @raw_name.blank? && @reports.is_a?(Array) && @reports[1].present?
24
- default_name = @reports[1]['reportName']
22
+ # If no report name is provided, redirect to a default report.
23
+ # Prefer the historical default (second report) when present, but fall back
24
+ # to the first report so a single configured report still renders the
25
+ # dashboard controls and chart area.
26
+ default_report = @reports[1] || @reports[0] if @reports.is_a?(Array)
27
+ default_name = default_report&.fetch('reportName', nil).presence
28
+ if @raw_name.blank? && default_name.present?
25
29
  query_params = { start_date: @start_date,
26
30
  end_date: @end_date,
27
31
  name: default_name,
@@ -29,8 +33,8 @@ module Kanaui
29
33
  sql_only: params[:sql_only],
30
34
  format: params[:format] }
31
35
 
32
- query_params[:fake] = params[:fake] unless params[:fake].blank?
33
- query_params[:type] = params[:type] unless params[:type].blank?
36
+ query_params[:fake] = params[:fake] if params[:fake].present?
37
+ query_params[:type] = params[:type] if params[:type].present?
34
38
 
35
39
  redirect_to dashboard_index_path(query_params) and return
36
40
  end
@@ -46,14 +50,14 @@ module Kanaui
46
50
  name = query.present? ? "#{params[:name]}#{query}^metric:count" : params[:name]
47
51
  query_params = { start_date: @start_date,
48
52
  end_date: @end_date,
49
- name: name,
53
+ name:,
50
54
  smooth: params[:smooth],
51
55
  sql_only: params[:sql_only],
52
56
  format: params[:format] }
53
57
 
54
58
  # Test only
55
- query_params[:fake] = params[:fake] unless params[:fake].blank?
56
- query_params[:type] = params[:type] unless params[:type].blank?
59
+ query_params[:fake] = params[:fake] if params[:fake].present?
60
+ query_params[:type] = params[:type] if params[:type].present?
57
61
 
58
62
  redirect_to dashboard_index_path(query_params) and return
59
63
  end
@@ -86,12 +90,12 @@ module Kanaui
86
90
  respond_to do |fmt|
87
91
  fmt.csv do
88
92
  filename = params[:name]
89
- unless params[:start_date].blank?
93
+ if params[:start_date].present?
90
94
  filename += "_#{params[:start_date]}"
91
- filename += "-#{params[:end_date]}" unless params[:end_date].blank?
95
+ filename += "-#{params[:end_date]}" if params[:end_date].present?
92
96
  end
93
97
  filename += '.csv'
94
- send_data(raw_reports, filename: filename)
98
+ send_data(raw_reports, filename:)
95
99
  end
96
100
  fmt.all { render json: reports }
97
101
  end
@@ -142,20 +146,19 @@ module Kanaui
142
146
  groups[field_name] = params["group_#{field_name}"]
143
147
  end
144
148
 
145
- filter_query = ''
149
+ filter_query = +''
146
150
  filters.each do |k, v|
147
151
  next if v.blank?
148
152
 
149
- filter_query << '%26' unless filter_query.blank?
153
+ filter_query.presence&.<<('%26')
150
154
  filter_query << "(#{k}=#{v.join("|#{k}=")})"
151
155
  end
152
- query << "^filter:#{filter_query}" unless filter_query.blank?
156
+ query << "^filter:#{filter_query}" if filter_query.present?
153
157
 
154
158
  groups.each do |k, v|
155
159
  next if v.blank?
156
160
 
157
- # TODO: Make "no other" configurable
158
- query << "^dimension:#{k}(#{v.join('|')}|-)"
161
+ query << "^dimension:#{k}(#{v.join('|')})"
159
162
  end
160
163
 
161
164
  # Template variables
@@ -27,8 +27,27 @@ module Kanaui
27
27
  end
28
28
 
29
29
  rescue_from(KillBillClient::API::ResponseError) do |killbill_exception|
30
- flash[:error] = "Error while communicating with the Kill Bill server: #{as_string(killbill_exception)}"
31
- redirect_to dashboard_index_path
30
+ error_message = I18n.t('kanaui.errors.killbill_communication', error: as_string(killbill_exception))
31
+
32
+ if json_request?
33
+ render json: { message: error_message }, status: killbill_exception.response.code.to_i
34
+ else
35
+ flash[:error] = error_message
36
+ # Avoid redirect loops when the dashboard itself cannot be loaded.
37
+ if controller_name == 'dashboard' && action_name == 'index'
38
+ render plain: error_message, status: killbill_exception.response.code.to_i
39
+ else
40
+ redirect_to dashboard_index_path
41
+ end
42
+ end
43
+ end
44
+
45
+ def json_request?
46
+ request.format.json? || params[:format] == 'json' || dashboard_reports_json_request?
47
+ end
48
+
49
+ def dashboard_reports_json_request?
50
+ controller_name == 'dashboard' && action_name == 'reports' && params[:format].blank?
32
51
  end
33
52
 
34
53
  def as_string(e)
@@ -4,48 +4,57 @@ module Kanaui
4
4
  class ReportsController < Kanaui::EngineController
5
5
  def index
6
6
  @reports = JSON.parse(Kanaui::DashboardHelper::DashboardApi.available_reports(options_for_klient)).map(&:deep_symbolize_keys)
7
+ @report_notice = report_notice_from_flash
7
8
  end
8
9
 
9
10
  def new
10
11
  @report = {}
11
12
  end
12
13
 
13
- def create
14
- Kanaui::DashboardHelper::DashboardApi.create_report(report_from_params.to_json, options_for_klient)
15
-
16
- flash[:notice] = 'Report successfully created'
17
- redirect_to action: :index
18
- end
19
-
20
14
  def edit
21
15
  @report = JSON.parse(Kanaui::DashboardHelper::DashboardApi.available_reports(options_for_klient))
22
16
  .find { |x| x['reportName'] == params.require(:id) }
23
17
  .deep_symbolize_keys
24
18
  end
25
19
 
20
+ def create
21
+ Kanaui::DashboardHelper::DashboardApi.create_report(report_from_params.to_json, options_for_klient)
22
+
23
+ redirect_to_index_with_notice(:created)
24
+ end
25
+
26
26
  def update
27
27
  Kanaui::DashboardHelper::DashboardApi.update_report(params.require(:id), report_from_params.to_json, options_for_klient)
28
28
 
29
- flash[:notice] = 'Report successfully updated'
30
- redirect_to action: :index
29
+ redirect_to_index_with_notice(:updated)
31
30
  end
32
31
 
33
32
  def refresh
34
33
  Kanaui::DashboardHelper::DashboardApi.refresh_report(params.require(:id), options_for_klient)
35
34
 
36
- flash[:notice] = 'Report refresh successfully scheduled'
37
- redirect_to action: :index
35
+ redirect_to_index_with_notice(:refresh_scheduled)
38
36
  end
39
37
 
40
38
  def destroy
41
39
  Kanaui::DashboardHelper::DashboardApi.delete_report(params.require(:id), options_for_klient)
42
40
 
43
- flash[:notice] = 'Report successfully deleted'
44
- redirect_to action: :index
41
+ redirect_to_index_with_notice(:deleted)
45
42
  end
46
43
 
47
44
  private
48
45
 
46
+ def report_notice_from_flash
47
+ notice_key = flash[:report_notice].presence&.to_s
48
+ return nil unless %w[created updated refresh_scheduled deleted].include?(notice_key)
49
+
50
+ I18n.t("kanaui.reports.notices.#{notice_key}", default: nil)
51
+ end
52
+
53
+ def redirect_to_index_with_notice(notice_key)
54
+ flash[:report_notice] = notice_key
55
+ redirect_to action: :index
56
+ end
57
+
49
58
  def report_from_params
50
59
  {
51
60
  reportName: params[:report_name],
@@ -18,9 +18,15 @@ module Kanaui
18
18
  end
19
19
 
20
20
  def reports(start_date, end_date, name, smooth, sql_only, format, options = {})
21
- path = "#{KILLBILL_ANALYTICS_PREFIX}/reports?format=#{format}&startDate=#{start_date}&endDate=#{end_date}&name=#{name}"
22
- path = "#{path}&smooth=#{smooth}" if smooth
23
- path = "#{path}&sqlOnly=true" unless sql_only.blank?
21
+ query = {
22
+ 'format' => format,
23
+ 'startDate' => start_date,
24
+ 'endDate' => end_date,
25
+ 'name' => name
26
+ }
27
+ query['smooth'] = smooth if smooth
28
+ query['sqlOnly'] = 'true' if sql_only.present?
29
+ path = "#{KILLBILL_ANALYTICS_PREFIX}/reports?#{query.to_query}"
24
30
  response = KillBillClient::API.get path, {}, options
25
31
  response.body
26
32
  end
@@ -43,6 +43,10 @@
43
43
  <%= link_to "Fake line", kanaui_engine.dashboard_index_path(fake: 1, name: 'fake_line', type: 'line') %>
44
44
  </li>
45
45
  </ul>
46
+ <% elsif @reports.empty? %>
47
+ <select class="form-select" id="report-select" disabled>
48
+ <option value=""><%= t('kanaui.dashboard.no_reports_available') %></option>
49
+ </select>
46
50
  <% else %>
47
51
  <select class="form-select" id="report-select" onchange="goToReport(this.value);">
48
52
  <% @reports.each_with_index do |r, idx| %>
@@ -58,7 +62,7 @@
58
62
 
59
63
 
60
64
  <div class="calendar-container">
61
- <% if params[:name] %>
65
+ <% if params[:name].present? || @reports.empty? %>
62
66
  <div class="form-container d-flex">
63
67
  <%= form_tag kanaui_engine.dashboard_index_path, :class => 'form-vertical d-flex', :method => :get do %>
64
68
  <div class="trigger-invoice-box form-group">
@@ -79,7 +83,7 @@
79
83
  <path d="M2.83325 8.33325H17.8333" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
80
84
  </svg>
81
85
 
82
- <input class="form-control datepicker" name="start_date" type="text" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-today-highlight="true" value="<%= @start_date %>">
86
+ <input class="form-control datepicker" name="start_date" type="text" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-today-highlight="true" value="<%= @start_date %>"<%= ' disabled' if @reports.empty? %>>
83
87
  </div>
84
88
  </div>
85
89
  <div class="form-group d-flex align-items-center gap-2">
@@ -92,7 +96,7 @@
92
96
  <path d="M2.83325 8.33325H17.8333" stroke="#A4A7AE" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
93
97
  </svg>
94
98
 
95
- <input class="form-control datepicker" name="end_date" type="text" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-today-highlight="true" value="<%= @end_date %>">
99
+ <input class="form-control datepicker" name="end_date" type="text" data-provide="datepicker" data-date-format="yyyy-mm-dd" data-date-today-highlight="true" value="<%= @end_date %>"<%= ' disabled' if @reports.empty? %>>
96
100
  </div>
97
101
  </div>
98
102
  <% ((@report['variables'] || {})['fields'] || []).each do |definition| %>
@@ -118,6 +122,7 @@
118
122
  variant: "outline-secondary d-inline-flex align-items-center gap-1",
119
123
  type: "submit",
120
124
  html_class: "btn btn-xs kaui-dropdown custom-hover py-2 px-3 refresh-button",
125
+ html_options: { disabled: @reports.empty? },
121
126
  } %>
122
127
 
123
128
  <%= link_to kanaui_engine.url_for(:controller => :reports), class: 'text-decoration-none' do %>
@@ -145,6 +150,11 @@
145
150
  <div class="">
146
151
  <% if params[:name] %>
147
152
 
153
+ <div class="chart-title-container mb-2">
154
+ <h2 class="chart-title" id="chart-title">
155
+ <%= @report['reportPrettyName'] %>
156
+ </h2>
157
+ </div>
148
158
  <div id="date-controls" style="display: none;">
149
159
  <div class="custom-tabs my-4">
150
160
  <% @available_start_dates.each do |key, value| %>
@@ -153,14 +163,7 @@
153
163
  </div>
154
164
  </div>
155
165
  <div id="loading-spinner"></div>
156
- <div class="d-flex w-100 align-items-center">
157
- <div class="chart-title-container">
158
- <h2 class="chart-title" id="chart-title">
159
- <%= @raw_name.titleize %>
160
- </h2>
161
- </div>
162
- <div id="chartAnchor" data-reports-path="<%= kanaui_engine.reports_path(params.to_h) %>" class="w-100 h-100"></div>
163
- </div>
166
+ <div id="chartAnchor" data-reports-path="<%= kanaui_engine.reports_path(params.to_h) %>" data-report-name="<%= @report['reportPrettyName'] %>" class="w-100"></div>
164
167
 
165
168
  <hr>
166
169
 
@@ -202,16 +205,16 @@
202
205
  <% at_least_two_months = params[:start_date].blank? || params[:end_date].blank? || (params[:end_date].to_date.beginning_of_month - 1.month > params[:start_date].to_date) %>
203
206
  <% at_least_two_weeks = params[:start_date].blank? || params[:end_date].blank? || (params[:end_date].to_date.beginning_of_week - 1.week > params[:start_date].to_date) %>
204
207
  <% if params[:smooth] != 'AVERAGE_WEEKLY' && at_least_two_weeks %>
205
- <li><%= link_to 'Weekly average', kanaui_engine.dashboard_index_path(params.to_h.merge(:smooth => 'AVERAGE_WEEKLY')) %></li>
208
+ <li class="smoothing-option"><%= link_to 'Weekly average', kanaui_engine.dashboard_index_path(params.to_h.merge(:smooth => 'AVERAGE_WEEKLY')) %></li>
206
209
  <% end %>
207
210
  <% if params[:smooth] != 'AVERAGE_MONTHLY' && at_least_two_months %>
208
- <li><%= link_to 'Monthly average', kanaui_engine.dashboard_index_path(params.to_h.merge(:smooth => 'AVERAGE_MONTHLY')) %></li>
211
+ <li class="smoothing-option"><%= link_to 'Monthly average', kanaui_engine.dashboard_index_path(params.to_h.merge(:smooth => 'AVERAGE_MONTHLY')) %></li>
209
212
  <% end %>
210
213
  <% if params[:smooth] != 'SUM_WEEKLY' && at_least_two_weeks %>
211
- <li><%= link_to 'Weekly sum', kanaui_engine.dashboard_index_path(params.to_h.merge(:smooth => 'SUM_WEEKLY')) %></li>
214
+ <li class="smoothing-option"><%= link_to 'Weekly sum', kanaui_engine.dashboard_index_path(params.to_h.merge(:smooth => 'SUM_WEEKLY')) %></li>
212
215
  <% end %>
213
216
  <% if params[:smooth] != 'SUM_MONTHLY' && at_least_two_months %>
214
- <li><%= link_to 'Monthly sum', kanaui_engine.dashboard_index_path(params.to_h.merge(:smooth => 'SUM_MONTHLY')) %></li>
217
+ <li class="smoothing-option"><%= link_to 'Monthly sum', kanaui_engine.dashboard_index_path(params.to_h.merge(:smooth => 'SUM_MONTHLY')) %></li>
215
218
  <% end %>
216
219
  <% end %>
217
220
  <% filter_fields = ((@report['schema'] || {})['fields'] || []).select { |field| !field['distinctValues'].blank? && field['dataType'] =~ /char/ } # To ignore tenant_record_id %>
@@ -254,14 +257,14 @@
254
257
  </div>
255
258
  <% end %>
256
259
  </li>
257
- <li>
260
+ <li class="query-block">
258
261
  <div class="query-label">
259
262
  Current Analytics query:&nbsp;<%= link_to '<i class="fa fa-question-circle"></i>'.html_safe, 'http://docs.killbill.io/latest/userguide_analytics.html#_dashboard_api', :target => '_blank' %>
260
263
  </div>
261
264
  <pre><%= params[:name] -%></pre>
262
265
  </li>
263
266
  <% end %>
264
- <li><%= link_to 'SQL query', kanaui_engine.reports_path(params.to_h.merge(:sql_only => true)) %></li>
267
+ <li class="smoothing-option"><%= link_to t('kanaui.dashboard.advanced_controls.sql_query'), kanaui_engine.reports_path(params.to_h.merge(:sql_only => true)) %></li>
265
268
  </ul>
266
269
  </div>
267
270
  </div>
@@ -313,4 +316,32 @@
313
316
  // Safe to navigate
314
317
  window.location.href = url;
315
318
  }
319
+
320
+ // Persist the open/closed state of #advanced-controls across page reloads
321
+ (function () {
322
+ var STORAGE_KEY = 'kanaui.advancedControls.open';
323
+ var panel = document.getElementById('advanced-controls');
324
+ if (!panel) {
325
+ return;
326
+ }
327
+ var toggle = document.querySelector('[data-bs-toggle="collapse"][href="#advanced-controls"]');
328
+
329
+ // Restore previous state before Bootstrap initializes
330
+ try {
331
+ if (localStorage.getItem(STORAGE_KEY) === '1') {
332
+ panel.classList.add('show');
333
+ if (toggle) {
334
+ toggle.setAttribute('aria-expanded', 'true');
335
+ toggle.classList.remove('collapsed');
336
+ }
337
+ }
338
+ } catch (e) { /* localStorage may be unavailable */ }
339
+
340
+ panel.addEventListener('shown.bs.collapse', function () {
341
+ try { localStorage.setItem(STORAGE_KEY, '1'); } catch (e) {}
342
+ });
343
+ panel.addEventListener('hidden.bs.collapse', function () {
344
+ try { localStorage.setItem(STORAGE_KEY, '0'); } catch (e) {}
345
+ });
346
+ })();
316
347
  <% end %>