sage-rails 0.0.3

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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +202 -0
  3. data/app/assets/images/chevron-down-zinc-500.svg +1 -0
  4. data/app/assets/images/chevron-right.svg +1 -0
  5. data/app/assets/images/loading.svg +4 -0
  6. data/app/assets/images/sage/chevron-down-zinc-500.svg +1 -0
  7. data/app/assets/images/sage/chevron-right.svg +1 -0
  8. data/app/assets/images/sage/loading.svg +4 -0
  9. data/app/assets/javascripts/sage/application.js +18 -0
  10. data/app/assets/stylesheets/sage/application.css +308 -0
  11. data/app/controllers/sage/actions_controller.rb +5 -0
  12. data/app/controllers/sage/application_controller.rb +4 -0
  13. data/app/controllers/sage/base_controller.rb +10 -0
  14. data/app/controllers/sage/checks_controller.rb +65 -0
  15. data/app/controllers/sage/dashboards_controller.rb +130 -0
  16. data/app/controllers/sage/queries/messages_controller.rb +62 -0
  17. data/app/controllers/sage/queries_controller.rb +596 -0
  18. data/app/helpers/sage/application_helper.rb +30 -0
  19. data/app/helpers/sage/queries_helper.rb +23 -0
  20. data/app/javascript/controllers/element_removal_controller.js +7 -0
  21. data/app/javascript/sage/controllers/clipboard_controller.js +26 -0
  22. data/app/javascript/sage/controllers/dashboard_controller.js +132 -0
  23. data/app/javascript/sage/controllers/reverse_infinite_scroll_controller.js +146 -0
  24. data/app/javascript/sage/controllers/search_controller.js +47 -0
  25. data/app/javascript/sage/controllers/select_controller.js +215 -0
  26. data/app/javascript/sage.js +19 -0
  27. data/app/jobs/sage/application_job.rb +4 -0
  28. data/app/jobs/sage/process_report_job.rb +80 -0
  29. data/app/mailers/sage/application_mailer.rb +6 -0
  30. data/app/models/sage/application_record.rb +5 -0
  31. data/app/models/sage/message.rb +8 -0
  32. data/app/schemas/sage/report_response_schema.rb +8 -0
  33. data/app/views/layouts/application.html.erb +34 -0
  34. data/app/views/layouts/sage/application.html.erb +94 -0
  35. data/app/views/sage/checks/_form.html.erb +81 -0
  36. data/app/views/sage/checks/_search.html.erb +8 -0
  37. data/app/views/sage/checks/edit.html.erb +10 -0
  38. data/app/views/sage/checks/index.html.erb +58 -0
  39. data/app/views/sage/checks/new.html.erb +8 -0
  40. data/app/views/sage/dashboards/_form.html.erb +50 -0
  41. data/app/views/sage/dashboards/_search.html.erb +8 -0
  42. data/app/views/sage/dashboards/index.html.erb +58 -0
  43. data/app/views/sage/dashboards/new.html.erb +8 -0
  44. data/app/views/sage/dashboards/show.html.erb +58 -0
  45. data/app/views/sage/messages/_form.html.erb +14 -0
  46. data/app/views/sage/queries/_caching.html.erb +17 -0
  47. data/app/views/sage/queries/_form.html.erb +72 -0
  48. data/app/views/sage/queries/_input.html.erb +17 -0
  49. data/app/views/sage/queries/_message.html.erb +25 -0
  50. data/app/views/sage/queries/_message.turbo_stream.erb +10 -0
  51. data/app/views/sage/queries/_new_form.html.erb +43 -0
  52. data/app/views/sage/queries/_run.html.erb +232 -0
  53. data/app/views/sage/queries/_search.html.erb +8 -0
  54. data/app/views/sage/queries/_statement_box.html.erb +241 -0
  55. data/app/views/sage/queries/_streaming_message.html.erb +14 -0
  56. data/app/views/sage/queries/create.turbo_stream.erb +114 -0
  57. data/app/views/sage/queries/edit.html.erb +48 -0
  58. data/app/views/sage/queries/index.html.erb +59 -0
  59. data/app/views/sage/queries/messages/create.turbo_stream.erb +22 -0
  60. data/app/views/sage/queries/messages/index.html.erb +44 -0
  61. data/app/views/sage/queries/messages/index.turbo_stream.erb +15 -0
  62. data/app/views/sage/queries/new.html.erb +195 -0
  63. data/app/views/sage/queries/run.html.erb +1 -0
  64. data/app/views/sage/queries/run.turbo_stream.erb +3 -0
  65. data/app/views/sage/queries/show.html.erb +49 -0
  66. data/app/views/sage/queries/table_schema.html.erb +77 -0
  67. data/app/views/sage/shared/_navigation.html.erb +26 -0
  68. data/app/views/sage/shared/_overlay.html.erb +11 -0
  69. data/config/importmap.rb +11 -0
  70. data/config/initializers/pagy.rb +2 -0
  71. data/config/initializers/ransack.rb +152 -0
  72. data/config/routes.rb +31 -0
  73. data/lib/generators/sage/USAGE +13 -0
  74. data/lib/generators/sage/install/install_generator.rb +128 -0
  75. data/lib/generators/sage/install/templates/sage.rb +22 -0
  76. data/lib/sage/database_schema_context.rb +56 -0
  77. data/lib/sage/engine.rb +260 -0
  78. data/lib/sage/model_scopes_context.rb +185 -0
  79. data/lib/sage/report_processor.rb +263 -0
  80. data/lib/sage/version.rb +3 -0
  81. data/lib/sage.rb +25 -0
  82. data/lib/tasks/sage_tasks.rake +4 -0
  83. metadata +245 -0
@@ -0,0 +1,130 @@
1
+ module Sage
2
+ class DashboardsController < BaseController
3
+ before_action :set_dashboard, only: [:show, :edit, :update, :destroy, :refresh]
4
+
5
+ def index
6
+ @q = Blazer::Dashboard.ransack(params[:q])
7
+ @dashboards = @q.result
8
+
9
+ # Only include creator if Blazer.user_class is configured
10
+ @dashboards = @dashboards.includes(:creator) if Blazer.user_class
11
+
12
+ @dashboards = @dashboards.order(:name)
13
+
14
+ # Apply additional filters if needed
15
+ if blazer_user && params[:filter] == "mine"
16
+ @dashboards = @dashboards.where(creator_id: blazer_user.id).reorder(updated_at: :desc)
17
+ end
18
+
19
+ # Apply pagination with Pagy
20
+ @pagy, @dashboards = pagy(@dashboards)
21
+ end
22
+
23
+ def new
24
+ @dashboard = Blazer::Dashboard.new
25
+ end
26
+
27
+ def create
28
+ @dashboard = Blazer::Dashboard.new
29
+ # use creator_id instead of creator
30
+ # since we setup association without checking if column exists
31
+ @dashboard.creator = blazer_user if @dashboard.respond_to?(:creator_id=) && blazer_user
32
+
33
+ if update_dashboard(@dashboard)
34
+ redirect_to dashboard_path(@dashboard)
35
+ else
36
+ render_errors @dashboard
37
+ end
38
+ end
39
+
40
+ def show
41
+ @queries = @dashboard.dashboard_queries.order(:position).preload(:query).map(&:query)
42
+ @query_errors = {}
43
+
44
+ @queries.each do |query|
45
+ # Check if the query has a valid data source
46
+ if query.data_source.blank? || !Blazer.data_sources.key?(query.data_source)
47
+ @query_errors[query.id] = "Invalid or missing data source: #{query.data_source.inspect}"
48
+ else
49
+ @success = process_vars(query.statement_object)
50
+ end
51
+ end
52
+ @bind_vars ||= []
53
+
54
+ @smart_vars = {}
55
+ @sql_errors = []
56
+ @data_sources = @queries.map { |q|
57
+ # Use the query's data source if specified, otherwise use the default
58
+ source = q.data_source.presence || Blazer.data_sources.keys.first
59
+ Blazer.data_sources[source]
60
+ }.compact.uniq
61
+
62
+ @bind_vars.each do |var|
63
+ @data_sources.each do |data_source|
64
+ smart_var, error = parse_smart_variables(var, data_source)
65
+ ((@smart_vars[var] ||= []).concat(smart_var)).uniq! if smart_var
66
+ @sql_errors << error if error
67
+ end
68
+ end
69
+
70
+ add_cohort_analysis_vars if @queries.any?(&:cohort_analysis?)
71
+ end
72
+
73
+ def edit
74
+ end
75
+
76
+ def update
77
+ if update_dashboard(@dashboard)
78
+ redirect_to dashboard_path(@dashboard, params: variable_params(@dashboard))
79
+ else
80
+ render_errors @dashboard
81
+ end
82
+ end
83
+
84
+ def destroy
85
+ @dashboard.destroy
86
+ redirect_to root_path
87
+ end
88
+
89
+ def refresh
90
+ @dashboard.queries.each do |query|
91
+ refresh_query(query)
92
+ end
93
+ redirect_to dashboard_path(@dashboard, params: variable_params(@dashboard))
94
+ end
95
+
96
+ private
97
+
98
+ def dashboard_params
99
+ params.require(:dashboard).permit(:name)
100
+ end
101
+
102
+ def set_dashboard
103
+ @dashboard = Blazer::Dashboard.find(params[:id])
104
+ end
105
+
106
+ def update_dashboard(dashboard)
107
+ dashboard.assign_attributes(dashboard_params)
108
+ Blazer::Dashboard.transaction do
109
+ if params[:query_ids].is_a?(Array)
110
+ query_ids = params[:query_ids].map(&:to_i)
111
+ @queries = Blazer::Query.find(query_ids).sort_by { |q| query_ids.index(q.id) }
112
+ end
113
+ if dashboard.save
114
+ if @queries
115
+ @queries.each_with_index do |query, i|
116
+ dashboard_query = dashboard.dashboard_queries.where(query_id: query.id).first_or_initialize
117
+ dashboard_query.position = i
118
+ dashboard_query.save!
119
+ end
120
+ if dashboard.persisted?
121
+ dashboard.dashboard_queries.where.not(query_id: query_ids).destroy_all
122
+ end
123
+ end
124
+ true
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+
@@ -0,0 +1,62 @@
1
+
2
+ module Sage
3
+ module Queries
4
+ class MessagesController < BaseController
5
+ before_action :set_query
6
+
7
+ def index
8
+ page = params[:page] || 1
9
+
10
+ # For reverse infinite scroll, we want:
11
+ # Page 1: Most recent messages (to show at bottom initially)
12
+ # Page 2: Older messages (to prepend when scrolling up)
13
+ # Page 3: Even older messages, etc.
14
+
15
+ base_query = @query.messages.order(created_at: :desc)
16
+ @pagy, messages = pagy(base_query, page: page, overflow: :last_page)
17
+
18
+ # For page 1 (initial load), show newest messages in chronological order (oldest first)
19
+ # For page 2+, keep reverse chronological order for prepending
20
+ if page.to_i == 1
21
+ @messages = messages.reverse # Show oldest first for initial display
22
+ else
23
+ @messages = messages # Keep newest first for prepending
24
+ end
25
+
26
+ respond_to do |format|
27
+ format.html # For turbo_frame initial load
28
+ format.turbo_stream # For infinite scroll pagination
29
+ end
30
+ end
31
+
32
+ def create
33
+ @message = @query.messages.create!(body: params[:statement])
34
+ creator_id = blazer_user.present? ? blazer_user.id : 0
35
+
36
+ @message.creator_id = creator_id
37
+ @message.save!
38
+ @message.reload
39
+
40
+ stream_target_id = SecureRandom.hex(8)
41
+
42
+ Sage::ProcessReportJob.perform_later(
43
+ @message.body,
44
+ query_id: @message.blazer_query.id,
45
+ stream_target_id:
46
+ )
47
+
48
+ # Check if we need a new day separator
49
+ @need_day_separator = @query.messages.where(
50
+ "DATE(created_at) = ?",
51
+ @message.created_at.to_date
52
+ ).count == 1
53
+ end
54
+
55
+ private
56
+
57
+ def set_query
58
+ @query = Blazer::Query.find(params[:query_id])
59
+ end
60
+ end
61
+ end
62
+ end