sage-rails 0.0.5 → 0.0.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb2c81d99ac3d6dcaa9640c0b5c9b3d15612d60d089aad3c7ee889b9b6ca9d09
4
- data.tar.gz: 4315483545ae23e66765121fa02a14d6259e98016a934d7e4953e67abfc1bb80
3
+ metadata.gz: f84a14a8dc13d5bca60926490250937bc5b52ae4f77c6962e328cbb28a5635a1
4
+ data.tar.gz: 59d079bb38465f75b165ed44d63b3cfd9d1f8d52a56fdb5c78feba7cdf987237
5
5
  SHA512:
6
- metadata.gz: 2d4f2a8b7e31ab1a692a26c0a72f29e8c56e526d578170533999c307bb7769ab9142ccc9fab544700bda31e4ed38ea635f3b7e3d321db3d31cad24361a9f6e80
7
- data.tar.gz: 6b0c40cd4407780316c5b955ce45d8738b7f378a5013adbec3e97e938762be95c1d4f75079c91a88c3a7441ab3ad701933b76f8985723c5a29fa6f99895816ff
6
+ metadata.gz: 71fe7deb5b559a26703f21a3f81f998a4e32a521c91c37e2f69b01957cd39266e098ad679330cfec776408f95b2001dfa79c67f6df5fd9221a55d8e4e826d163
7
+ data.tar.gz: 4a30c3d554af61200f3424c30c00ab51263229470c8977799289f30fee9dac43605b867f7e7719af9b82c7c995288a13f493f289ccd393ad09198c01b70fcaa1
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,42 @@
1
+ # Contributing
2
+
3
+ First, thanks for wanting to contribute. You’re awesome! :heart:
4
+
5
+ ## Help
6
+
7
+ We’re not able to provide support through GitHub Issues. If you’re looking for help with your code, try posting on [Stack Overflow](https://stackoverflow.com/).
8
+
9
+ All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
10
+
11
+ ## Bugs
12
+
13
+ Think you’ve discovered a bug?
14
+
15
+ 1. Search existing issues to see if it’s been reported.
16
+ 2. Try the `master` branch to make sure it hasn’t been fixed.
17
+
18
+ ```rb
19
+ gem "sage-rails", github: "mrjonesbot/sage"
20
+ ```
21
+
22
+ If the above steps don’t help, create an issue. Include:
23
+
24
+ - Detailed steps to reproduce
25
+ - Complete backtraces for exceptions
26
+
27
+ ## New Features
28
+
29
+ If you’d like to discuss a new feature, create an issue and start the title with `[Idea]`.
30
+
31
+ ## Pull Requests
32
+
33
+ Fork the project and create a pull request. A few tips:
34
+
35
+ - Keep changes to a minimum. If you have multiple features or fixes, submit multiple pull requests.
36
+ - Follow the existing style. The code should read like it’s written by a single person.
37
+
38
+ Feel free to open an issue to get feedback on your idea before spending too much time on it.
39
+
40
+ ---
41
+
42
+ This contributing guide is released under [CCO](https://creativecommons.org/publicdomain/zero/1.0/) (public domain). Use it for your own project without attribution.
data/README.md CHANGED
@@ -2,16 +2,29 @@
2
2
 
3
3
  ![Sage Query Interface](screenshots/sage-query-interface.png)
4
4
 
5
+ > **Note:** This project is pre-v1 and is subject to frequent changes.
6
+
5
7
  **Natural language reporting to help your team build accurate reports, faster.**
6
8
 
7
9
  Sage is a Rails engine built on top of the excellent [Blazer](https://github.com/ankane/blazer) gem, adding an LLM interface to make data exploration accessible via natural language.
8
10
 
11
+ ## Requirements
12
+
13
+ Sage relies on `turbo-rails` and `stimulus-rails` to render the dashboard. These gems should already be included in most modern Rails applications. If not, add them to your Gemfile:
14
+
15
+ ```ruby
16
+ gem "turbo-rails"
17
+ gem "stimulus-rails"
18
+ ```
19
+
20
+ Refer to the [stimulus-rails installation page](https://github.com/hotwired/stimulus-rails) to get started.
21
+
9
22
  ## Installation
10
23
 
11
24
  Add Sage to your application's Gemfile:
12
25
 
13
26
  ```ruby
14
- gem "sage-rails"
27
+ gem "sage-rails", require: 'sage'
15
28
  ```
16
29
 
17
30
  Run bundle install:
@@ -88,26 +101,17 @@ For detailed information on Blazer-specific features, refer to the [Blazer docum
88
101
 
89
102
  ## Database Context
90
103
 
91
- Sage introspects your database schema to provide context for more accurate SQL generation. This feature works out of the box with Blazer's data sources.
104
+ Sage introspects your database schema to provide context for more accurate SQL generation.
92
105
 
93
106
  ### Multiple Data Sources
94
107
 
95
- If you have multiple Blazer data sources configured, Sage will use the appropriate schema for each:
108
+ If you have multiple databases, Sage will use the appropriate schema for each:
96
109
 
97
- ```ruby
98
- # config/blazer.yml
99
- data_sources:
100
- main:
101
- url: <%= ENV["DATABASE_URL"] %>
102
- analytics:
103
- url: <%= ENV["ANALYTICS_DATABASE_URL"] %>
104
- ```
105
-
106
- When querying from different data sources in Blazer, Sage automatically switches schema context.
110
+ When querying different data sources in Blazer, Sage will switch schema context.
107
111
 
108
112
  ## Model Scope Context
109
113
 
110
- Sage leverages your Rails model scopes as documentation for query patterns, dramatically improving the accuracy of generated SQL queries, especially for complex multi-table reports.
114
+ Sage introspects your Rails model scopes to understand common query patterns, dramatically improving the accuracy of generated SQL queries.
111
115
 
112
116
  ### Example
113
117
 
@@ -144,9 +148,8 @@ Sage understands:
144
148
  ### Benefits
145
149
 
146
150
  1. **Business Logic Awareness**: Scopes encode your business rules (what makes a customer "active" or "high-value")
147
- 2. **Correct JOIN Patterns**: Scopes show the proper way to join tables in your application
148
- 3. **Aggregation Patterns**: Complex scopes with GROUP BY and HAVING clauses guide report generation
149
- 4. **Consistency**: Generated queries follow the same patterns as your application code
151
+ 2. **Aggregation Patterns**: Complex scopes with GROUP BY and HAVING clauses guide report generation
152
+ 3. **Consistency**: Generated queries follow the same patterns as your application code
150
153
 
151
154
  Scopes now serve dual purposes:
152
155
  1. Reusable query logic in your Rails application
@@ -186,11 +189,6 @@ $ rails test
186
189
  - Ensure model files are in standard Rails locations (`app/models/`)
187
190
  - Check that Blazer is properly configured and can execute queries
188
191
 
189
- ### Performance
190
- - Use lighter models (Claude Haiku, GPT-3.5) for faster response times
191
- - Consider caching frequently used queries
192
- - Scope context is cached per request to minimize processing
193
-
194
192
  ## Contributing
195
193
 
196
194
  Bug reports and pull requests are welcome on GitHub. This project is intended to be a safe, welcoming space for collaboration.
@@ -4,7 +4,7 @@ import "@hotwired/turbo-rails"
4
4
  import { Application } from "@hotwired/stimulus"
5
5
 
6
6
  // Import and register Sage controllers
7
- import { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController } from "sage"
7
+ import { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController } from "sage"
8
8
 
9
9
  const application = Application.start()
10
10
  application.debug = true
@@ -16,3 +16,29 @@ application.register("sage--clipboard", ClipboardController)
16
16
  application.register("sage--select", SelectController)
17
17
  application.register("sage--dashboard", DashboardController)
18
18
  application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
19
+ application.register("sage--variables", VariablesController)
20
+
21
+ // Override Blazer's submitIfCompleted function globally
22
+ window.submitIfCompleted = function($form) {
23
+ // Try to find our Sage variables controller first
24
+ const controller = document.querySelector('[data-controller*="sage--variables"]')
25
+ if (controller && window.Stimulus) {
26
+ const controllerInstance = window.Stimulus.getControllerForElementAndIdentifier(controller, 'sage--variables')
27
+ if (controllerInstance) {
28
+ controllerInstance.submitIfCompleted($form[0] || $form)
29
+ return
30
+ }
31
+ }
32
+
33
+ // Fallback to improved version of original logic
34
+ var completed = true
35
+ $form.find("input[name], select").each(function () {
36
+ const value = $(this).val()
37
+ if (!value || value.toString().trim() === "") {
38
+ completed = false
39
+ }
40
+ })
41
+ if (completed) {
42
+ $form.submit()
43
+ }
44
+ }
@@ -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
 
@@ -0,0 +1,168 @@
1
+ <% if @bind_vars.any? %>
2
+ <% var_params = request.query_parameters %>
3
+ <%= javascript_tag nonce: true do %>
4
+ <%= blazer_js_var "timeZone", Blazer.time_zone.tzinfo.name %>
5
+ var now = moment.tz(timeZone)
6
+ var format = "YYYY-MM-DD"
7
+
8
+ function toDate(time) {
9
+ return moment.tz(time.format(format), timeZone)
10
+ }
11
+ <% end %>
12
+ <div data-controller="sage--variables" data-sage--variables-form-target="form">
13
+ <form id="bind" method="get" action="<%= action %>" class="form-inline" style="margin-bottom: 15px;">
14
+ <% date_vars = ["start_time", "end_time"] %>
15
+ <% if (date_vars - @bind_vars).empty? %>
16
+ <% @bind_vars = @bind_vars - date_vars %>
17
+ <% else %>
18
+ <% date_vars = nil %>
19
+ <% end %>
20
+
21
+ <% @bind_vars.each_with_index do |var, i| %>
22
+ <div style="margin-bottom: 10px;">
23
+ <div style="margin-bottom: 3px;">
24
+ <small class="text-muted"><code>{{<%= var %>}}</code></small>
25
+ </div>
26
+ </div>
27
+ <% if (data = @smart_vars[var]) %>
28
+ <%= select_tag var, options_for_select([[nil, nil]] + data, selected: var_params[var]), style: "margin-right: 20px; width: 200px; display: none;", data: { "sage--variables-target": "variable" } %>
29
+ <%= javascript_tag nonce: true do %>
30
+ $("#<%= var %>").selectize({
31
+ create: true
32
+ });
33
+ <% end %>
34
+ <% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
35
+ <%= hidden_field_tag var, var_params[var], data: { "sage--variables-target": "variable" } %>
36
+
37
+ <div class="selectize-control single" style="width: 200px;">
38
+ <div id="<%= var %>-select" class="selectize-input" style="display: inline-block;">
39
+ <span>Select a date</span>
40
+ </div>
41
+ </div>
42
+
43
+ <%= javascript_tag nonce: true do %>
44
+ (function() {
45
+ var input = $("#<%= var %>")
46
+ var datePicker = $("#<%= var %>-select")
47
+ datePicker.daterangepicker({
48
+ singleDatePicker: true,
49
+ locale: {format: format},
50
+ autoUpdateInput: false,
51
+ autoApply: true,
52
+ startDate: input.val().length > 0 ? moment.tz(input.val(), timeZone) : now
53
+ })
54
+ // hack to start with empty date
55
+ datePicker.on("apply.daterangepicker", function(ev, picker) {
56
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
57
+ input.val(toDate(picker.startDate).utc().format())
58
+
59
+ // Trigger our custom variable controller instead of submitIfCompleted
60
+ var controller = document.querySelector('[data-controller*="sage--variables"]')
61
+ if (controller && window.Stimulus) {
62
+ var controllerInstance = window.Stimulus.getControllerForElementAndIdentifier(controller, 'sage--variables')
63
+ if (controllerInstance) {
64
+ controllerInstance.handleDateRangeChange(input[0], picker)
65
+ }
66
+ }
67
+ })
68
+ if (input.val().length > 0) {
69
+ var picker = datePicker.data("daterangepicker")
70
+ datePicker.find("span").html(toDate(picker.startDate).format("MMMM D, YYYY"))
71
+ }
72
+ })()
73
+ <% end %>
74
+ <% else %>
75
+ <%= text_field_tag var, var_params[var], style: "width: 120px; margin-right: 20px;", autofocus: i == 0 && !var.end_with?("_at") && !var_params[var], class: "form-control", data: { "sage--variables-target": "variable" } %>
76
+ <% end %>
77
+ <% end %>
78
+
79
+ <% if date_vars %>
80
+ <% date_vars.each do |var| %>
81
+ <%= hidden_field_tag var, var_params[var], data: { "sage--variables-target": "variable" } %>
82
+ <% end %>
83
+
84
+ <div style="margin-bottom: 5px; text-align: left;">
85
+ <small class="text-muted"><code>{{<%= date_vars.join('}}, {{') %>}}</code></small>
86
+ </div>
87
+ <div class="selectize-control single" style="width: 300px;">
88
+ <div id="reportrange" class="selectize-input" style="display: inline-block;">
89
+ <span>Select a time range</span>
90
+ </div>
91
+ </div>
92
+
93
+ <%= javascript_tag nonce: true do %>
94
+ function dateStr(daysAgo) {
95
+ return now.clone().subtract(daysAgo || 0, "days").format(format)
96
+ }
97
+
98
+ function setTimeInputs(start, end) {
99
+ $("#start_time").val(toDate(start).utc().format())
100
+ $("#end_time").val(toDate(end).endOf("day").utc().format())
101
+ }
102
+
103
+ $("#reportrange").daterangepicker(
104
+ {
105
+ ranges: {
106
+ "Today": [dateStr(), dateStr()],
107
+ "Last 7 Days": [dateStr(6), dateStr()],
108
+ "Last 30 Days": [dateStr(29), dateStr()]
109
+ },
110
+ locale: {
111
+ format: format
112
+ },
113
+ startDate: dateStr(29),
114
+ endDate: dateStr(),
115
+ opens: "right",
116
+ alwaysShowCalendars: true
117
+ },
118
+ function(start, end) {
119
+ setTimeInputs(start, end)
120
+
121
+ // Trigger our custom variable controller instead of submitIfCompleted
122
+ var controller = document.querySelector('[data-controller*="sage--variables"]')
123
+ if (controller && window.Stimulus) {
124
+ var controllerInstance = window.Stimulus.getControllerForElementAndIdentifier(controller, 'sage--variables')
125
+ if (controllerInstance) {
126
+ controllerInstance.triggerSubmit()
127
+ }
128
+ }
129
+ }
130
+ ).on("apply.daterangepicker", function(ev, picker) {
131
+ setTimeInputs(picker.startDate, picker.endDate)
132
+ $("#reportrange span").html(toDate(picker.startDate).format("MMMM D, YYYY") + " - " + toDate(picker.endDate).format("MMMM D, YYYY"))
133
+
134
+ // Trigger our custom variable controller
135
+ var controller = document.querySelector('[data-controller*="sage--variables"]')
136
+ if (controller && window.Stimulus) {
137
+ var controllerInstance = window.Stimulus.getControllerForElementAndIdentifier(controller, 'sage--variables')
138
+ if (controllerInstance) {
139
+ controllerInstance.handleDateRangeChange(document.getElementById('start_time'), picker)
140
+ }
141
+ }
142
+ })
143
+
144
+ if ($("#start_time").val().length > 0) {
145
+ var picker = $("#reportrange").data("daterangepicker")
146
+ picker.setStartDate(moment.tz($("#start_time").val(), timeZone))
147
+ picker.setEndDate(moment.tz($("#end_time").val(), timeZone))
148
+ $("#reportrange").trigger("apply.daterangepicker", picker)
149
+ } else {
150
+ var picker = $("#reportrange").data("daterangepicker")
151
+ $("#reportrange").trigger("apply.daterangepicker", picker)
152
+
153
+ // Initial trigger for our custom controller
154
+ var controller = document.querySelector('[data-controller*="sage--variables"]')
155
+ if (controller && window.Stimulus) {
156
+ var controllerInstance = window.Stimulus.getControllerForElementAndIdentifier(controller, 'sage--variables')
157
+ if (controllerInstance) {
158
+ controllerInstance.triggerSubmit()
159
+ }
160
+ }
161
+ }
162
+ <% end %>
163
+ <% end %>
164
+
165
+ <button type='submit' class='btn btn-primary'>Run</button>
166
+ </form>
167
+ </div>
168
+ <% end %>
@@ -22,7 +22,7 @@
22
22
  <% end %>
23
23
 
24
24
  <% if @bind_vars.any? %>
25
- <%= render partial: "blazer/variables", locals: {action: dashboard_path(@dashboard)} %>
25
+ <%= render partial: "sage/variables", locals: {action: dashboard_path(@dashboard)} %>
26
26
  <% else %>
27
27
  <div style="padding-bottom: 15px;"></div>
28
28
  <% end %>
@@ -28,12 +28,12 @@
28
28
  <p style="white-space: pre-line;"><%= @query.description %></p>
29
29
  <% end %>
30
30
 
31
- <%= render partial: "blazer/variables", locals: { action: query_path(@query) } %>
31
+ <%= render partial: "sage/variables", locals: { action: query_path(@query) } %>
32
32
 
33
33
  <pre id="code"><code><%= @statement.display_statement %></code></pre>
34
34
 
35
35
  <% if @success %>
36
- <%= turbo_frame_tag dom_id(@query, 'results'), src: run_query_path(@query.id, from_show: true) do %>
36
+ <%= turbo_frame_tag dom_id(@query, 'results'), src: run_query_path(@query.id, from_show: true, variables: variable_params(@query)) do %>
37
37
  loading...
38
38
  <% end %>
39
39
  <% end %>
@@ -1,17 +1,17 @@
1
- require 'ransack'
1
+ require "ransack"
2
2
 
3
3
  # Configure Ransack for Blazer models
4
4
  Rails.application.config.after_initialize do
5
5
  # Ensure Blazer is loaded first
6
- require 'blazer' if defined?(Blazer)
7
-
6
+ require "blazer" if defined?(Blazer)
7
+
8
8
  # Extend Blazer::Query with Ransack capabilities
9
9
  if defined?(Blazer::Query)
10
10
  # First, ensure Ransack is included in the model
11
11
  unless Blazer::Query.respond_to?(:ransack)
12
12
  Blazer::Query.send(:extend, Ransack::Adapters::ActiveRecord::Base)
13
13
  end
14
-
14
+
15
15
  Blazer::Query.class_eval do
16
16
  # Define which attributes can be searched
17
17
  def self.ransackable_attributes(auth_object = nil)
@@ -21,7 +21,7 @@ Rails.application.config.after_initialize do
21
21
  # Define which associations can be searched
22
22
  def self.ransackable_associations(auth_object = nil)
23
23
  associations = %w[checks audits dashboard_queries dashboards]
24
- associations << 'creator' if Blazer.user_class
24
+ associations << "creator" if Blazer.user_class
25
25
  associations
26
26
  end
27
27
 
@@ -37,14 +37,14 @@ Rails.application.config.after_initialize do
37
37
  end
38
38
  end
39
39
  end
40
-
40
+
41
41
  # Extend Blazer::Dashboard with Ransack capabilities
42
42
  if defined?(Blazer::Dashboard)
43
43
  # First, ensure Ransack is included in the model
44
44
  unless Blazer::Dashboard.respond_to?(:ransack)
45
45
  Blazer::Dashboard.send(:extend, Ransack::Adapters::ActiveRecord::Base)
46
46
  end
47
-
47
+
48
48
  Blazer::Dashboard.class_eval do
49
49
  # Define which attributes can be searched
50
50
  def self.ransackable_attributes(auth_object = nil)
@@ -54,7 +54,7 @@ Rails.application.config.after_initialize do
54
54
  # Define which associations can be searched
55
55
  def self.ransackable_associations(auth_object = nil)
56
56
  associations = %w[dashboard_queries queries]
57
- associations << 'creator' if Blazer.user_class
57
+ associations << "creator" if Blazer.user_class
58
58
  associations
59
59
  end
60
60
  end
@@ -68,7 +68,7 @@ Rails.application.config.to_prepare do
68
68
  unless Blazer::Query.respond_to?(:ransack)
69
69
  Blazer::Query.send(:extend, Ransack::Adapters::ActiveRecord::Base)
70
70
  end
71
-
71
+
72
72
  unless Blazer::Query.respond_to?(:ransackable_attributes)
73
73
  Blazer::Query.class_eval do
74
74
  def self.ransackable_attributes(auth_object = nil)
@@ -85,13 +85,13 @@ Rails.application.config.to_prepare do
85
85
  end
86
86
  end
87
87
  end
88
-
88
+
89
89
  if defined?(Blazer::Dashboard)
90
90
  # Ensure Ransack is included
91
91
  unless Blazer::Dashboard.respond_to?(:ransack)
92
92
  Blazer::Dashboard.send(:extend, Ransack::Adapters::ActiveRecord::Base)
93
93
  end
94
-
94
+
95
95
  unless Blazer::Dashboard.respond_to?(:ransackable_attributes)
96
96
  Blazer::Dashboard.class_eval do
97
97
  def self.ransackable_attributes(auth_object = nil)
@@ -100,19 +100,19 @@ Rails.application.config.to_prepare do
100
100
 
101
101
  def self.ransackable_associations(auth_object = nil)
102
102
  associations = %w[dashboard_queries queries]
103
- associations << 'creator' if Blazer.user_class
103
+ associations << "creator" if Blazer.user_class
104
104
  associations
105
105
  end
106
106
  end
107
107
  end
108
108
  end
109
-
109
+
110
110
  if defined?(Blazer::Check)
111
111
  # Ensure Ransack is included
112
112
  unless Blazer::Check.respond_to?(:ransack)
113
113
  Blazer::Check.send(:extend, Ransack::Adapters::ActiveRecord::Base)
114
114
  end
115
-
115
+
116
116
  unless Blazer::Check.respond_to?(:ransackable_attributes)
117
117
  Blazer::Check.class_eval do
118
118
  def self.ransackable_attributes(auth_object = nil)
@@ -121,20 +121,20 @@ Rails.application.config.to_prepare do
121
121
 
122
122
  def self.ransackable_associations(auth_object = nil)
123
123
  associations = %w[query]
124
- associations << 'creator' if Blazer.user_class
124
+ associations << "creator" if Blazer.user_class
125
125
  associations
126
126
  end
127
127
  end
128
128
  end
129
129
  end
130
-
130
+
131
131
  # Extend Blazer::Check with Ransack capabilities
132
132
  if defined?(Blazer::Check)
133
133
  # First, ensure Ransack is included in the model
134
134
  unless Blazer::Check.respond_to?(:ransack)
135
135
  Blazer::Check.send(:extend, Ransack::Adapters::ActiveRecord::Base)
136
136
  end
137
-
137
+
138
138
  Blazer::Check.class_eval do
139
139
  # Define which attributes can be searched
140
140
  def self.ransackable_attributes(auth_object = nil)
@@ -144,9 +144,9 @@ Rails.application.config.to_prepare do
144
144
  # Define which associations can be searched
145
145
  def self.ransackable_associations(auth_object = nil)
146
146
  associations = %w[query]
147
- associations << 'creator' if Blazer.user_class
147
+ associations << "creator" if Blazer.user_class
148
148
  associations
149
149
  end
150
150
  end
151
151
  end
152
- end
152
+ end
data/config/routes.rb CHANGED
@@ -5,7 +5,7 @@ Sage::Engine.routes.draw do
5
5
  post :refresh, on: :member
6
6
  get :run, on: :member
7
7
 
8
- resources :messages, only: [:index, :create], controller: 'queries/messages'
8
+ resources :messages, only: [ :index, :create ], controller: "queries/messages"
9
9
 
10
10
  collection do
11
11
  post :run
@@ -1,22 +1,8 @@
1
1
  Sage.configure do |config|
2
2
  # Configure the AI provider (options: :anthropic, :openai)
3
3
  config.provider = :anthropic
4
+ # config.provider = :openai
4
5
 
5
- # API Key Configuration
6
- # Priority order:
7
- # 1. Rails credentials: rails credentials:edit
8
- # anthropic:
9
- # api_key: your_key_here
10
- # openai:
11
- # api_key: your_key_here
12
- # 2. .env file: ANTHROPIC_API_KEY=your_key_here or OPENAI_API_KEY=your_key_here
13
- # 3. Direct configuration (not recommended for production):
14
- # config.anthropic_api_key = "your_key_here"
15
- # config.open_ai_key = "your_key_here"
16
-
17
- # Model selection (optional)
18
- # For Anthropic (defaults to claude-3-opus-20240229):
19
- # config.anthropic_model = "claude-3-sonnet-20240229"
20
- # For OpenAI (defaults to gpt-4):
21
- # config.open_ai_model = "gpt-3.5-turbo"
6
+ config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
7
+ # config.openai_api_key = ENV["OPENAI_API_KEY"]
22
8
  end
@@ -43,20 +43,20 @@ module Sage
43
43
 
44
44
  def collect_models_with_scopes
45
45
  models_with_scopes = []
46
-
46
+
47
47
  # Find all model files in the app/models directory
48
48
  model_files = Dir.glob(Rails.root.join("app/models/**/*.rb"))
49
-
49
+
50
50
  model_files.each do |file_path|
51
51
  # Skip concern files and other non-model files
52
52
  next if file_path.include?("/concerns/")
53
-
53
+
54
54
  # Read the file content
55
55
  file_content = File.read(file_path)
56
-
56
+
57
57
  # Extract model name from file path
58
58
  model_name = File.basename(file_path, ".rb").camelize
59
-
59
+
60
60
  # Find all scope definitions using regex
61
61
  # Match various scope patterns:
62
62
  # scope :active, -> { where(active: true) }
@@ -70,7 +70,7 @@ module Sage
70
70
  # Pattern 3: Simple one-liner scopes
71
71
  /scope\s+:(\w+)\s*,\s*(.+?)$/
72
72
  ]
73
-
73
+
74
74
  scope_matches = []
75
75
  scope_patterns.each do |pattern|
76
76
  matches = file_content.scan(pattern)
@@ -79,11 +79,11 @@ module Sage
79
79
  scope_body = match[1] || ""
80
80
  # Avoid duplicate entries
81
81
  unless scope_matches.any? { |s| s[0] == scope_name }
82
- scope_matches << [scope_name, scope_body]
82
+ scope_matches << [ scope_name, scope_body ]
83
83
  end
84
84
  end
85
85
  end
86
-
86
+
87
87
  if scope_matches.any?
88
88
  # Try to get the actual model class and table name
89
89
  begin
@@ -93,17 +93,17 @@ module Sage
93
93
  table_name = model_name.tableize
94
94
  model_class = nil
95
95
  end
96
-
96
+
97
97
  model_info = {
98
98
  name: model_name,
99
99
  table: table_name,
100
100
  scopes: []
101
101
  }
102
-
102
+
103
103
  scope_matches.each do |match|
104
104
  scope_name = match[0]
105
105
  scope_body = match[1]
106
-
106
+
107
107
  if scope_body
108
108
  # Try to extract SQL-like patterns from the scope body
109
109
  # Look for where conditions, joins, etc.
@@ -114,20 +114,20 @@ module Sage
114
114
  model_info[:scopes] << " • `#{scope_name}` → (check model file for implementation)"
115
115
  end
116
116
  end
117
-
117
+
118
118
  models_with_scopes << model_info if model_info[:scopes].any?
119
119
  end
120
120
  end
121
121
 
122
122
  models_with_scopes
123
123
  end
124
-
124
+
125
125
  def extract_sql_from_scope_body(scope_body)
126
126
  # Clean up the scope body
127
127
  cleaned = scope_body.strip
128
-
128
+
129
129
  sql_parts = []
130
-
130
+
131
131
  # Extract WHERE conditions
132
132
  if cleaned =~ /where\s*\(["']([^"']+)["'](?:,\s*(.+?))?\)/
133
133
  # String SQL with potential parameters
@@ -146,14 +146,14 @@ module Sage
146
146
  not_conditions = not_conditions.gsub(/(\w+):\s*/, '\1 != ')
147
147
  sql_parts << "WHERE NOT (#{not_conditions})"
148
148
  end
149
-
149
+
150
150
  # Extract JOINs
151
151
  if cleaned =~ /joins?\s*\(:?(\w+)\)/
152
152
  sql_parts << "JOIN #{$1}"
153
153
  elsif cleaned =~ /includes?\s*\(:?(\w+)\)/
154
154
  sql_parts << "LEFT JOIN #{$1}"
155
155
  end
156
-
156
+
157
157
  # Extract ORDER
158
158
  if cleaned =~ /order\s*\(["']([^"']+)["']\)/
159
159
  sql_parts << "ORDER BY #{$1}"
@@ -162,12 +162,12 @@ module Sage
162
162
  order_clause = order_clause.gsub(/(\w+):\s*:?(asc|desc)/i, '\1 \2')
163
163
  sql_parts << "ORDER BY #{order_clause}"
164
164
  end
165
-
165
+
166
166
  # Extract LIMIT
167
167
  if cleaned =~ /limit\s*\((\d+)\)/
168
168
  sql_parts << "LIMIT #{$1}"
169
169
  end
170
-
170
+
171
171
  # If we found SQL parts, join them
172
172
  if sql_parts.any?
173
173
  sql_parts.join(" ")
data/lib/sage/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Sage
2
- VERSION = "0.0.5"
2
+ VERSION = "0.0.7"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sage-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Jones
@@ -140,6 +140,7 @@ executables: []
140
140
  extensions: []
141
141
  extra_rdoc_files: []
142
142
  files:
143
+ - CONTRIBUTING.md
143
144
  - README.md
144
145
  - app/assets/images/chevron-down-zinc-500.svg
145
146
  - app/assets/images/chevron-right.svg
@@ -165,6 +166,7 @@ files:
165
166
  - app/javascript/sage/controllers/reverse_infinite_scroll_controller.js
166
167
  - app/javascript/sage/controllers/search_controller.js
167
168
  - app/javascript/sage/controllers/select_controller.js
169
+ - app/javascript/sage/controllers/variables_controller.js
168
170
  - app/jobs/sage/application_job.rb
169
171
  - app/jobs/sage/process_report_job.rb
170
172
  - app/mailers/sage/application_mailer.rb
@@ -173,6 +175,7 @@ files:
173
175
  - app/schemas/sage/report_response_schema.rb
174
176
  - app/views/layouts/application.html.erb
175
177
  - app/views/layouts/sage/application.html.erb
178
+ - app/views/sage/_variables.html.erb
176
179
  - app/views/sage/checks/_form.html.erb
177
180
  - app/views/sage/checks/_search.html.erb
178
181
  - app/views/sage/checks/edit.html.erb