sage-rails 0.0.6 → 0.0.8

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.
@@ -0,0 +1,215 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ // Connects to data-controller="select"
4
+ export default class extends Controller {
5
+ static targets = ["input", "dropdown", "option", "hidden"];
6
+ static values = {
7
+ options: Array,
8
+ placeholder: String,
9
+ selected: String,
10
+ maxOptions: { type: Number, default: 100 }
11
+ };
12
+
13
+ connect() {
14
+ this.selectedValue = this.selectedValue || "";
15
+
16
+ // Parse options if they're a string
17
+ let options = this.optionsValue;
18
+ if (typeof options === 'string') {
19
+ try {
20
+ options = JSON.parse(options);
21
+ } catch (e) {
22
+ console.error("Failed to parse options:", e);
23
+ options = [];
24
+ }
25
+ }
26
+
27
+ this.filteredOptions = Array.isArray(options) ? options.slice(0, this.maxOptionsValue) : [];
28
+
29
+ // If there's a pre-selected value from the data attribute, set it
30
+ if (this.hasSelectedValue && this.selectedValue) {
31
+ const selectedOption = this.filteredOptions.find(opt => opt.value == this.selectedValue);
32
+ if (selectedOption) {
33
+ this.inputTarget.value = selectedOption.text;
34
+ if (this.hasHiddenTarget) {
35
+ this.hiddenTarget.value = selectedOption.value;
36
+ }
37
+ }
38
+ }
39
+
40
+ this.render();
41
+ this.setupEventListeners();
42
+ }
43
+
44
+ setupEventListeners() {
45
+ // Close dropdown when clicking outside
46
+ document.addEventListener('click', this.handleOutsideClick.bind(this));
47
+
48
+ // Handle keyboard navigation
49
+ this.inputTarget.addEventListener('keydown', this.handleKeydown.bind(this));
50
+ }
51
+
52
+ disconnect() {
53
+ document.removeEventListener('click', this.handleOutsideClick.bind(this));
54
+ }
55
+
56
+ handleOutsideClick(event) {
57
+ if (!this.element.contains(event.target)) {
58
+ this.closeDropdown();
59
+ }
60
+ }
61
+
62
+ handleKeydown(event) {
63
+ const dropdown = this.dropdownTarget;
64
+ const options = this.optionTargets;
65
+ const activeOption = dropdown.querySelector('.active');
66
+
67
+ switch(event.key) {
68
+ case 'ArrowDown':
69
+ event.preventDefault();
70
+ this.navigateOptions(options, activeOption, 1);
71
+ break;
72
+ case 'ArrowUp':
73
+ event.preventDefault();
74
+ this.navigateOptions(options, activeOption, -1);
75
+ break;
76
+ case 'Enter':
77
+ event.preventDefault();
78
+ if (activeOption) {
79
+ this.selectOption(activeOption.dataset.value, activeOption.textContent);
80
+ }
81
+ break;
82
+ case 'Escape':
83
+ this.closeDropdown();
84
+ break;
85
+ }
86
+ }
87
+
88
+ navigateOptions(options, activeOption, direction) {
89
+ let currentIndex = activeOption ? Array.from(options).indexOf(activeOption) : -1;
90
+ let nextIndex = currentIndex + direction;
91
+
92
+ if (nextIndex < 0) nextIndex = options.length - 1;
93
+ if (nextIndex >= options.length) nextIndex = 0;
94
+
95
+ // Remove active class from all options
96
+ options.forEach(option => option.classList.remove('active'));
97
+
98
+ // Add active class to next option
99
+ if (options[nextIndex]) {
100
+ options[nextIndex].classList.add('active');
101
+ options[nextIndex].scrollIntoView({ block: 'nearest' });
102
+ }
103
+ }
104
+
105
+ search(event) {
106
+ const query = event.target.value.toLowerCase().trim();
107
+
108
+ // Parse options properly
109
+ let options = this.optionsValue;
110
+ if (typeof options === 'string') {
111
+ try {
112
+ options = JSON.parse(options);
113
+ } catch (e) {
114
+ options = [];
115
+ }
116
+ }
117
+ options = Array.isArray(options) ? options : [];
118
+
119
+ if (query === '' || query === ' ') {
120
+ this.filteredOptions = options.slice(0, this.maxOptionsValue);
121
+ } else {
122
+ this.filteredOptions = options
123
+ .filter(option => option.text.toLowerCase().includes(query))
124
+ .slice(0, this.maxOptionsValue);
125
+ }
126
+
127
+ this.renderOptions();
128
+ this.openDropdown();
129
+ }
130
+
131
+ selectOption(value, text) {
132
+ // Update the input to show selected text
133
+ this.inputTarget.value = text;
134
+ this.selectedValue = value;
135
+
136
+ // Update hidden field if present
137
+ if (this.hasHiddenTarget) {
138
+ this.hiddenTarget.value = value;
139
+ }
140
+
141
+ // Dispatch custom event for external handling
142
+ const selectEvent = new CustomEvent('select:change', {
143
+ detail: { value: value, text: text },
144
+ bubbles: true
145
+ });
146
+ this.element.dispatchEvent(selectEvent);
147
+
148
+ this.closeDropdown();
149
+ }
150
+
151
+ openDropdown() {
152
+ this.dropdownTarget.classList.remove('hidden');
153
+ this.dropdownTarget.classList.add('visible');
154
+ }
155
+
156
+ closeDropdown() {
157
+ this.dropdownTarget.classList.remove('visible');
158
+ this.dropdownTarget.classList.add('hidden');
159
+ }
160
+
161
+ focus() {
162
+ // Reset search to show all options when focusing
163
+ const currentValue = this.inputTarget.value.trim();
164
+ if (currentValue === '' || currentValue === ' ') {
165
+ // Parse options properly
166
+ let options = this.optionsValue;
167
+ if (typeof options === 'string') {
168
+ try {
169
+ options = JSON.parse(options);
170
+ } catch (e) {
171
+ options = [];
172
+ }
173
+ }
174
+ this.filteredOptions = Array.isArray(options) ? options.slice(0, this.maxOptionsValue) : [];
175
+ this.renderOptions();
176
+ }
177
+ this.openDropdown();
178
+ }
179
+
180
+ blur() {
181
+ // Delay hiding to allow option clicks
182
+ setTimeout(() => {
183
+ this.closeDropdown();
184
+ }, 150);
185
+ }
186
+
187
+ render() {
188
+ // Don't set placeholder for Beer CSS floating labels
189
+ this.renderOptions();
190
+ }
191
+
192
+ renderOptions() {
193
+ this.dropdownTarget.innerHTML = '';
194
+
195
+ if (this.filteredOptions.length === 0) {
196
+ const noResults = document.createElement('div');
197
+ noResults.className = 'select-option no-results';
198
+ noResults.textContent = 'No results found';
199
+ this.dropdownTarget.appendChild(noResults);
200
+ return;
201
+ }
202
+
203
+ this.filteredOptions.forEach(option => {
204
+ const optionElement = document.createElement('div');
205
+ optionElement.className = 'select-option';
206
+ optionElement.dataset.selectTarget = 'option';
207
+ optionElement.dataset.value = option.value;
208
+ optionElement.textContent = option.text;
209
+ optionElement.addEventListener('click', () => {
210
+ this.selectOption(option.value, option.text);
211
+ });
212
+ this.dropdownTarget.appendChild(optionElement);
213
+ });
214
+ }
215
+ }
@@ -0,0 +1,122 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["form", "variable"]
5
+
6
+ connect() {
7
+ // Override the global submitIfCompleted function from Blazer
8
+ window.submitIfCompleted = this.submitIfCompleted.bind(this)
9
+
10
+ // Set up event listeners for variable changes
11
+ this.setupVariableListeners()
12
+ }
13
+
14
+ setupVariableListeners() {
15
+ // Listen for changes on all variable inputs
16
+ this.variableTargets.forEach(input => {
17
+ input.addEventListener("change", (event) => {
18
+ this.handleVariableChange(event)
19
+ })
20
+
21
+ // Also listen for daterangepicker events if present
22
+ if (input.dataset.daterangepicker) {
23
+ const picker = $(input).data('daterangepicker')
24
+ if (picker) {
25
+ $(input).on('apply.daterangepicker', (ev, picker) => {
26
+ this.handleDateRangeChange(input, picker)
27
+ })
28
+ }
29
+ }
30
+ })
31
+ }
32
+
33
+ handleVariableChange(event) {
34
+ const input = event.target
35
+ console.log(`Variable ${input.name} changed to:`, input.value)
36
+
37
+ // Check if all required variables are filled and submit if so
38
+ setTimeout(() => {
39
+ this.submitIfCompleted(this.formTarget)
40
+ }, 100) // Small delay to ensure all values are updated
41
+ }
42
+
43
+ handleDateRangeChange(input, picker) {
44
+ console.log(`Date range updated for ${input.name}:`, input.value)
45
+
46
+ // Force update any related hidden fields
47
+ this.updateRelatedDateFields(input, picker)
48
+
49
+ // Submit the form after date range is updated
50
+ setTimeout(() => {
51
+ this.submitIfCompleted(this.formTarget)
52
+ }, 200) // Longer delay for date picker updates
53
+ }
54
+
55
+ updateRelatedDateFields(input, picker) {
56
+ // If this is a start_time/end_time combo, make sure both are updated
57
+ if (input.name === "start_time" || input.name === "end_time") {
58
+ const startTimeInput = this.formTarget.querySelector('input[name="start_time"]')
59
+ const endTimeInput = this.formTarget.querySelector('input[name="end_time"]')
60
+
61
+ if (startTimeInput && endTimeInput && picker) {
62
+ // Update both fields based on the picker values
63
+ if (picker.startDate) {
64
+ startTimeInput.value = picker.startDate.utc().format()
65
+ }
66
+ if (picker.endDate) {
67
+ endTimeInput.value = picker.endDate.endOf("day").utc().format()
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ submitIfCompleted(form) {
74
+ if (!form) return
75
+
76
+ let completed = true
77
+ const requiredInputs = form.querySelectorAll('input[name], select[name]')
78
+
79
+ // Check each required input
80
+ requiredInputs.forEach(input => {
81
+ const value = input.value
82
+
83
+ // More robust empty check
84
+ if (this.isEmpty(value)) {
85
+ completed = false
86
+ console.log(`Variable ${input.name} is empty:`, value)
87
+ } else {
88
+ console.log(`Variable ${input.name} has value:`, value)
89
+ }
90
+ })
91
+
92
+ console.log(`Form completion check: ${completed ? 'Complete' : 'Incomplete'}`)
93
+
94
+ if (completed) {
95
+ console.log('Submitting form with all variables filled')
96
+ form.submit()
97
+ }
98
+ }
99
+
100
+ isEmpty(value) {
101
+ // More comprehensive empty check
102
+ return value === null ||
103
+ value === undefined ||
104
+ value === "" ||
105
+ (typeof value === "string" && value.trim() === "")
106
+ }
107
+
108
+ // Manual trigger for testing
109
+ triggerSubmit() {
110
+ this.submitIfCompleted(this.formTarget)
111
+ }
112
+
113
+ // Debug method to check current variable states
114
+ debugVariables() {
115
+ console.log("=== Current Variable States ===")
116
+ const inputs = this.formTarget.querySelectorAll('input[name], select[name]')
117
+ inputs.forEach(input => {
118
+ console.log(`${input.name}: "${input.value}" (${typeof input.value})`)
119
+ })
120
+ console.log("===============================")
121
+ }
122
+ }
@@ -0,0 +1,21 @@
1
+ // Import controllers
2
+ import SearchController from "sage/controllers/search_controller"
3
+ import ClipboardController from "sage/controllers/clipboard_controller"
4
+ import SelectController from "sage/controllers/select_controller"
5
+ import DashboardController from "sage/controllers/dashboard_controller"
6
+ import ReverseInfiniteScrollController from "sage/controllers/reverse_infinite_scroll_controller"
7
+ import VariablesController from "sage/controllers/variables_controller"
8
+
9
+ // Export all Sage controllers for manual registration
10
+ export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController }
11
+
12
+ // Register all Sage controllers with the provided Stimulus application
13
+ export function registerControllers(application) {
14
+ application.register("sage--search", SearchController)
15
+ application.register("sage--clipboard", ClipboardController)
16
+ application.register("sage--select", SelectController)
17
+ application.register("sage--dashboard", DashboardController)
18
+ application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
19
+ application.register("sage--variables", VariablesController)
20
+ }
21
+
@@ -5,13 +5,13 @@ module Sage
5
5
  def index
6
6
  @q = Blazer::Check.ransack(params[:q])
7
7
  @checks = @q.result.joins(:query).includes(:query)
8
-
8
+
9
9
  # Apply basic ordering first
10
10
  @checks = @checks.order("blazer_queries.name, blazer_checks.id")
11
-
11
+
12
12
  # Apply pagination with Pagy
13
13
  @pagy, @checks = pagy(@checks)
14
-
14
+
15
15
  # Apply state-based sorting on the paginated results
16
16
  state_order = [ nil, "disabled", "error", "timed out", "failing", "passing" ]
17
17
  @checks = @checks.sort_by { |q| state_order.index(q.state) || 99 }
@@ -1,21 +1,21 @@
1
1
  module Sage
2
2
  class DashboardsController < BaseController
3
- before_action :set_dashboard, only: [:show, :edit, :update, :destroy, :refresh]
3
+ before_action :set_dashboard, only: [ :show, :edit, :update, :destroy, :refresh ]
4
4
 
5
5
  def index
6
6
  @q = Blazer::Dashboard.ransack(params[:q])
7
7
  @dashboards = @q.result
8
-
8
+
9
9
  # Only include creator if Blazer.user_class is configured
10
10
  @dashboards = @dashboards.includes(:creator) if Blazer.user_class
11
-
11
+
12
12
  @dashboards = @dashboards.order(:name)
13
-
13
+
14
14
  # Apply additional filters if needed
15
15
  if blazer_user && params[:filter] == "mine"
16
16
  @dashboards = @dashboards.where(creator_id: blazer_user.id).reorder(updated_at: :desc)
17
17
  end
18
-
18
+
19
19
  # Apply pagination with Pagy
20
20
  @pagy, @dashboards = pagy(@dashboards)
21
21
  end
@@ -40,7 +40,7 @@ module Sage
40
40
  def show
41
41
  @queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query)
42
42
  @query_errors = {}
43
-
43
+
44
44
  @queries.each do |query|
45
45
  # Check if the query has a valid data source
46
46
  if query.data_source.blank? || !Blazer.data_sources.key?(query.data_source)
@@ -53,12 +53,12 @@ module Sage
53
53
 
54
54
  @smart_vars = {}
55
55
  @sql_errors = []
56
- @data_sources = @queries.map { |q|
56
+ @data_sources = @queries.map { |q|
57
57
  # Use the query's data source if specified, otherwise use the default
58
58
  source = q.data_source.presence || Blazer.data_sources.keys.first
59
59
  Blazer.data_sources[source]
60
60
  }.compact.uniq
61
-
61
+
62
62
  @bind_vars.each do |var|
63
63
  @data_sources.each do |data_source|
64
64
  smart_var, error = parse_smart_variables(var, data_source)
@@ -127,4 +127,3 @@ module Sage
127
127
  end
128
128
  end
129
129
  end
130
-
@@ -150,7 +150,7 @@ module Sage
150
150
 
151
151
  # fallback for now for users with open tabs
152
152
  # TODO remove fallback in future version
153
- @var_params = request.request_parameters["variables"] || request.request_parameters
153
+ @var_params = params[:variables] || request.request_parameters["variables"] || request.request_parameters
154
154
  @success = process_vars(@statement, @var_params)
155
155
  @only_chart = params[:only_chart]
156
156
  @run_id = blazer_params[:run_id]
@@ -11,7 +11,7 @@ module Sage
11
11
  messages.group_by { |message| message.created_at.to_date }
12
12
  .sort_by { |date, _| date }
13
13
  end
14
-
14
+
15
15
  def format_message_date(date)
16
16
  case date
17
17
  when Date.current
@@ -4,7 +4,7 @@ module Sage
4
4
  messages.group_by { |message| message.created_at.to_date }
5
5
  .sort_by { |date, _| date }
6
6
  end
7
-
7
+
8
8
  def format_message_date(date)
9
9
  case date
10
10
  when Date.current
@@ -20,4 +20,4 @@ module Sage
20
20
  end
21
21
  end
22
22
  end
23
- end
23
+ end
@@ -0,0 +1,122 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["form", "variable"]
5
+
6
+ connect() {
7
+ // Override the global submitIfCompleted function from Blazer
8
+ window.submitIfCompleted = this.submitIfCompleted.bind(this)
9
+
10
+ // Set up event listeners for variable changes
11
+ this.setupVariableListeners()
12
+ }
13
+
14
+ setupVariableListeners() {
15
+ // Listen for changes on all variable inputs
16
+ this.variableTargets.forEach(input => {
17
+ input.addEventListener("change", (event) => {
18
+ this.handleVariableChange(event)
19
+ })
20
+
21
+ // Also listen for daterangepicker events if present
22
+ if (input.dataset.daterangepicker) {
23
+ const picker = $(input).data('daterangepicker')
24
+ if (picker) {
25
+ $(input).on('apply.daterangepicker', (ev, picker) => {
26
+ this.handleDateRangeChange(input, picker)
27
+ })
28
+ }
29
+ }
30
+ })
31
+ }
32
+
33
+ handleVariableChange(event) {
34
+ const input = event.target
35
+ console.log(`Variable ${input.name} changed to:`, input.value)
36
+
37
+ // Check if all required variables are filled and submit if so
38
+ setTimeout(() => {
39
+ this.submitIfCompleted(this.formTarget)
40
+ }, 100) // Small delay to ensure all values are updated
41
+ }
42
+
43
+ handleDateRangeChange(input, picker) {
44
+ console.log(`Date range updated for ${input.name}:`, input.value)
45
+
46
+ // Force update any related hidden fields
47
+ this.updateRelatedDateFields(input, picker)
48
+
49
+ // Submit the form after date range is updated
50
+ setTimeout(() => {
51
+ this.submitIfCompleted(this.formTarget)
52
+ }, 200) // Longer delay for date picker updates
53
+ }
54
+
55
+ updateRelatedDateFields(input, picker) {
56
+ // If this is a start_time/end_time combo, make sure both are updated
57
+ if (input.name === "start_time" || input.name === "end_time") {
58
+ const startTimeInput = this.formTarget.querySelector('input[name="start_time"]')
59
+ const endTimeInput = this.formTarget.querySelector('input[name="end_time"]')
60
+
61
+ if (startTimeInput && endTimeInput && picker) {
62
+ // Update both fields based on the picker values
63
+ if (picker.startDate) {
64
+ startTimeInput.value = picker.startDate.utc().format()
65
+ }
66
+ if (picker.endDate) {
67
+ endTimeInput.value = picker.endDate.endOf("day").utc().format()
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ submitIfCompleted(form) {
74
+ if (!form) return
75
+
76
+ let completed = true
77
+ const requiredInputs = form.querySelectorAll('input[name], select[name]')
78
+
79
+ // Check each required input
80
+ requiredInputs.forEach(input => {
81
+ const value = input.value
82
+
83
+ // More robust empty check
84
+ if (this.isEmpty(value)) {
85
+ completed = false
86
+ console.log(`Variable ${input.name} is empty:`, value)
87
+ } else {
88
+ console.log(`Variable ${input.name} has value:`, value)
89
+ }
90
+ })
91
+
92
+ console.log(`Form completion check: ${completed ? 'Complete' : 'Incomplete'}`)
93
+
94
+ if (completed) {
95
+ console.log('Submitting form with all variables filled')
96
+ form.submit()
97
+ }
98
+ }
99
+
100
+ isEmpty(value) {
101
+ // More comprehensive empty check
102
+ return value === null ||
103
+ value === undefined ||
104
+ value === "" ||
105
+ (typeof value === "string" && value.trim() === "")
106
+ }
107
+
108
+ // Manual trigger for testing
109
+ triggerSubmit() {
110
+ this.submitIfCompleted(this.formTarget)
111
+ }
112
+
113
+ // Debug method to check current variable states
114
+ debugVariables() {
115
+ console.log("=== Current Variable States ===")
116
+ const inputs = this.formTarget.querySelectorAll('input[name], select[name]')
117
+ inputs.forEach(input => {
118
+ console.log(`${input.name}: "${input.value}" (${typeof input.value})`)
119
+ })
120
+ console.log("===============================")
121
+ }
122
+ }
@@ -4,9 +4,10 @@ import ClipboardController from "sage/controllers/clipboard_controller"
4
4
  import SelectController from "sage/controllers/select_controller"
5
5
  import DashboardController from "sage/controllers/dashboard_controller"
6
6
  import ReverseInfiniteScrollController from "sage/controllers/reverse_infinite_scroll_controller"
7
+ import VariablesController from "sage/controllers/variables_controller"
7
8
 
8
9
  // Export all Sage controllers for manual registration
9
- export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController }
10
+ export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController }
10
11
 
11
12
  // Register all Sage controllers with the provided Stimulus application
12
13
  export function registerControllers(application) {
@@ -15,5 +16,6 @@ export function registerControllers(application) {
15
16
  application.register("sage--select", SelectController)
16
17
  application.register("sage--dashboard", DashboardController)
17
18
  application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
19
+ application.register("sage--variables", VariablesController)
18
20
  }
19
21