sage-rails 0.0.8 → 0.1.0
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 +4 -4
- data/app/assets/javascripts/sage/controllers/clipboard_controller.js +0 -1
- data/app/assets/javascripts/sage/controllers/dashboard_controller.js +1 -7
- data/app/assets/javascripts/sage/controllers/query_toggle_controller.js +22 -0
- data/app/assets/javascripts/sage/controllers/reverse_infinite_scroll_controller.js +0 -32
- data/app/assets/javascripts/sage/controllers/search_controller.js +0 -1
- data/app/assets/javascripts/sage/controllers/variables_controller.js +1 -13
- data/app/assets/javascripts/sage.js +3 -1
- data/app/controllers/sage/queries_controller.rb +2 -70
- data/app/javascript/sage/controllers/clipboard_controller.js +0 -1
- data/app/javascript/sage/controllers/dashboard_controller.js +0 -3
- data/app/javascript/sage/controllers/query_toggle_controller.js +24 -0
- data/app/javascript/sage/controllers/reverse_infinite_scroll_controller.js +0 -27
- data/app/javascript/sage/controllers/search_controller.js +0 -1
- data/app/javascript/sage/controllers/variables_controller.js +0 -11
- data/app/javascript/sage.js +3 -1
- data/app/views/sage/_variables.html.erb +64 -48
- data/app/views/sage/dashboards/index.html.erb +1 -1
- data/app/views/sage/queries/_new_form.html.erb +19 -5
- data/app/views/sage/queries/_statement_box.html.erb +39 -18
- data/app/views/sage/queries/messages/index.html.erb +0 -2
- data/app/views/sage/queries/new.html.erb +188 -60
- data/app/views/sage/queries/show.html.erb +10 -3
- data/config/importmap.rb +2 -0
- data/lib/sage/engine.rb +2 -1
- data/lib/sage/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc7c4334ddc11b1353e8ad25985adb53a4b7cff4ddc03a6f1e6304773f0a3e52
|
4
|
+
data.tar.gz: b1c9ee4ea70e918eb1592ae56e0f596edf5e7b08670b8769ca8347d1d2cf7ab0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64b12cbfa6ac68335f01a13e0c40d08caf9bbc66ce0a160c8eb277aea82303c43e3fdf4499356b13dcb0688fde80904800b1af752b63b402d966d10b7bf2d90b
|
7
|
+
data.tar.gz: 96e9c95b61d505d644859862f5866ebdca3decccb55b54ad03f609541a5f67779474c31f6a4ea688b0889db19494652cd4a82eb2d19d2874370c0d2415739613
|
@@ -6,16 +6,10 @@ export default class extends Controller {
|
|
6
6
|
static values = { queries: Array };
|
7
7
|
|
8
8
|
connect() {
|
9
|
-
console.log("Dashboard controller connected");
|
10
|
-
console.log("Queries value:", this.queriesValue);
|
11
|
-
|
12
9
|
// Parse the queries value if it's a string
|
13
10
|
const queriesData = typeof this.queriesValue === 'string' ?
|
14
11
|
JSON.parse(this.queriesValue) : this.queriesValue;
|
15
12
|
this.selectedQueries = Array.isArray(queriesData) ? [...queriesData] : [];
|
16
|
-
|
17
|
-
console.log("Selected queries:", this.selectedQueries);
|
18
|
-
|
19
13
|
this.render();
|
20
14
|
this.setupSortable();
|
21
15
|
}
|
@@ -129,4 +123,4 @@ export default class extends Controller {
|
|
129
123
|
}
|
130
124
|
}
|
131
125
|
}
|
132
|
-
}
|
126
|
+
}
|
@@ -0,0 +1,22 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["content", "icon"]
|
5
|
+
|
6
|
+
toggle() {
|
7
|
+
try {
|
8
|
+
const content = this.contentTarget
|
9
|
+
const icon = this.iconTarget
|
10
|
+
|
11
|
+
if (content.style.display === "none") {
|
12
|
+
content.style.display = "block"
|
13
|
+
icon.textContent = "visibility_off"
|
14
|
+
} else {
|
15
|
+
content.style.display = "none"
|
16
|
+
icon.textContent = "visibility"
|
17
|
+
}
|
18
|
+
} catch (error) {
|
19
|
+
console.error("Error in toggle:", error)
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
@@ -1,7 +1,5 @@
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
2
2
|
|
3
|
-
console.log("Reverse infinite scroll controller file loaded!");
|
4
|
-
|
5
3
|
export default class extends Controller {
|
6
4
|
static targets = ["entries", "pagination"];
|
7
5
|
static values = {
|
@@ -11,12 +9,10 @@ export default class extends Controller {
|
|
11
9
|
};
|
12
10
|
|
13
11
|
initialize() {
|
14
|
-
console.log("Reverse infinite scroll controller initialized");
|
15
12
|
this.intersectionObserver = new IntersectionObserver(
|
16
13
|
(entries) => {
|
17
14
|
entries.forEach((entry) => {
|
18
15
|
if (entry.isIntersecting) {
|
19
|
-
console.log("Pagination target is visible - loading older messages");
|
20
16
|
this.loadOlder();
|
21
17
|
}
|
22
18
|
});
|
@@ -29,33 +25,24 @@ export default class extends Controller {
|
|
29
25
|
}
|
30
26
|
|
31
27
|
connect() {
|
32
|
-
console.log("Reverse infinite scroll controller connected");
|
33
|
-
console.log("Initial page value:", this.pageValue);
|
34
|
-
|
35
28
|
if (this.hasPaginationTarget) {
|
36
|
-
console.log("Found pagination target, observing...");
|
37
29
|
this.intersectionObserver.observe(this.paginationTarget);
|
38
30
|
} else {
|
39
|
-
console.log("No pagination target found");
|
40
31
|
}
|
41
32
|
}
|
42
33
|
|
43
34
|
paginationTargetConnected(target) {
|
44
|
-
console.log("Pagination target connected dynamically, observing...");
|
45
35
|
// Add a delay to allow scroll-to-bottom to happen first
|
46
36
|
setTimeout(() => {
|
47
|
-
console.log("Starting to observe pagination target after delay");
|
48
37
|
this.intersectionObserver.observe(target);
|
49
38
|
}, 1000);
|
50
39
|
}
|
51
40
|
|
52
41
|
paginationTargetDisconnected(target) {
|
53
|
-
console.log("Pagination target disconnected, stop observing...");
|
54
42
|
this.intersectionObserver.unobserve(target);
|
55
43
|
}
|
56
44
|
|
57
45
|
disconnect() {
|
58
|
-
console.log("Reverse infinite scroll controller disconnected");
|
59
46
|
this.intersectionObserver.disconnect();
|
60
47
|
}
|
61
48
|
|
@@ -67,15 +54,11 @@ export default class extends Controller {
|
|
67
54
|
|
68
55
|
async loadOlder() {
|
69
56
|
if (this.loadingValue) {
|
70
|
-
console.log("Already loading, skipping...");
|
71
57
|
return;
|
72
58
|
}
|
73
59
|
|
74
|
-
console.log("Loading older messages...");
|
75
|
-
console.log("Current pageValue:", this.pageValue);
|
76
60
|
this.loadingValue = true;
|
77
61
|
const nextPage = this.pageValue + 1;
|
78
|
-
console.log("Requesting page:", nextPage);
|
79
62
|
|
80
63
|
// Store current scroll position
|
81
64
|
const oldScrollHeight = this.element.scrollHeight;
|
@@ -83,7 +66,6 @@ export default class extends Controller {
|
|
83
66
|
|
84
67
|
try {
|
85
68
|
const url = this.buildUrl(nextPage);
|
86
|
-
console.log("Fetching URL:", url);
|
87
69
|
|
88
70
|
const response = await fetch(url, {
|
89
71
|
headers: {
|
@@ -96,7 +78,6 @@ export default class extends Controller {
|
|
96
78
|
}
|
97
79
|
|
98
80
|
const html = await response.text();
|
99
|
-
console.log("Received response for page", nextPage);
|
100
81
|
|
101
82
|
// Parse the turbo-stream content
|
102
83
|
const parser = new DOMParser();
|
@@ -112,7 +93,6 @@ export default class extends Controller {
|
|
112
93
|
// Manually prepend the template content (for reverse infinite scroll)
|
113
94
|
this.entriesTarget.insertAdjacentHTML("afterbegin", template.innerHTML);
|
114
95
|
this.pageValue = nextPage;
|
115
|
-
console.log(`Updated to page ${nextPage}`);
|
116
96
|
|
117
97
|
// Maintain scroll position (prevent jumping to top)
|
118
98
|
setTimeout(() => {
|
@@ -120,25 +100,13 @@ export default class extends Controller {
|
|
120
100
|
const heightDifference = newScrollHeight - oldScrollHeight;
|
121
101
|
this.element.scrollTop = oldScrollTop + heightDifference;
|
122
102
|
|
123
|
-
console.log("Scroll position updated:", {
|
124
|
-
oldScrollHeight,
|
125
|
-
newScrollHeight,
|
126
|
-
heightDifference,
|
127
|
-
newScrollTop: this.element.scrollTop
|
128
|
-
});
|
129
|
-
|
130
103
|
// Set up observer for new pagination target if it exists
|
131
104
|
if (this.hasPaginationTarget) {
|
132
|
-
console.log("Found new pagination target, observing...");
|
133
105
|
this.intersectionObserver.observe(this.paginationTarget);
|
134
|
-
} else {
|
135
|
-
console.log("No more pages to load");
|
136
106
|
}
|
137
107
|
}, 50);
|
138
108
|
}
|
139
109
|
} catch (error) {
|
140
|
-
console.error("Error loading older messages:", error);
|
141
|
-
console.log("Staying on page", this.pageValue, "due to error");
|
142
110
|
} finally {
|
143
111
|
this.loadingValue = false;
|
144
112
|
}
|
@@ -32,7 +32,6 @@ export default class extends Controller {
|
|
32
32
|
|
33
33
|
handleVariableChange(event) {
|
34
34
|
const input = event.target
|
35
|
-
console.log(`Variable ${input.name} changed to:`, input.value)
|
36
35
|
|
37
36
|
// Check if all required variables are filled and submit if so
|
38
37
|
setTimeout(() => {
|
@@ -41,8 +40,6 @@ export default class extends Controller {
|
|
41
40
|
}
|
42
41
|
|
43
42
|
handleDateRangeChange(input, picker) {
|
44
|
-
console.log(`Date range updated for ${input.name}:`, input.value)
|
45
|
-
|
46
43
|
// Force update any related hidden fields
|
47
44
|
this.updateRelatedDateFields(input, picker)
|
48
45
|
|
@@ -83,16 +80,10 @@ export default class extends Controller {
|
|
83
80
|
// More robust empty check
|
84
81
|
if (this.isEmpty(value)) {
|
85
82
|
completed = false
|
86
|
-
console.log(`Variable ${input.name} is empty:`, value)
|
87
|
-
} else {
|
88
|
-
console.log(`Variable ${input.name} has value:`, value)
|
89
83
|
}
|
90
84
|
})
|
91
85
|
|
92
|
-
console.log(`Form completion check: ${completed ? 'Complete' : 'Incomplete'}`)
|
93
|
-
|
94
86
|
if (completed) {
|
95
|
-
console.log('Submitting form with all variables filled')
|
96
87
|
form.submit()
|
97
88
|
}
|
98
89
|
}
|
@@ -112,11 +103,8 @@ export default class extends Controller {
|
|
112
103
|
|
113
104
|
// Debug method to check current variable states
|
114
105
|
debugVariables() {
|
115
|
-
console.log("=== Current Variable States ===")
|
116
106
|
const inputs = this.formTarget.querySelectorAll('input[name], select[name]')
|
117
107
|
inputs.forEach(input => {
|
118
|
-
console.log(`${input.name}: "${input.value}" (${typeof input.value})`)
|
119
108
|
})
|
120
|
-
console.log("===============================")
|
121
109
|
}
|
122
|
-
}
|
110
|
+
}
|
@@ -5,9 +5,10 @@ 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
7
|
import VariablesController from "sage/controllers/variables_controller"
|
8
|
+
import QueryToggleController from "sage/controllers/query_toggle_controller"
|
8
9
|
|
9
10
|
// Export all Sage controllers for manual registration
|
10
|
-
export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController }
|
11
|
+
export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController, QueryToggleController }
|
11
12
|
|
12
13
|
// Register all Sage controllers with the provided Stimulus application
|
13
14
|
export function registerControllers(application) {
|
@@ -17,5 +18,6 @@ export function registerControllers(application) {
|
|
17
18
|
application.register("sage--dashboard", DashboardController)
|
18
19
|
application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
|
19
20
|
application.register("sage--variables", VariablesController)
|
21
|
+
application.register("sage--query-toggle", QueryToggleController)
|
20
22
|
}
|
21
23
|
|
@@ -96,6 +96,8 @@ module Sage
|
|
96
96
|
@query = Blazer::Query.new(query_params)
|
97
97
|
@query.creator = blazer_user if @query.respond_to?(:creator)
|
98
98
|
@query.status = "active" if @query.respond_to?(:status)
|
99
|
+
# Ensure data_source is set for SQL queries
|
100
|
+
@query.data_source ||= Blazer.data_sources.keys.first
|
99
101
|
|
100
102
|
if @query.save
|
101
103
|
redirect_to query_path(@query, params: variable_params(@query))
|
@@ -524,73 +526,3 @@ module Sage
|
|
524
526
|
end
|
525
527
|
end
|
526
528
|
end
|
527
|
-
#
|
528
|
-
# module Sage
|
529
|
-
# class QueriesController < ApplicationController
|
530
|
-
# before_action :set_data_source
|
531
|
-
#
|
532
|
-
# def new
|
533
|
-
# @query = Blazer::Query.new
|
534
|
-
# end
|
535
|
-
#
|
536
|
-
# def create
|
537
|
-
# @query = Blazer::Query.new(name: "Sage Query: #{Time.current}")
|
538
|
-
# @query.statement = generate_sql_from_question(query_params[:question])
|
539
|
-
# @query.creator = blazer_user if defined?(blazer_user)
|
540
|
-
#
|
541
|
-
# # Optionally save the query if configured
|
542
|
-
# if Sage.configuration.auto_save_queries && @query.statement.present?
|
543
|
-
# @query.save
|
544
|
-
# end
|
545
|
-
#
|
546
|
-
# # Store the question for display
|
547
|
-
# @question = query_params[:question]
|
548
|
-
#
|
549
|
-
# respond_to do |format|
|
550
|
-
# format.turbo_stream
|
551
|
-
# format.html { render :new }
|
552
|
-
# end
|
553
|
-
# end
|
554
|
-
#
|
555
|
-
# def run
|
556
|
-
# @query = Blazer::Query.new(statement: query_params[:sql])
|
557
|
-
#
|
558
|
-
# # Use Blazer's run method to execute the query
|
559
|
-
# @result = Blazer.data_sources[@data_source].run_statement(@query.statement)
|
560
|
-
#
|
561
|
-
# # Check for errors
|
562
|
-
# if @result.error.present?
|
563
|
-
# @error = @result.error
|
564
|
-
# end
|
565
|
-
#
|
566
|
-
# respond_to do |format|
|
567
|
-
# format.turbo_stream
|
568
|
-
# format.html
|
569
|
-
# end
|
570
|
-
# end
|
571
|
-
#
|
572
|
-
# private
|
573
|
-
#
|
574
|
-
# def set_data_source
|
575
|
-
# @data_source = params[:data_source] || Blazer.data_sources.keys.first
|
576
|
-
# end
|
577
|
-
#
|
578
|
-
# def query_params
|
579
|
-
# params.require(:query).permit(:question, :sql, :data_source)
|
580
|
-
# end
|
581
|
-
#
|
582
|
-
# def generate_sql_from_question(question)
|
583
|
-
# # Placeholder for AI integration
|
584
|
-
# # In production, this would call your AI service (OpenAI, Anthropic, etc.)
|
585
|
-
# "-- AI generated SQL for: #{question}\n" +
|
586
|
-
# "-- TODO: Integrate with AI service\n" +
|
587
|
-
# "SELECT 'Please configure AI service' as message;"
|
588
|
-
# end
|
589
|
-
#
|
590
|
-
# def blazer_user
|
591
|
-
# # Override this method to provide the current user
|
592
|
-
# # For example: current_user.email if using Devise
|
593
|
-
# "sage-user"
|
594
|
-
# end
|
595
|
-
# end
|
596
|
-
# end
|
@@ -6,15 +6,12 @@ export default class extends Controller {
|
|
6
6
|
static values = { queries: Array };
|
7
7
|
|
8
8
|
connect() {
|
9
|
-
console.log("Dashboard controller connected");
|
10
|
-
console.log("Queries value:", this.queriesValue);
|
11
9
|
|
12
10
|
// Parse the queries value if it's a string
|
13
11
|
const queriesData = typeof this.queriesValue === 'string' ?
|
14
12
|
JSON.parse(this.queriesValue) : this.queriesValue;
|
15
13
|
this.selectedQueries = Array.isArray(queriesData) ? [...queriesData] : [];
|
16
14
|
|
17
|
-
console.log("Selected queries:", this.selectedQueries);
|
18
15
|
|
19
16
|
this.render();
|
20
17
|
this.setupSortable();
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { Controller } from "@hotwired/stimulus";
|
2
|
+
|
3
|
+
export default class extends Controller {
|
4
|
+
static targets = ["content", "icon"]
|
5
|
+
|
6
|
+
toggle() {
|
7
|
+
try {
|
8
|
+
const content = this.contentTarget
|
9
|
+
const icon = this.iconTarget
|
10
|
+
|
11
|
+
const isHidden = window.getComputedStyle(content).display === "none"
|
12
|
+
|
13
|
+
if (isHidden) {
|
14
|
+
content.style.display = "block"
|
15
|
+
icon.textContent = "visibility_off"
|
16
|
+
} else {
|
17
|
+
content.style.display = "none"
|
18
|
+
icon.textContent = "visibility"
|
19
|
+
}
|
20
|
+
} catch (error) {
|
21
|
+
console.error("Error in toggle:", error)
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { Controller } from "@hotwired/stimulus";
|
2
2
|
|
3
|
-
console.log("Reverse infinite scroll controller file loaded!");
|
4
3
|
|
5
4
|
export default class extends Controller {
|
6
5
|
static targets = ["entries", "pagination"];
|
@@ -11,12 +10,10 @@ export default class extends Controller {
|
|
11
10
|
};
|
12
11
|
|
13
12
|
initialize() {
|
14
|
-
console.log("Reverse infinite scroll controller initialized");
|
15
13
|
this.intersectionObserver = new IntersectionObserver(
|
16
14
|
(entries) => {
|
17
15
|
entries.forEach((entry) => {
|
18
16
|
if (entry.isIntersecting) {
|
19
|
-
console.log("Pagination target is visible - loading older messages");
|
20
17
|
this.loadOlder();
|
21
18
|
}
|
22
19
|
});
|
@@ -29,33 +26,25 @@ export default class extends Controller {
|
|
29
26
|
}
|
30
27
|
|
31
28
|
connect() {
|
32
|
-
console.log("Reverse infinite scroll controller connected");
|
33
|
-
console.log("Initial page value:", this.pageValue);
|
34
29
|
|
35
30
|
if (this.hasPaginationTarget) {
|
36
|
-
console.log("Found pagination target, observing...");
|
37
31
|
this.intersectionObserver.observe(this.paginationTarget);
|
38
32
|
} else {
|
39
|
-
console.log("No pagination target found");
|
40
33
|
}
|
41
34
|
}
|
42
35
|
|
43
36
|
paginationTargetConnected(target) {
|
44
|
-
console.log("Pagination target connected dynamically, observing...");
|
45
37
|
// Add a delay to allow scroll-to-bottom to happen first
|
46
38
|
setTimeout(() => {
|
47
|
-
console.log("Starting to observe pagination target after delay");
|
48
39
|
this.intersectionObserver.observe(target);
|
49
40
|
}, 1000);
|
50
41
|
}
|
51
42
|
|
52
43
|
paginationTargetDisconnected(target) {
|
53
|
-
console.log("Pagination target disconnected, stop observing...");
|
54
44
|
this.intersectionObserver.unobserve(target);
|
55
45
|
}
|
56
46
|
|
57
47
|
disconnect() {
|
58
|
-
console.log("Reverse infinite scroll controller disconnected");
|
59
48
|
this.intersectionObserver.disconnect();
|
60
49
|
}
|
61
50
|
|
@@ -67,15 +56,11 @@ export default class extends Controller {
|
|
67
56
|
|
68
57
|
async loadOlder() {
|
69
58
|
if (this.loadingValue) {
|
70
|
-
console.log("Already loading, skipping...");
|
71
59
|
return;
|
72
60
|
}
|
73
61
|
|
74
|
-
console.log("Loading older messages...");
|
75
|
-
console.log("Current pageValue:", this.pageValue);
|
76
62
|
this.loadingValue = true;
|
77
63
|
const nextPage = this.pageValue + 1;
|
78
|
-
console.log("Requesting page:", nextPage);
|
79
64
|
|
80
65
|
// Store current scroll position
|
81
66
|
const oldScrollHeight = this.element.scrollHeight;
|
@@ -83,7 +68,6 @@ export default class extends Controller {
|
|
83
68
|
|
84
69
|
try {
|
85
70
|
const url = this.buildUrl(nextPage);
|
86
|
-
console.log("Fetching URL:", url);
|
87
71
|
|
88
72
|
const response = await fetch(url, {
|
89
73
|
headers: {
|
@@ -96,7 +80,6 @@ export default class extends Controller {
|
|
96
80
|
}
|
97
81
|
|
98
82
|
const html = await response.text();
|
99
|
-
console.log("Received response for page", nextPage);
|
100
83
|
|
101
84
|
// Parse the turbo-stream content
|
102
85
|
const parser = new DOMParser();
|
@@ -112,7 +95,6 @@ export default class extends Controller {
|
|
112
95
|
// Manually prepend the template content (for reverse infinite scroll)
|
113
96
|
this.entriesTarget.insertAdjacentHTML("afterbegin", template.innerHTML);
|
114
97
|
this.pageValue = nextPage;
|
115
|
-
console.log(`Updated to page ${nextPage}`);
|
116
98
|
|
117
99
|
// Maintain scroll position (prevent jumping to top)
|
118
100
|
setTimeout(() => {
|
@@ -120,25 +102,16 @@ export default class extends Controller {
|
|
120
102
|
const heightDifference = newScrollHeight - oldScrollHeight;
|
121
103
|
this.element.scrollTop = oldScrollTop + heightDifference;
|
122
104
|
|
123
|
-
console.log("Scroll position updated:", {
|
124
|
-
oldScrollHeight,
|
125
|
-
newScrollHeight,
|
126
|
-
heightDifference,
|
127
|
-
newScrollTop: this.element.scrollTop
|
128
|
-
});
|
129
105
|
|
130
106
|
// Set up observer for new pagination target if it exists
|
131
107
|
if (this.hasPaginationTarget) {
|
132
|
-
console.log("Found new pagination target, observing...");
|
133
108
|
this.intersectionObserver.observe(this.paginationTarget);
|
134
109
|
} else {
|
135
|
-
console.log("No more pages to load");
|
136
110
|
}
|
137
111
|
}, 50);
|
138
112
|
}
|
139
113
|
} catch (error) {
|
140
114
|
console.error("Error loading older messages:", error);
|
141
|
-
console.log("Staying on page", this.pageValue, "due to error");
|
142
115
|
} finally {
|
143
116
|
this.loadingValue = false;
|
144
117
|
}
|
@@ -32,7 +32,6 @@ export default class extends Controller {
|
|
32
32
|
|
33
33
|
handleVariableChange(event) {
|
34
34
|
const input = event.target
|
35
|
-
console.log(`Variable ${input.name} changed to:`, input.value)
|
36
35
|
|
37
36
|
// Check if all required variables are filled and submit if so
|
38
37
|
setTimeout(() => {
|
@@ -41,7 +40,6 @@ export default class extends Controller {
|
|
41
40
|
}
|
42
41
|
|
43
42
|
handleDateRangeChange(input, picker) {
|
44
|
-
console.log(`Date range updated for ${input.name}:`, input.value)
|
45
43
|
|
46
44
|
// Force update any related hidden fields
|
47
45
|
this.updateRelatedDateFields(input, picker)
|
@@ -83,16 +81,12 @@ export default class extends Controller {
|
|
83
81
|
// More robust empty check
|
84
82
|
if (this.isEmpty(value)) {
|
85
83
|
completed = false
|
86
|
-
console.log(`Variable ${input.name} is empty:`, value)
|
87
84
|
} else {
|
88
|
-
console.log(`Variable ${input.name} has value:`, value)
|
89
85
|
}
|
90
86
|
})
|
91
87
|
|
92
|
-
console.log(`Form completion check: ${completed ? 'Complete' : 'Incomplete'}`)
|
93
88
|
|
94
89
|
if (completed) {
|
95
|
-
console.log('Submitting form with all variables filled')
|
96
90
|
form.submit()
|
97
91
|
}
|
98
92
|
}
|
@@ -112,11 +106,6 @@ export default class extends Controller {
|
|
112
106
|
|
113
107
|
// Debug method to check current variable states
|
114
108
|
debugVariables() {
|
115
|
-
console.log("=== Current Variable States ===")
|
116
109
|
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
110
|
}
|
122
111
|
}
|
data/app/javascript/sage.js
CHANGED
@@ -5,9 +5,10 @@ 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
7
|
import VariablesController from "sage/controllers/variables_controller"
|
8
|
+
import QueryToggleController from "sage/controllers/query_toggle_controller.js"
|
8
9
|
|
9
10
|
// Export all Sage controllers for manual registration
|
10
|
-
export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController }
|
11
|
+
export { SearchController, ClipboardController, SelectController, DashboardController, ReverseInfiniteScrollController, VariablesController, QueryToggleController }
|
11
12
|
|
12
13
|
// Register all Sage controllers with the provided Stimulus application
|
13
14
|
export function registerControllers(application) {
|
@@ -17,5 +18,6 @@ export function registerControllers(application) {
|
|
17
18
|
application.register("sage--dashboard", DashboardController)
|
18
19
|
application.register("sage--reverse-infinite-scroll", ReverseInfiniteScrollController)
|
19
20
|
application.register("sage--variables", VariablesController)
|
21
|
+
application.register("sage--query-toggle", QueryToggleController)
|
20
22
|
}
|
21
23
|
|
@@ -9,36 +9,40 @@
|
|
9
9
|
return moment.tz(time.format(format), timeZone)
|
10
10
|
}
|
11
11
|
<% end %>
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
});
|
12
|
+
|
13
|
+
<!-- Filter Navigation Area -->
|
14
|
+
<div class="surface border left-round" style="padding: 16px; margin-bottom: 20px; background-color: #f8f9fa;">
|
15
|
+
<h6 style="margin: 0 0 16px 0; color: #6b7280; font-weight: 500;">Query Filters</h6>
|
16
|
+
|
17
|
+
<div data-controller="sage--variables" data-sage--variables-form-target="form">
|
18
|
+
<form id="bind" method="get" action="<%= action %>">
|
19
|
+
<div class="grid">
|
20
|
+
<% date_vars = ["start_time", "end_time"] %>
|
21
|
+
<% if (date_vars - @bind_vars).empty? %>
|
22
|
+
<% @bind_vars = @bind_vars - date_vars %>
|
23
|
+
<% else %>
|
24
|
+
<% date_vars = nil %>
|
33
25
|
<% 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
26
|
|
37
|
-
|
38
|
-
<div
|
39
|
-
<
|
40
|
-
|
41
|
-
|
27
|
+
<% @bind_vars.each_with_index do |var, i| %>
|
28
|
+
<div class="s12 m6 l4" style="margin-bottom: 20px;">
|
29
|
+
<label style="display: block; margin-bottom: 6px; color: #6b7280; font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">
|
30
|
+
{<%= var %>}
|
31
|
+
</label>
|
32
|
+
<% if (data = @smart_vars[var]) %>
|
33
|
+
<%= select_tag var, options_for_select([[nil, nil]] + data, selected: var_params[var]),
|
34
|
+
style: "width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; height: 42px; box-sizing: border-box;",
|
35
|
+
data: { "sage--variables-target": "variable" } %>
|
36
|
+
<%= javascript_tag nonce: true do %>
|
37
|
+
$("#<%= var %>").selectize({
|
38
|
+
create: true
|
39
|
+
});
|
40
|
+
<% end %>
|
41
|
+
<% elsif var.end_with?("_at") || var == "start_time" || var == "end_time" %>
|
42
|
+
<%= hidden_field_tag var, var_params[var], data: { "sage--variables-target": "variable" } %>
|
43
|
+
<div id="<%= var %>-select" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; background: white; cursor: pointer; font-size: 14px; color: #6b7280; height: 42px; box-sizing: border-box; display: flex; align-items: center;">
|
44
|
+
<span>Select a date</span>
|
45
|
+
</div>
|
42
46
|
|
43
47
|
<%= javascript_tag nonce: true do %>
|
44
48
|
(function() {
|
@@ -71,24 +75,41 @@
|
|
71
75
|
}
|
72
76
|
})()
|
73
77
|
<% end %>
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
78
|
+
<% else %>
|
79
|
+
<%= text_field_tag var, var_params[var],
|
80
|
+
style: "width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; font-size: 14px; height: 42px; box-sizing: border-box;",
|
81
|
+
autofocus: i == 0 && !var.end_with?("_at") && !var_params[var],
|
82
|
+
data: { "sage--variables-target": "variable" },
|
83
|
+
placeholder: "Enter #{var.humanize.downcase}" %>
|
84
|
+
<% end %>
|
85
|
+
</div>
|
86
|
+
<% end %>
|
83
87
|
|
84
|
-
|
85
|
-
|
88
|
+
<% if date_vars %>
|
89
|
+
<% date_vars.each do |var| %>
|
90
|
+
<%= hidden_field_tag var, var_params[var], data: { "sage--variables-target": "variable" } %>
|
91
|
+
<% end %>
|
92
|
+
|
93
|
+
<div class="s12 m6 l4" style="margin-bottom: 20px;">
|
94
|
+
<label style="display: block; margin-bottom: 6px; color: #6b7280; font-size: 12px; font-weight: 500; text-transform: uppercase; letter-spacing: 0.5px;">
|
95
|
+
{<%= date_vars.join('}, {') %>}
|
96
|
+
</label>
|
97
|
+
<div id="reportrange" style="width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; background: white; cursor: pointer; font-size: 14px; color: #6b7280; height: 42px; box-sizing: border-box; display: flex; align-items: center;">
|
98
|
+
<span>Select a time range</span>
|
99
|
+
</div>
|
100
|
+
</div>
|
101
|
+
<% end %>
|
86
102
|
</div>
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
103
|
+
|
104
|
+
<div class="right-align" style="margin-top: 16px;">
|
105
|
+
<button type='submit' class='button primary'>
|
106
|
+
<i>play_arrow</i>
|
107
|
+
Run Query
|
108
|
+
</button>
|
91
109
|
</div>
|
110
|
+
</form>
|
111
|
+
</div>
|
112
|
+
</div>
|
92
113
|
|
93
114
|
<%= javascript_tag nonce: true do %>
|
94
115
|
function dateStr(daysAgo) {
|
@@ -160,9 +181,4 @@
|
|
160
181
|
}
|
161
182
|
}
|
162
183
|
<% end %>
|
163
|
-
<% end %>
|
164
|
-
|
165
|
-
<button type='submit' class='btn btn-primary'>Run</button>
|
166
|
-
</form>
|
167
|
-
</div>
|
168
184
|
<% end %>
|
@@ -11,7 +11,7 @@
|
|
11
11
|
<% @variable_params = @query.persisted? ? variable_params(@query) : nested_variable_params(@query) %>
|
12
12
|
|
13
13
|
<%= form_for @query, url: (@query.persisted? ? query_path(@query, params: @variable_params) : queries_path(params: @variable_params)), html: {autocomplete: "off", class: ""} do |f| %>
|
14
|
-
<div class="grid">
|
14
|
+
<div class="grid" style='margin-top: 0px'>
|
15
15
|
<div class="s10">
|
16
16
|
<div class="field label border">
|
17
17
|
<%= f.text_field :name %>
|
@@ -19,7 +19,7 @@
|
|
19
19
|
</div>
|
20
20
|
|
21
21
|
<div class="field textarea label border">
|
22
|
-
<%= f.text_area :description, style: "height:
|
22
|
+
<%= f.text_area :description, style: "height: 145px; resize: vertical;" %>
|
23
23
|
<%= f.label :description, "Description (Optional)", class: (@query.description.present? ? "active" : "") %>
|
24
24
|
</div>
|
25
25
|
|
@@ -28,14 +28,28 @@
|
|
28
28
|
|
29
29
|
<div class="s2">
|
30
30
|
<nav style="display: flex; flex-direction: column; gap: 12px; padding-left: 8px;">
|
31
|
+
<button type='button' onclick="formatSQL()" class="button secondary" title="Format SQL">
|
32
|
+
<i>auto_fix_high</i>
|
33
|
+
</button>
|
34
|
+
|
35
|
+
<%= form_with url: (defined?(sage) && sage.respond_to?(:run_queries_path) ? sage.run_queries_path : run_queries_path), method: :post, local: false, style: "margin: 0;" do |f| %>
|
36
|
+
<%= f.hidden_field :statement, id: 'run_query_statement' %>
|
37
|
+
<%= f.hidden_field :query_id, value: @query.id %>
|
38
|
+
<%= f.hidden_field :data_source, value: @query.data_source || Blazer.data_sources.keys.first %>
|
39
|
+
<button type='submit' class="button primary" title="Run Query">
|
40
|
+
<i>play_arrow</i>
|
41
|
+
</button>
|
42
|
+
<% end %>
|
43
|
+
|
44
|
+
<button type="submit" name="commit" value="<%= @query.persisted? ? "Update" : "Create" %>" class="button primary" title="<%= @query.persisted? ? "Update" : "Create" %>">
|
45
|
+
<i><%= @query.persisted? ? "save" : "add" %></i>
|
46
|
+
</button>
|
47
|
+
|
31
48
|
<% if @query.persisted? %>
|
32
49
|
<%= link_to query_path(@query), method: :delete, "data-confirm" => "Are you sure?", class: "button red", title: "Delete" do %>
|
33
50
|
<i>delete</i>
|
34
51
|
<% end %>
|
35
52
|
<% end %>
|
36
|
-
<button type="submit" name="commit" value="<%= @query.persisted? ? "Update" : "Create" %>" class="button primary" title="<%= @query.persisted? ? "Update" : "Create" %>">
|
37
|
-
<i><%= @query.persisted? ? "save" : "add" %></i>
|
38
|
-
</button>
|
39
53
|
</nav>
|
40
54
|
</div>
|
41
55
|
</div>
|
@@ -28,7 +28,7 @@
|
|
28
28
|
}
|
29
29
|
</style>
|
30
30
|
|
31
|
-
<div id=<%= dom_id(query, 'statement-box') %> class='' style="position: relative;">
|
31
|
+
<div id=<%= dom_id(query, 'statement-box') %> class='' style="position: relative; border-radius: 10px;">
|
32
32
|
<%
|
33
33
|
# Handle different contexts - job vs regular view rendering
|
34
34
|
form_url = if local_assigns[:form_url]
|
@@ -46,12 +46,8 @@
|
|
46
46
|
<%= f.hidden_field :statement, id: 'query_statement' %>
|
47
47
|
<%= f.hidden_field :query_id, value: query.id %>
|
48
48
|
<%= f.hidden_field :data_source, value: query.data_source || Blazer.data_sources.keys.first %>
|
49
|
-
<div id="editor-container" style="position: relative;
|
50
|
-
<div id="editor" style="height:
|
51
|
-
<div style="position: absolute; bottom: 10px; right: 15px; z-index: 1000; border-radius: 4px; padding: 5px; display: flex; gap: 8px;">
|
52
|
-
<button type='button' onclick="formatSQL()" class="secondary">Format</button>
|
53
|
-
<button type='submit' class="<%= 'glow-button' if local_assigns[:statement].present? %>">Run</button>
|
54
|
-
</div>
|
49
|
+
<div id="editor-container" style="position: relative;">
|
50
|
+
<div id="editor" style="height: 225px;" data-initial-content="<%= html_escape(local_assigns[:statement] || query.statement) %>"><%= local_assigns[:statement] || query.statement %></div>
|
55
51
|
</div>
|
56
52
|
<% end %>
|
57
53
|
</div>
|
@@ -79,7 +75,7 @@
|
|
79
75
|
'COALESCE', 'NULLIF', 'DATE_TRUNC', 'EXTRACT', 'SUBSTRING',
|
80
76
|
'CONCAT', 'LENGTH', 'LOWER', 'UPPER', 'TRIM', 'REPLACE'];
|
81
77
|
|
82
|
-
// Preserve string literals and
|
78
|
+
// Preserve string literals, comments, and JSON operators
|
83
79
|
const preservedStrings = [];
|
84
80
|
let preservedIndex = 0;
|
85
81
|
|
@@ -100,6 +96,12 @@
|
|
100
96
|
return `__PRESERVED_STRING_${preservedIndex++}__`;
|
101
97
|
});
|
102
98
|
|
99
|
+
// Preserve JSON operators and comparison operators EARLY before any other processing
|
100
|
+
sql = sql.replace(/->>|->|#>>|#>|@>|<@|>=|<=|<>|!=|\?\?|\?&|\?/g, (match) => {
|
101
|
+
preservedStrings.push(match);
|
102
|
+
return `__PRESERVED_STRING_${preservedIndex++}__`;
|
103
|
+
});
|
104
|
+
|
103
105
|
// Remove extra whitespace and normalize spaces
|
104
106
|
sql = sql.replace(/\s+/g, ' ').trim();
|
105
107
|
|
@@ -154,15 +156,25 @@
|
|
154
156
|
// Remove leading newline if present
|
155
157
|
sql = sql.replace(/^\n+/, '');
|
156
158
|
|
157
|
-
// Ensure consistent spacing around operators
|
159
|
+
// Ensure consistent spacing around basic operators (complex operators already preserved)
|
158
160
|
sql = sql.replace(/\s*=\s*/g, ' = ');
|
159
|
-
sql = sql.replace(/\s*<>\s*/g, ' <> ');
|
160
|
-
sql = sql.replace(/\s*!=\s*/g, ' != ');
|
161
|
-
sql = sql.replace(/\s*<=\s*/g, ' <= ');
|
162
|
-
sql = sql.replace(/\s*>=\s*/g, ' >= ');
|
163
161
|
sql = sql.replace(/\s*<\s*/g, ' < ');
|
164
162
|
sql = sql.replace(/\s*>\s*/g, ' > ');
|
165
163
|
|
164
|
+
// Final fix: repair any broken operators that slipped through
|
165
|
+
sql = sql.replace(/>\s*=/g, '>=');
|
166
|
+
sql = sql.replace(/<\s*=/g, '<=');
|
167
|
+
sql = sql.replace(/@\s*>/g, '@>');
|
168
|
+
sql = sql.replace(/<\s*>/g, '<>');
|
169
|
+
sql = sql.replace(/!\s*=/g, '!=');
|
170
|
+
sql = sql.replace(/-\s*>/g, '->');
|
171
|
+
sql = sql.replace(/-\s*>>/g, '->>');
|
172
|
+
sql = sql.replace(/#\s*>/g, '#>');
|
173
|
+
sql = sql.replace(/#\s*>>/g, '#>>');
|
174
|
+
sql = sql.replace(/<\s*@/g, '<@');
|
175
|
+
sql = sql.replace(/\?\s*\?/g, '??');
|
176
|
+
sql = sql.replace(/\?\s*&/g, '?&');
|
177
|
+
|
166
178
|
return sql;
|
167
179
|
}
|
168
180
|
|
@@ -182,7 +194,8 @@
|
|
182
194
|
enableLiveAutocompletion: false,
|
183
195
|
highlightActiveLine: false,
|
184
196
|
fontSize: 12,
|
185
|
-
minLines: 10
|
197
|
+
minLines: 10,
|
198
|
+
scrollPastEnd: 0.25
|
186
199
|
})
|
187
200
|
editor.renderer.setShowGutter(true)
|
188
201
|
editor.renderer.setPrintMarginColumn(false)
|
@@ -190,15 +203,22 @@
|
|
190
203
|
editor.renderer.setPadding(10)
|
191
204
|
editor.getSession().setUseWrapMode(true)
|
192
205
|
|
193
|
-
// Ensure scrolling
|
194
|
-
editor.renderer.setScrollMargin(0, 0,
|
206
|
+
// Ensure proper scrolling with bottom margin for buttons
|
207
|
+
editor.renderer.setScrollMargin(0, 0, 60, 0) // top, right, bottom, left margins
|
195
208
|
editor.setAutoScrollEditorIntoView(true)
|
209
|
+
|
210
|
+
// Force scrollbar to always be visible for consistent layout
|
211
|
+
editor.setOption("vScrollBarAlwaysVisible", true)
|
212
|
+
|
213
|
+
// Add bottom padding to content area to ensure last line is fully visible
|
214
|
+
editor.renderer.setPadding(10, 10, 60, 10) // top, right, bottom, left padding
|
196
215
|
|
197
|
-
// Update
|
216
|
+
// Update all hidden fields when editor content changes
|
198
217
|
editor.getSession().on("change", function () {
|
199
218
|
const value = editor.getValue()
|
200
|
-
$("#query_statement").val(value) // For run form
|
219
|
+
$("#query_statement").val(value) // For statement_box run form
|
201
220
|
$("#query_statement_form").val(value) // For main form
|
221
|
+
$("#run_query_statement").val(value) // For new_form run button
|
202
222
|
})
|
203
223
|
|
204
224
|
// Set initial value from data attribute or current content
|
@@ -212,6 +232,7 @@
|
|
212
232
|
const initialValue = editor.getValue()
|
213
233
|
$("#query_statement").val(initialValue)
|
214
234
|
$("#query_statement_form").val(initialValue)
|
235
|
+
$("#run_query_statement").val(initialValue)
|
215
236
|
editor.focus()
|
216
237
|
|
217
238
|
// Add top padding to ace_scroller and gutter to keep them aligned
|
@@ -27,7 +27,6 @@
|
|
27
27
|
setTimeout(() => {
|
28
28
|
const messagesDiv = document.getElementById('<%= dom_id(@query, 'messages') %>');
|
29
29
|
if (messagesDiv) {
|
30
|
-
console.log("Scrolling to bottom, scrollHeight:", messagesDiv.scrollHeight);
|
31
30
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
32
31
|
}
|
33
32
|
}, 100);
|
@@ -35,7 +34,6 @@
|
|
35
34
|
setTimeout(() => {
|
36
35
|
const messagesDiv = document.getElementById('<%= dom_id(@query, 'messages') %>');
|
37
36
|
if (messagesDiv) {
|
38
|
-
console.log("Final scroll to bottom, scrollHeight:", messagesDiv.scrollHeight);
|
39
37
|
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
40
38
|
}
|
41
39
|
}, 500);
|
@@ -1,36 +1,67 @@
|
|
1
1
|
<%= blazer_title "New Query" %>
|
2
|
-
<%# <%= render partial: "form" %>
|
3
2
|
|
4
3
|
<div class="padding left-align">
|
5
|
-
<h5
|
4
|
+
<h5>New Query</h5>
|
6
5
|
</div>
|
7
6
|
|
8
7
|
<div class="sage-container">
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
<div class="form-group">
|
15
|
-
<%= label_tag "query[question]", "Your Question", class: "form-label" %>
|
16
|
-
<%= text_area_tag "query[question]", nil,
|
17
|
-
class: "form-control",
|
18
|
-
rows: 8,
|
19
|
-
placeholder: "e.g., Show me the top 10 customers by revenue in the last 30 days",
|
20
|
-
required: true,
|
21
|
-
data: { sage_target: "question" } %>
|
22
|
-
<small class="form-text text-muted">
|
23
|
-
Be specific about tables, columns, and conditions when possible
|
24
|
-
</small>
|
8
|
+
<!-- BeerCSS Tabs -->
|
9
|
+
<div>
|
10
|
+
<div class="tabs">
|
11
|
+
<a id="prompt-tab" class="active">Ask AI</a>
|
12
|
+
<a id="sql-tab">Write SQL</a>
|
25
13
|
</div>
|
14
|
+
|
15
|
+
<!-- Prompt Mode Page -->
|
16
|
+
<div id="prompt-page" class="page padding active">
|
17
|
+
<%= form_with url: queries_path, method: :post, local: true, scope: :query do |f| %>
|
18
|
+
<div class="form-group">
|
19
|
+
<%= f.label :question, "Your Question", class: "form-label left-align" %>
|
20
|
+
<%= f.text_area :question,
|
21
|
+
class: "form-control",
|
22
|
+
rows: 8,
|
23
|
+
placeholder: "e.g., Show me the top 10 customers by revenue in the last 30 days" %>
|
24
|
+
<small class="form-text text-muted">
|
25
|
+
Be specific about tables, columns, and conditions when possible
|
26
|
+
</small>
|
27
|
+
</div>
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
+
<div class="form-actions">
|
30
|
+
<button type='submit' data-disable-with='Generating...'>Generate Report</button>
|
31
|
+
</div>
|
32
|
+
<% end %>
|
29
33
|
</div>
|
30
|
-
<% end %>
|
31
34
|
|
32
|
-
|
33
|
-
|
35
|
+
<!-- SQL Mode Page -->
|
36
|
+
<div id="sql-page" class="page padding">
|
37
|
+
<%= form_with url: queries_path, method: :post, local: true, scope: :query do |f| %>
|
38
|
+
<div class="form-group">
|
39
|
+
<%= f.label :name, "Query Name", class: "form-label left-align" %>
|
40
|
+
<%= f.text_field :name,
|
41
|
+
class: "form-control",
|
42
|
+
placeholder: "Enter a name for your query",
|
43
|
+
value: params[:name] %>
|
44
|
+
</div>
|
45
|
+
|
46
|
+
<div class="form-group">
|
47
|
+
<%= f.label :statement, "SQL Statement", class: "form-label left-align" %>
|
48
|
+
<%
|
49
|
+
forked_statement = ""
|
50
|
+
if params[:fork_query_id].present?
|
51
|
+
forked_query = Blazer::Query.find_by(id: params[:fork_query_id])
|
52
|
+
forked_statement = forked_query&.statement || ""
|
53
|
+
end
|
54
|
+
%>
|
55
|
+
<%= f.hidden_field :statement, id: 'sql_statement_field' %>
|
56
|
+
<div id="sql-editor" style="height: 300px; border: 1px solid #ddd; border-radius: 4px;" data-initial-content="<%= html_escape(forked_statement) %>"><%= forked_statement %></div>
|
57
|
+
</div>
|
58
|
+
|
59
|
+
<div class="form-actions">
|
60
|
+
<button type='submit' data-disable-with='Creating...'>Create Query</button>
|
61
|
+
</div>
|
62
|
+
<% end %>
|
63
|
+
</div>
|
64
|
+
</div>
|
34
65
|
|
35
66
|
<% if @schema.present? %>
|
36
67
|
<div class="padding">
|
@@ -75,15 +106,6 @@
|
|
75
106
|
padding: 20px;
|
76
107
|
}
|
77
108
|
|
78
|
-
.sage-header {
|
79
|
-
margin-bottom: 30px;
|
80
|
-
text-align: center;
|
81
|
-
}
|
82
|
-
|
83
|
-
.sage-header h1 {
|
84
|
-
margin-bottom: 10px;
|
85
|
-
}
|
86
|
-
|
87
109
|
.form-group {
|
88
110
|
margin-bottom: 20px;
|
89
111
|
}
|
@@ -94,6 +116,10 @@
|
|
94
116
|
display: block;
|
95
117
|
}
|
96
118
|
|
119
|
+
.form-label.left-align {
|
120
|
+
text-align: left;
|
121
|
+
}
|
122
|
+
|
97
123
|
.form-control {
|
98
124
|
width: 100%;
|
99
125
|
padding: 8px 12px;
|
@@ -130,33 +156,6 @@
|
|
130
156
|
text-align: center;
|
131
157
|
}
|
132
158
|
|
133
|
-
.btn {
|
134
|
-
display: inline-block;
|
135
|
-
padding: 10px 20px;
|
136
|
-
font-size: 16px;
|
137
|
-
font-weight: 500;
|
138
|
-
text-align: center;
|
139
|
-
text-decoration: none;
|
140
|
-
border: none;
|
141
|
-
border-radius: 4px;
|
142
|
-
cursor: pointer;
|
143
|
-
transition: all 0.2s;
|
144
|
-
}
|
145
|
-
|
146
|
-
.btn-primary {
|
147
|
-
background-color: #4CAF50;
|
148
|
-
color: white;
|
149
|
-
}
|
150
|
-
|
151
|
-
.btn-primary:hover {
|
152
|
-
background-color: #45a049;
|
153
|
-
}
|
154
|
-
|
155
|
-
.btn-primary:disabled {
|
156
|
-
opacity: 0.6;
|
157
|
-
cursor: not-allowed;
|
158
|
-
}
|
159
|
-
|
160
159
|
.text-muted {
|
161
160
|
color: #666;
|
162
161
|
font-size: 14px;
|
@@ -190,6 +189,135 @@
|
|
190
189
|
text-overflow: ellipsis;
|
191
190
|
margin-bottom: 10px;
|
192
191
|
}
|
193
|
-
|
194
192
|
</style>
|
195
193
|
|
194
|
+
<script>
|
195
|
+
// ACE Editor initialization
|
196
|
+
let sqlEditor = null;
|
197
|
+
|
198
|
+
function initializeAceEditor() {
|
199
|
+
if (sqlEditor) {
|
200
|
+
sqlEditor.destroy();
|
201
|
+
}
|
202
|
+
|
203
|
+
const editorDiv = document.getElementById('sql-editor');
|
204
|
+
if (!editorDiv) return;
|
205
|
+
|
206
|
+
sqlEditor = ace.edit("sql-editor");
|
207
|
+
sqlEditor.setTheme("ace/theme/twilight");
|
208
|
+
sqlEditor.getSession().setMode("ace/mode/sql");
|
209
|
+
sqlEditor.setOptions({
|
210
|
+
enableBasicAutocompletion: false,
|
211
|
+
enableSnippets: false,
|
212
|
+
enableLiveAutocompletion: false,
|
213
|
+
highlightActiveLine: false,
|
214
|
+
fontSize: 12,
|
215
|
+
minLines: 10,
|
216
|
+
scrollPastEnd: 0.25
|
217
|
+
});
|
218
|
+
sqlEditor.renderer.setShowGutter(true);
|
219
|
+
sqlEditor.renderer.setPrintMarginColumn(false);
|
220
|
+
sqlEditor.setShowPrintMargin(false);
|
221
|
+
sqlEditor.renderer.setPadding(10);
|
222
|
+
sqlEditor.getSession().setUseWrapMode(true);
|
223
|
+
|
224
|
+
// Set initial content
|
225
|
+
const initialContent = editorDiv.getAttribute("data-initial-content") || "";
|
226
|
+
if (initialContent) {
|
227
|
+
sqlEditor.setValue(initialContent, -1);
|
228
|
+
}
|
229
|
+
|
230
|
+
// Update hidden field when content changes
|
231
|
+
sqlEditor.getSession().on("change", function() {
|
232
|
+
const statementField = document.getElementById('sql_statement_field');
|
233
|
+
if (statementField) {
|
234
|
+
statementField.value = sqlEditor.getValue();
|
235
|
+
}
|
236
|
+
});
|
237
|
+
|
238
|
+
// Set initial value in hidden field
|
239
|
+
const statementField = document.getElementById('sql_statement_field');
|
240
|
+
if (statementField) {
|
241
|
+
statementField.value = sqlEditor.getValue();
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
document.addEventListener('DOMContentLoaded', function() {
|
246
|
+
const promptTab = document.getElementById('prompt-tab');
|
247
|
+
const sqlTab = document.getElementById('sql-tab');
|
248
|
+
const promptPage = document.getElementById('prompt-page');
|
249
|
+
const sqlPage = document.getElementById('sql-page');
|
250
|
+
|
251
|
+
function switchToPromptMode() {
|
252
|
+
promptTab.classList.add('active');
|
253
|
+
sqlTab.classList.remove('active');
|
254
|
+
promptPage.classList.add('active');
|
255
|
+
sqlPage.classList.remove('active');
|
256
|
+
}
|
257
|
+
|
258
|
+
function switchToSqlMode() {
|
259
|
+
sqlTab.classList.add('active');
|
260
|
+
promptTab.classList.remove('active');
|
261
|
+
sqlPage.classList.add('active');
|
262
|
+
promptPage.classList.remove('active');
|
263
|
+
}
|
264
|
+
|
265
|
+
promptTab.addEventListener('click', switchToPromptMode);
|
266
|
+
sqlTab.addEventListener('click', switchToSqlMode);
|
267
|
+
|
268
|
+
// If we have fork parameters, automatically switch to SQL mode
|
269
|
+
const urlParams = new URLSearchParams(window.location.search);
|
270
|
+
if (urlParams.get('fork_query_id') || urlParams.get('name')) {
|
271
|
+
console.log('Fork detected, switching to SQL mode');
|
272
|
+
switchToSqlMode();
|
273
|
+
// Initialize ACE editor immediately for forks
|
274
|
+
setTimeout(() => {
|
275
|
+
console.log('Initializing ACE editor for fork');
|
276
|
+
initializeAceEditor();
|
277
|
+
}, 200);
|
278
|
+
}
|
279
|
+
});
|
280
|
+
|
281
|
+
// Initialize ACE editor when SQL tab is clicked
|
282
|
+
document.addEventListener('click', function(e) {
|
283
|
+
if (e.target.id === 'sql-tab') {
|
284
|
+
setTimeout(() => {
|
285
|
+
initializeAceEditor();
|
286
|
+
}, 100);
|
287
|
+
}
|
288
|
+
});
|
289
|
+
|
290
|
+
// Ensure form submission captures the editor value
|
291
|
+
document.addEventListener('submit', function(e) {
|
292
|
+
if (e.target.closest('#sql-page') && sqlEditor) {
|
293
|
+
const statementField = document.getElementById('sql_statement_field');
|
294
|
+
if (statementField) {
|
295
|
+
statementField.value = sqlEditor.getValue();
|
296
|
+
}
|
297
|
+
}
|
298
|
+
});
|
299
|
+
|
300
|
+
// Handle Turbo navigation for fork detection
|
301
|
+
document.addEventListener('turbo:load', function() {
|
302
|
+
const urlParams = new URLSearchParams(window.location.search);
|
303
|
+
if (urlParams.get('fork_query_id') || urlParams.get('name')) {
|
304
|
+
console.log('Fork detected via turbo:load, switching to SQL mode');
|
305
|
+
const promptTab = document.getElementById('prompt-tab');
|
306
|
+
const sqlTab = document.getElementById('sql-tab');
|
307
|
+
const promptPage = document.getElementById('prompt-page');
|
308
|
+
const sqlPage = document.getElementById('sql-page');
|
309
|
+
|
310
|
+
if (sqlTab && promptTab && sqlPage && promptPage) {
|
311
|
+
sqlTab.classList.add('active');
|
312
|
+
promptTab.classList.remove('active');
|
313
|
+
sqlPage.classList.add('active');
|
314
|
+
promptPage.classList.remove('active');
|
315
|
+
|
316
|
+
setTimeout(() => {
|
317
|
+
console.log('Initializing ACE editor for fork via turbo:load');
|
318
|
+
initializeAceEditor();
|
319
|
+
}, 200);
|
320
|
+
}
|
321
|
+
}
|
322
|
+
});
|
323
|
+
</script>
|
@@ -6,11 +6,11 @@
|
|
6
6
|
</h5>
|
7
7
|
</div>
|
8
8
|
|
9
|
-
<div class="row right-align">
|
9
|
+
<div class="row right-align" style="margin-bottom: 24px;">
|
10
10
|
<%= link_to "Edit", edit_query_path(@query, params: variable_params(@query)), class: "btn btn-default", data: { turbo: false }, disabled: !@query.editable?(blazer_user) %>
|
11
11
|
<%= link_to "Fork", new_query_path(params: {variables: variable_params(@query), fork_query_id: @query.id, data_source: @query.data_source, name: @query.name}), class: "btn btn-info" %>
|
12
12
|
<% if !@error && @success %>
|
13
|
-
<%= button_to "Download",
|
13
|
+
<%= button_to "Download", run_queries_path(format: :csv), params: @run_data, class: "btn btn-primary" %>
|
14
14
|
<% end %>
|
15
15
|
</div>
|
16
16
|
|
@@ -30,7 +30,14 @@
|
|
30
30
|
|
31
31
|
<%= render partial: "sage/variables", locals: { action: query_path(@query) } %>
|
32
32
|
|
33
|
-
<
|
33
|
+
<div data-controller="sage--query-toggle">
|
34
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
35
|
+
<span>Statement</span>
|
36
|
+
<i class="material-icons" data-sage--query-toggle-target="icon" data-action="click->sage--query-toggle#toggle" style="cursor: pointer; font-size: 18px; opacity: 0.6;">visibility_off</i>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<pre id="code" data-sage--query-toggle-target="content"><code><%= @statement.try(:display_statement) || @query.statement %></code></pre>
|
40
|
+
</div>
|
34
41
|
|
35
42
|
<% if @success %>
|
36
43
|
<%= turbo_frame_tag dom_id(@query, 'results'), src: run_query_path(@query.id, from_show: true, variables: variable_params(@query)) do %>
|
data/config/importmap.rb
CHANGED
@@ -5,6 +5,8 @@ pin "sage/controllers/clipboard_controller", to: "sage/controllers/clipboard_con
|
|
5
5
|
pin "sage/controllers/select_controller", to: "sage/controllers/select_controller.js"
|
6
6
|
pin "sage/controllers/dashboard_controller", to: "sage/controllers/dashboard_controller.js"
|
7
7
|
pin "sage/controllers/reverse_infinite_scroll_controller", to: "sage/controllers/reverse_infinite_scroll_controller.js"
|
8
|
+
pin "sage/controllers/variables_controller", to: "sage/controllers/variables_controller.js"
|
9
|
+
pin "sage/controllers/query_toggle_controller", to: "sage/controllers/query_toggle_controller.js"
|
8
10
|
|
9
11
|
# Don't pin common libraries - let the host app handle them
|
10
12
|
# pin "@hotwired/stimulus", to: "stimulus.min.js"
|
data/lib/sage/engine.rb
CHANGED
@@ -192,7 +192,8 @@ module Sage
|
|
192
192
|
"sage/controllers/clipboard_controller.js",
|
193
193
|
"sage/controllers/select_controller.js",
|
194
194
|
"sage/controllers/dashboard_controller.js",
|
195
|
-
"sage/controllers/reverse_infinite_scroll_controller.js"
|
195
|
+
"sage/controllers/reverse_infinite_scroll_controller.js",
|
196
|
+
"sage/controllers/query_toggle_controller.js"
|
196
197
|
]
|
197
198
|
|
198
199
|
if defined?(Sprockets)
|
data/lib/sage/version.rb
CHANGED
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
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nathan Jones
|
@@ -152,6 +152,7 @@ files:
|
|
152
152
|
- app/assets/javascripts/sage/application.js
|
153
153
|
- app/assets/javascripts/sage/controllers/clipboard_controller.js
|
154
154
|
- app/assets/javascripts/sage/controllers/dashboard_controller.js
|
155
|
+
- app/assets/javascripts/sage/controllers/query_toggle_controller.js
|
155
156
|
- app/assets/javascripts/sage/controllers/reverse_infinite_scroll_controller.js
|
156
157
|
- app/assets/javascripts/sage/controllers/search_controller.js
|
157
158
|
- app/assets/javascripts/sage/controllers/select_controller.js
|
@@ -170,6 +171,7 @@ files:
|
|
170
171
|
- app/javascript/sage.js
|
171
172
|
- app/javascript/sage/controllers/clipboard_controller.js
|
172
173
|
- app/javascript/sage/controllers/dashboard_controller.js
|
174
|
+
- app/javascript/sage/controllers/query_toggle_controller.js
|
173
175
|
- app/javascript/sage/controllers/reverse_infinite_scroll_controller.js
|
174
176
|
- app/javascript/sage/controllers/search_controller.js
|
175
177
|
- app/javascript/sage/controllers/select_controller.js
|