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.
- checksums.yaml +7 -0
- data/README.md +202 -0
- data/app/assets/images/chevron-down-zinc-500.svg +1 -0
- data/app/assets/images/chevron-right.svg +1 -0
- data/app/assets/images/loading.svg +4 -0
- data/app/assets/images/sage/chevron-down-zinc-500.svg +1 -0
- data/app/assets/images/sage/chevron-right.svg +1 -0
- data/app/assets/images/sage/loading.svg +4 -0
- data/app/assets/javascripts/sage/application.js +18 -0
- data/app/assets/stylesheets/sage/application.css +308 -0
- data/app/controllers/sage/actions_controller.rb +5 -0
- data/app/controllers/sage/application_controller.rb +4 -0
- data/app/controllers/sage/base_controller.rb +10 -0
- data/app/controllers/sage/checks_controller.rb +65 -0
- data/app/controllers/sage/dashboards_controller.rb +130 -0
- data/app/controllers/sage/queries/messages_controller.rb +62 -0
- data/app/controllers/sage/queries_controller.rb +596 -0
- data/app/helpers/sage/application_helper.rb +30 -0
- data/app/helpers/sage/queries_helper.rb +23 -0
- data/app/javascript/controllers/element_removal_controller.js +7 -0
- data/app/javascript/sage/controllers/clipboard_controller.js +26 -0
- data/app/javascript/sage/controllers/dashboard_controller.js +132 -0
- data/app/javascript/sage/controllers/reverse_infinite_scroll_controller.js +146 -0
- data/app/javascript/sage/controllers/search_controller.js +47 -0
- data/app/javascript/sage/controllers/select_controller.js +215 -0
- data/app/javascript/sage.js +19 -0
- data/app/jobs/sage/application_job.rb +4 -0
- data/app/jobs/sage/process_report_job.rb +80 -0
- data/app/mailers/sage/application_mailer.rb +6 -0
- data/app/models/sage/application_record.rb +5 -0
- data/app/models/sage/message.rb +8 -0
- data/app/schemas/sage/report_response_schema.rb +8 -0
- data/app/views/layouts/application.html.erb +34 -0
- data/app/views/layouts/sage/application.html.erb +94 -0
- data/app/views/sage/checks/_form.html.erb +81 -0
- data/app/views/sage/checks/_search.html.erb +8 -0
- data/app/views/sage/checks/edit.html.erb +10 -0
- data/app/views/sage/checks/index.html.erb +58 -0
- data/app/views/sage/checks/new.html.erb +8 -0
- data/app/views/sage/dashboards/_form.html.erb +50 -0
- data/app/views/sage/dashboards/_search.html.erb +8 -0
- data/app/views/sage/dashboards/index.html.erb +58 -0
- data/app/views/sage/dashboards/new.html.erb +8 -0
- data/app/views/sage/dashboards/show.html.erb +58 -0
- data/app/views/sage/messages/_form.html.erb +14 -0
- data/app/views/sage/queries/_caching.html.erb +17 -0
- data/app/views/sage/queries/_form.html.erb +72 -0
- data/app/views/sage/queries/_input.html.erb +17 -0
- data/app/views/sage/queries/_message.html.erb +25 -0
- data/app/views/sage/queries/_message.turbo_stream.erb +10 -0
- data/app/views/sage/queries/_new_form.html.erb +43 -0
- data/app/views/sage/queries/_run.html.erb +232 -0
- data/app/views/sage/queries/_search.html.erb +8 -0
- data/app/views/sage/queries/_statement_box.html.erb +241 -0
- data/app/views/sage/queries/_streaming_message.html.erb +14 -0
- data/app/views/sage/queries/create.turbo_stream.erb +114 -0
- data/app/views/sage/queries/edit.html.erb +48 -0
- data/app/views/sage/queries/index.html.erb +59 -0
- data/app/views/sage/queries/messages/create.turbo_stream.erb +22 -0
- data/app/views/sage/queries/messages/index.html.erb +44 -0
- data/app/views/sage/queries/messages/index.turbo_stream.erb +15 -0
- data/app/views/sage/queries/new.html.erb +195 -0
- data/app/views/sage/queries/run.html.erb +1 -0
- data/app/views/sage/queries/run.turbo_stream.erb +3 -0
- data/app/views/sage/queries/show.html.erb +49 -0
- data/app/views/sage/queries/table_schema.html.erb +77 -0
- data/app/views/sage/shared/_navigation.html.erb +26 -0
- data/app/views/sage/shared/_overlay.html.erb +11 -0
- data/config/importmap.rb +11 -0
- data/config/initializers/pagy.rb +2 -0
- data/config/initializers/ransack.rb +152 -0
- data/config/routes.rb +31 -0
- data/lib/generators/sage/USAGE +13 -0
- data/lib/generators/sage/install/install_generator.rb +128 -0
- data/lib/generators/sage/install/templates/sage.rb +22 -0
- data/lib/sage/database_schema_context.rb +56 -0
- data/lib/sage/engine.rb +260 -0
- data/lib/sage/model_scopes_context.rb +185 -0
- data/lib/sage/report_processor.rb +263 -0
- data/lib/sage/version.rb +3 -0
- data/lib/sage.rb +25 -0
- data/lib/tasks/sage_tasks.rake +4 -0
- metadata +245 -0
@@ -0,0 +1,34 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title><%= content_for(:title) || "Sage" %></title>
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
6
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
7
|
+
<meta name="mobile-web-app-capable" content="yes">
|
8
|
+
<%= csrf_meta_tags %>
|
9
|
+
<%= csp_meta_tag %>
|
10
|
+
|
11
|
+
<%= yield :head %>
|
12
|
+
|
13
|
+
<%# Enable PWA manifest for installable apps (make sure to enable in config/routes.rb too!) %>
|
14
|
+
<%#= tag.link rel: "manifest", href: pwa_manifest_path(format: :json) %>
|
15
|
+
|
16
|
+
<link rel="icon" href="/icon.png" type="image/png">
|
17
|
+
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
18
|
+
<link rel="apple-touch-icon" href="/icon.png">
|
19
|
+
</head>
|
20
|
+
|
21
|
+
<body class="header-layout">
|
22
|
+
<header id="header">
|
23
|
+
<div class="container">
|
24
|
+
<h1 class="font-bold text-2xl">Sage</h1>
|
25
|
+
</div>
|
26
|
+
</header>
|
27
|
+
|
28
|
+
<main id="main">
|
29
|
+
<div class="container">
|
30
|
+
<%= yield %>
|
31
|
+
</div>
|
32
|
+
</main>
|
33
|
+
</body>
|
34
|
+
</html>
|
@@ -0,0 +1,94 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title><%= blazer_title ? "Sage - #{blazer_title}": "Sage - Natural language reporting" %></title>
|
5
|
+
|
6
|
+
<%= csrf_meta_tags %>
|
7
|
+
<%= csp_meta_tag %>
|
8
|
+
|
9
|
+
<%= yield :head %>
|
10
|
+
|
11
|
+
<% if defined?(Propshaft::Railtie) && Rails.application.assets.is_a?(Propshaft::Assembly) %>
|
12
|
+
<%= stylesheet_link_tag "sage/application", "blazer/selectize", "blazer/daterangepicker" %>
|
13
|
+
<%= javascript_include_tag "blazer/jquery", "blazer/rails-ujs", "blazer/stupidtable", "blazer/stupidtable-custom-settings", "blazer/jquery.stickytableheaders", "blazer/selectize", "blazer/highlight.min", "blazer/moment", "blazer/moment-timezone-with-data", "blazer/daterangepicker", "blazer/chart.umd", "blazer/chartjs-adapter-date-fns.bundle", "blazer/chartkick", "blazer/mapkick.bundle", "blazer/ace/ace", "blazer/ace/ext-language_tools", "blazer/ace/theme-twilight", "blazer/ace/mode-sql", "blazer/ace/snippets/text", "blazer/ace/snippets/sql", "blazer/Sortable", "blazer/vue.global.prod", "blazer/routes", "blazer/queries", "blazer/fuzzysearch", nonce: true %>
|
14
|
+
<% elsif defined?(Sprockets) %>
|
15
|
+
<%= stylesheet_link_tag "sage/application", "blazer/selectize", "blazer/daterangepicker" %>
|
16
|
+
<%= javascript_include_tag "blazer/jquery", "blazer/rails-ujs", "blazer/stupidtable", "blazer/stupidtable-custom-settings", "blazer/jquery.stickytableheaders", "blazer/selectize", "blazer/highlight.min", "blazer/moment", "blazer/moment-timezone-with-data", "blazer/daterangepicker", "blazer/chart.umd", "blazer/chartjs-adapter-date-fns.bundle", "blazer/chartkick", "blazer/mapkick.bundle", "blazer/ace/ace", "blazer/ace/ext-language_tools", "blazer/ace/theme-twilight", "blazer/ace/mode-sql", "blazer/ace/snippets/text", "blazer/ace/snippets/sql", "blazer/Sortable", "blazer/vue.global.prod", "blazer/routes", "blazer/queries", "blazer/fuzzysearch", "sage/application", nonce: true %>
|
17
|
+
<% else %>
|
18
|
+
<%= stylesheet_link_tag "sage/application" %>
|
19
|
+
<%= javascript_importmap_tags %>
|
20
|
+
<% end %>
|
21
|
+
|
22
|
+
<!-- Beer CSS - loaded after other stylesheets to take precedence -->
|
23
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/beercss@3.11.33/dist/cdn/beer.min.css">
|
24
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/beercss@3.11.33/dist/cdn/beer.min.js"></script>
|
25
|
+
<script type="module" src="https://cdn.jsdelivr.net/npm/material-dynamic-colors@1.1.2/dist/cdn/material-dynamic-colors.min.js"></script>
|
26
|
+
|
27
|
+
<!-- Material Icons -->
|
28
|
+
<%# <link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" /> %>
|
29
|
+
|
30
|
+
<%= javascript_importmap_tags %>
|
31
|
+
<%= javascript_tag nonce: true do %>
|
32
|
+
<%= blazer_js_var "rootPath", root_path %>
|
33
|
+
|
34
|
+
function changeTheme() {
|
35
|
+
// Use Beer CSS mode functions
|
36
|
+
if (typeof ui === 'function') {
|
37
|
+
const currentMode = ui("mode");
|
38
|
+
const newMode = currentMode === 'dark' ? 'light' : 'dark';
|
39
|
+
ui("mode", newMode);
|
40
|
+
// Store the preference to prevent system override
|
41
|
+
localStorage.setItem('sage-theme-mode', newMode);
|
42
|
+
updateThemeIcon();
|
43
|
+
}
|
44
|
+
}
|
45
|
+
|
46
|
+
function updateThemeIcon() {
|
47
|
+
const themeButton = document.getElementById('theme-toggle');
|
48
|
+
const themeIcon = themeButton ? themeButton.querySelector('i') : null;
|
49
|
+
|
50
|
+
if (themeIcon && typeof ui === 'function') {
|
51
|
+
const currentMode = ui("mode");
|
52
|
+
themeIcon.textContent = currentMode === 'dark' ? 'light_mode' : 'dark_mode';
|
53
|
+
}
|
54
|
+
}
|
55
|
+
|
56
|
+
// Initialize theme on page load (works with Turbo)
|
57
|
+
function initializeTheme() {
|
58
|
+
// Check if user has a saved preference
|
59
|
+
const savedMode = localStorage.getItem('sage-theme-mode');
|
60
|
+
if (savedMode && typeof ui === 'function') {
|
61
|
+
ui("mode", savedMode);
|
62
|
+
}
|
63
|
+
// Update the icon
|
64
|
+
updateThemeIcon();
|
65
|
+
}
|
66
|
+
|
67
|
+
// Initialize on DOM ready and Turbo navigation
|
68
|
+
document.addEventListener('DOMContentLoaded', initializeTheme);
|
69
|
+
document.addEventListener('turbo:load', initializeTheme);
|
70
|
+
|
71
|
+
// Handle modal dialogs when turbo frames load
|
72
|
+
document.addEventListener('turbo:frame-load', function(event) {
|
73
|
+
// Check if this is the overlay frame and contains a dialog
|
74
|
+
if (event.target.id === 'overlay') {
|
75
|
+
const dialog = event.target.querySelector('#dialog');
|
76
|
+
if (dialog && typeof ui === 'function') {
|
77
|
+
// Small delay to ensure DOM is fully rendered
|
78
|
+
requestAnimationFrame(() => {
|
79
|
+
ui('#dialog');
|
80
|
+
});
|
81
|
+
}
|
82
|
+
}
|
83
|
+
});
|
84
|
+
<% end %>
|
85
|
+
</head>
|
86
|
+
<body id='layout' class=''>
|
87
|
+
<%= render partial: 'sage/shared/navigation' %>
|
88
|
+
|
89
|
+
<main class='responsive center-align <%= "max" if current_page?(edit_query_path(params[:id])) rescue nil %>'>
|
90
|
+
<%= yield %>
|
91
|
+
<%= turbo_frame_tag "overlay" %>
|
92
|
+
</main>
|
93
|
+
</body>
|
94
|
+
</html>
|
@@ -0,0 +1,81 @@
|
|
1
|
+
<%= form_for @check do |f| %>
|
2
|
+
<% unless @check.respond_to?(:check_type) || @check.respond_to?(:invert) %>
|
3
|
+
<p class="secondary-text">Checks are designed to identify bad data. A check fails if there are any results.</p>
|
4
|
+
<% end %>
|
5
|
+
|
6
|
+
<% if @check.errors.any? %>
|
7
|
+
<div class="error padding margin"><%= @check.errors.full_messages.first %></div>
|
8
|
+
<% end %>
|
9
|
+
|
10
|
+
<div class="grid">
|
11
|
+
<div class="s12 m6">
|
12
|
+
<div data-controller="sage--select"
|
13
|
+
data-sage--select-options-value="<%= Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} }.to_json %>"
|
14
|
+
data-sage--select-selected-value="<%= @check.query_id %>"
|
15
|
+
data-sage--select-placeholder-value="Select query">
|
16
|
+
<div class="field label border select-container">
|
17
|
+
<input type="text"
|
18
|
+
data-sage--select-target="input"
|
19
|
+
data-action="input->sage--select#search focus->sage--select#focus blur->sage--select#blur"
|
20
|
+
autocomplete="off"
|
21
|
+
placeholder=" ">
|
22
|
+
<label>Search queries</label>
|
23
|
+
<input type="hidden" name="check[query_id]" id="check_query_id" value="<%= @check.query_id %>" data-sage--select-target="hidden">
|
24
|
+
<div data-sage--select-target="dropdown" class="select-dropdown hidden">
|
25
|
+
</div>
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
|
29
|
+
<% if @check.respond_to?(:check_type) %>
|
30
|
+
<div class="field label border">
|
31
|
+
<% check_options = [["Any results (bad data)", "bad_data"], ["No results (missing data)", "missing_data"]] %>
|
32
|
+
<% check_options << ["Anomaly (most recent data point)", "anomaly"] if Blazer.anomaly_checks %>
|
33
|
+
<%= f.select :check_type, check_options, {}, {class: "active"} %>
|
34
|
+
<label>Alert if</label>
|
35
|
+
</div>
|
36
|
+
<% elsif @check.respond_to?(:invert) %>
|
37
|
+
<div class="field label border">
|
38
|
+
<%= f.select :invert, [["Any results (bad data)", false], ["No results (missing data)", true]], {}, {class: "active"} %>
|
39
|
+
<label>Fails if</label>
|
40
|
+
</div>
|
41
|
+
<% end %>
|
42
|
+
|
43
|
+
<% if @check.respond_to?(:schedule) && Blazer.check_schedules %>
|
44
|
+
<div class="field label border">
|
45
|
+
<%= f.select :schedule, Blazer.check_schedules.map { |v| [v, v] }, {}, {class: "active"} %>
|
46
|
+
<label>Run every</label>
|
47
|
+
</div>
|
48
|
+
<% end %>
|
49
|
+
</div>
|
50
|
+
|
51
|
+
<div class="s12 m6">
|
52
|
+
<div class="field label border">
|
53
|
+
<%= f.text_field :emails %>
|
54
|
+
<label>Emails</label>
|
55
|
+
<span class="helper">Optional, comma separated</span>
|
56
|
+
</div>
|
57
|
+
|
58
|
+
<% if Blazer.slack? %>
|
59
|
+
<div class="field label border">
|
60
|
+
<%= f.text_field :slack_channels %>
|
61
|
+
<label>Slack channels</label>
|
62
|
+
<span class="helper">Optional, comma separated</span>
|
63
|
+
</div>
|
64
|
+
<% end %>
|
65
|
+
|
66
|
+
<p class="secondary-text small-padding">
|
67
|
+
Emails <%= Blazer.slack? ? "and Slack notifications " : nil %>are sent when a check starts failing, and when it starts passing again.
|
68
|
+
</p>
|
69
|
+
</div>
|
70
|
+
</div>
|
71
|
+
|
72
|
+
<div class="medium-space"></div>
|
73
|
+
|
74
|
+
<nav>
|
75
|
+
<% if @check.persisted? %>
|
76
|
+
<%= link_to "Delete", check_path(@check), method: :delete, "data-confirm" => "Are you sure?", class: "button error" %>
|
77
|
+
<% end %>
|
78
|
+
<button type="submit">Save</button>
|
79
|
+
<%= link_to "Back", :back %>
|
80
|
+
</nav>
|
81
|
+
<% end %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<%= search_form_for @q, url: checks_path, class: 'field border prefix round', data: { controller: 'sage--search' } do |f| %>
|
2
|
+
|
3
|
+
<i class="front">search</i>
|
4
|
+
<%= f.search_field :emails_or_slack_channels_or_check_type_cont,
|
5
|
+
placeholder: "Search checks by emails, slack channels, or type...",
|
6
|
+
class: 'text',
|
7
|
+
data: { action: 'input->sage--search#submit' } %>
|
8
|
+
<% end %>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<% blazer_title "Checks" %>
|
2
|
+
|
3
|
+
<div class="padding left-align <%= "#{current_page?(checks_path) ? 'active' : ''}" %>">
|
4
|
+
<h5>Checks</h5>
|
5
|
+
</div>
|
6
|
+
|
7
|
+
<div id="">
|
8
|
+
<div class="row right-align">
|
9
|
+
<%= link_to new_check_path do %>
|
10
|
+
<button>
|
11
|
+
New Check
|
12
|
+
</button>
|
13
|
+
<% end %>
|
14
|
+
</div>
|
15
|
+
</div>
|
16
|
+
|
17
|
+
<%= render partial: 'search' %>
|
18
|
+
|
19
|
+
<table id="checks" class="table">
|
20
|
+
<thead>
|
21
|
+
<tr>
|
22
|
+
<th>Query</th>
|
23
|
+
<th style="width: 15%;">State</th>
|
24
|
+
<th style="width: 10%;">Run</th>
|
25
|
+
<th style="width: 25%;">Notify</th>
|
26
|
+
<th style="width: 15%; text-align: right;"></th>
|
27
|
+
</tr>
|
28
|
+
</thead>
|
29
|
+
<tbody>
|
30
|
+
<% @checks.each do |check| %>
|
31
|
+
<tr>
|
32
|
+
<td>
|
33
|
+
<%= link_to check.query.name, check.query %>
|
34
|
+
<% if check.try(:check_type) %>
|
35
|
+
<span class="text-muted"><%= check.check_type.to_s.gsub("_", " ") %></span>
|
36
|
+
<% end %>
|
37
|
+
</td>
|
38
|
+
<td style="vertical-align: middle;">
|
39
|
+
<% if check.state %>
|
40
|
+
<span class="chip <%= check.state.parameterize.gsub("-", "_") %>"><%= check.state.upcase %></span>
|
41
|
+
<% end %>
|
42
|
+
</td>
|
43
|
+
<td style="vertical-align: middle;">
|
44
|
+
<%= check.schedule if check.respond_to?(:schedule) %>
|
45
|
+
</td>
|
46
|
+
<td style="vertical-align: middle;">
|
47
|
+
<% notify_items = check.split_emails + check.split_slack_channels %>
|
48
|
+
<%= notify_items.join(", ") if notify_items.any? %>
|
49
|
+
</td>
|
50
|
+
<td style="text-align: right; vertical-align: middle;">
|
51
|
+
<%= link_to "Edit", edit_check_path(check) %>
|
52
|
+
<%= link_to "Run Now", run_check_path(check), class: "button primary" %>
|
53
|
+
</td>
|
54
|
+
</tr>
|
55
|
+
<% end %>
|
56
|
+
</tbody>
|
57
|
+
</table>
|
58
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
<%= form_for @dashboard, url: (@dashboard.persisted? ? dashboard_path(@dashboard, params: variable_params(@dashboard)) : dashboards_path(params: variable_params(@dashboard))), html: {class: "small-form", data: {controller: "sage--dashboard", "sage--dashboard-queries-value": (@queries || @dashboard.dashboard_queries.order(:position).map(&:query)).to_json, action: "select:change->sage--dashboard#addQuery"}} do |f| %>
|
2
|
+
<% if @dashboard.errors.any? %>
|
3
|
+
<div class="error-container"><%= @dashboard.errors.full_messages.first %></div>
|
4
|
+
<% end %>
|
5
|
+
|
6
|
+
<div class="field border label">
|
7
|
+
<input type='text' name='dashboard[name]' id='dashboard_name' value="<%= @dashboard.name %>">
|
8
|
+
<label>Name</label>
|
9
|
+
</div>
|
10
|
+
|
11
|
+
<div class="grid">
|
12
|
+
<div class="s6">
|
13
|
+
<fieldset>
|
14
|
+
<legend>Add Chart</legend>
|
15
|
+
<div data-controller="sage--select"
|
16
|
+
data-sage--select-options-value="<%= Blazer::Query.active.named.order(:name).select("id, name").map { |q| {text: q.name, value: q.id} }.to_json %>"
|
17
|
+
data-sage--select-placeholder-value="Select chart">
|
18
|
+
<div class="field label border select-container">
|
19
|
+
<input type="text"
|
20
|
+
data-sage--select-target="input"
|
21
|
+
data-action="input->sage--select#search focus->sage--select#focus blur->sage--select#blur"
|
22
|
+
autocomplete="off"
|
23
|
+
placeholder=" ">
|
24
|
+
<label>Search charts</label>
|
25
|
+
<div data-sage--select-target="dropdown" class="select-dropdown hidden">
|
26
|
+
</div>
|
27
|
+
</div>
|
28
|
+
</div>
|
29
|
+
</fieldset>
|
30
|
+
|
31
|
+
<p style="padding-bottom: 140px;">
|
32
|
+
<% if @dashboard.persisted? %>
|
33
|
+
<%= link_to "Delete", dashboard_path(@dashboard), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
|
34
|
+
<% end %>
|
35
|
+
<button type='submit'>
|
36
|
+
Save
|
37
|
+
</button>
|
38
|
+
<%= link_to "Back", :back, class: "" %>
|
39
|
+
</p>
|
40
|
+
</div>
|
41
|
+
|
42
|
+
<div class="s6">
|
43
|
+
<fieldset data-sage--dashboard-target="queryList" style="display: none; opacity: 1 !important;">
|
44
|
+
<legend style="opacity: 1 !important; color: black !important;">Selected Charts</legend>
|
45
|
+
<nav id="queries" style="display: flex; flex-direction: column; gap: 8px; opacity: 1 !important;">
|
46
|
+
</nav>
|
47
|
+
</fieldset>
|
48
|
+
</div>
|
49
|
+
</div>
|
50
|
+
<% end %>
|
@@ -0,0 +1,8 @@
|
|
1
|
+
<%= search_form_for @q, url: dashboards_path, class: 'field border prefix round', data: { controller: 'sage--search', turbo_frame: "dashboards" } do |f| %>
|
2
|
+
|
3
|
+
<i class="front">search</i>
|
4
|
+
<%= f.search_field :name_cont,
|
5
|
+
placeholder: "Search dashboards by name...",
|
6
|
+
class: 'text',
|
7
|
+
data: { action: 'input->sage--search#submit' } %>
|
8
|
+
<% end %>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<%# re-structure header, so that navigation and actions are inline %>
|
2
|
+
<div id="dashboards">
|
3
|
+
<div class="padding left-align <%= "#{current_page?(dashboards_path) ? 'active' : ''}" %>">
|
4
|
+
<h5>Dashboards</h5>
|
5
|
+
</div>
|
6
|
+
|
7
|
+
<div class="right-align" style="">
|
8
|
+
<% if blazer_user %>
|
9
|
+
<%= link_to "All", root_path, class: !params[:filter] ? "active" : nil, style: "margin-right: 40px;" %>
|
10
|
+
|
11
|
+
<% if Blazer.audit %>
|
12
|
+
<%= link_to "Viewed", root_path(filter: "viewed"), class: params[:filter] == "viewed" ? "active" : nil, style: "margin-right: 40px;" %>
|
13
|
+
<% end %>
|
14
|
+
|
15
|
+
<%= link_to "Mine", root_path(filter: "mine"), class: params[:filter] == "mine" ? "active" : nil, style: "margin-right: 40px;" %>
|
16
|
+
<% end %>
|
17
|
+
<%= link_to new_dashboard_path do %>
|
18
|
+
<button>
|
19
|
+
New Dashboard
|
20
|
+
</button>
|
21
|
+
<% end %>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
<%= render partial: 'search' %>
|
25
|
+
|
26
|
+
<% if @dashboards.any? %>
|
27
|
+
<table class="border">
|
28
|
+
<thead>
|
29
|
+
<tr>
|
30
|
+
<th>Name</th>
|
31
|
+
<% if Blazer.user_class %>
|
32
|
+
<th style="width: 20%;">Mastermind</th>
|
33
|
+
<% end%>
|
34
|
+
</tr>
|
35
|
+
</thead>
|
36
|
+
<tbody>
|
37
|
+
<% @dashboards.each do |dashboard| %>
|
38
|
+
<tr>
|
39
|
+
<td>
|
40
|
+
<%= link_to dashboard.name, dashboard_path(dashboard), class: "text-decoration-none" %>
|
41
|
+
</td>
|
42
|
+
<% if Blazer.user_class %>
|
43
|
+
<td><%= dashboard.creator.name %></td>
|
44
|
+
<% end %>
|
45
|
+
</tr>
|
46
|
+
<% end %>
|
47
|
+
</tbody>
|
48
|
+
</table>
|
49
|
+
|
50
|
+
<div style="display: flex; justify-content: center; padding: 1rem;">
|
51
|
+
<%== pagy_nav(@pagy) if @pagy.pages > 1 %>
|
52
|
+
</div>
|
53
|
+
<% else %>
|
54
|
+
<div class="mt-3">
|
55
|
+
<p>No dashboards found.</p>
|
56
|
+
</div>
|
57
|
+
<% end %>
|
58
|
+
</div>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<% blazer_title @dashboard.name %>
|
2
|
+
|
3
|
+
<div class="padding left-align row" style="">
|
4
|
+
<h5 style="">
|
5
|
+
<%= @dashboard.name %>
|
6
|
+
</h5>
|
7
|
+
<div class="">
|
8
|
+
<%= link_to edit_dashboard_path(@dashboard, params: variable_params(@dashboard)) do %>
|
9
|
+
<button>Edit</button>
|
10
|
+
<% end %>
|
11
|
+
<%= link_to "Back", dashboards_path %>
|
12
|
+
</div>
|
13
|
+
</div>
|
14
|
+
|
15
|
+
<div style="margin-bottom: 60px;"></div>
|
16
|
+
|
17
|
+
<% if @data_sources.any? { |ds| ds.cache_mode != "off" } %>
|
18
|
+
<p class="text-muted" style="float: right;">
|
19
|
+
Some queries may be cached
|
20
|
+
<%= link_to "Refresh", refresh_dashboard_path(@dashboard, params: variable_params(@dashboard)), method: :post %>
|
21
|
+
</p>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
<% if @bind_vars.any? %>
|
25
|
+
<%= render partial: "blazer/variables", locals: {action: dashboard_path(@dashboard)} %>
|
26
|
+
<% else %>
|
27
|
+
<div style="padding-bottom: 15px;"></div>
|
28
|
+
<% end %>
|
29
|
+
|
30
|
+
<% @queries.each_with_index do |query, i| %>
|
31
|
+
<div class="chart-container">
|
32
|
+
<h4><%= link_to query.friendly_name, query_path(query, params: variable_params(query)), target: "_blank" %></h4>
|
33
|
+
<div id="chart-<%= i %>" class="chart">
|
34
|
+
<% if @query_errors[query.id] %>
|
35
|
+
<div class="alert alert-danger">
|
36
|
+
<%= @query_errors[query.id] %>
|
37
|
+
</div>
|
38
|
+
<% else %>
|
39
|
+
<p class="text-muted">Loading...</p>
|
40
|
+
<% end %>
|
41
|
+
</div>
|
42
|
+
</div>
|
43
|
+
<% unless @query_errors[query.id] %>
|
44
|
+
<%= javascript_tag nonce: true do %>
|
45
|
+
<% data = {statement: query.statement, query_id: query.id, data_source: query.data_source || Blazer.data_sources.keys.first, variables: variable_params(query), only_chart: true} %>
|
46
|
+
<% data.merge!(cohort_period: params[:cohort_period]) if params[:cohort_period] %>
|
47
|
+
<%= blazer_js_var "data", data %>
|
48
|
+
|
49
|
+
runQuery(data, function (data) {
|
50
|
+
$("#chart-<%= i %>").html(data)
|
51
|
+
$("#chart-<%= i %> table").stupidtable(stupidtableCustomSettings)
|
52
|
+
}, function (message) {
|
53
|
+
$("#chart-<%= i %>").addClass("query-error").html(message)
|
54
|
+
});
|
55
|
+
<% end %>
|
56
|
+
<% end %>
|
57
|
+
<% end %>
|
58
|
+
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<%#= turbo_frame_tag dom_id(@query, 'message_form') do %>
|
2
|
+
<div style='position: absolute; bottom: 0px; left: 0; right: 0; z-index: 1000; padding: 0 20px;'>
|
3
|
+
<%= form_with url: "/queries/run", method: :post, local: false do |f| %>
|
4
|
+
<%= f.hidden_field :query_id, value: @query.id %>
|
5
|
+
<%= f.hidden_field :data_source, value: @query.data_source || Blazer.data_sources.keys.first %>
|
6
|
+
<div style='display: flex; align-items: center; gap: 12px; border: 1px solid #e0e0e0; border-radius: 24px; padding: 4px; background: white;'>
|
7
|
+
<%= f.text_field :statement, placeholder: 'How can I help?', style: 'flex: 1; border: none; outline: none; background: transparent; padding: 12px 16px; font-size: 16px;' %>
|
8
|
+
<button class='primary' type='submit' style='border: none; border-radius: 20px; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center; color: white; cursor: pointer; flex-shrink: 0;'>
|
9
|
+
<i style='font-size: 20px;'>send</i>
|
10
|
+
</button>
|
11
|
+
</div>
|
12
|
+
<% end %>
|
13
|
+
</div>
|
14
|
+
<%# end %>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<% if @cached_at || @just_cached %>
|
2
|
+
<p class="text-muted" style="float: right;">
|
3
|
+
<% if @cached_at %>
|
4
|
+
Cached <%= time_ago_in_words(@cached_at, include_seconds: true) %> ago
|
5
|
+
<% elsif params[:query_id] %>
|
6
|
+
Cached just now
|
7
|
+
<% if @data_source.cache_mode == "slow" %>
|
8
|
+
(over <%= "%g" % @data_source.cache_slow_threshold %>s)
|
9
|
+
<% end %>
|
10
|
+
<% end %>
|
11
|
+
|
12
|
+
<% if @query && params[:query_id] %>
|
13
|
+
<%= link_to "Refresh", refresh_query_path(@query, params: variable_params(@query, @var_params)), method: :post %>
|
14
|
+
<% end %>
|
15
|
+
</p>
|
16
|
+
<% end %>
|
17
|
+
|
@@ -0,0 +1,72 @@
|
|
1
|
+
<% if @query.errors.any? %>
|
2
|
+
<div class="alert alert-danger"><%= @query.errors.full_messages.first %></div>
|
3
|
+
<% end %>
|
4
|
+
|
5
|
+
<% @variable_params = @query.persisted? ? variable_params(@query) : nested_variable_params(@query) %>
|
6
|
+
|
7
|
+
<div id="app" v-cloak>
|
8
|
+
<%= form_for @query, url: (@query.persisted? ? query_path(@query, params: @variable_params) : queries_path(params: @variable_params)), html: {autocomplete: "off"} do |f| %>
|
9
|
+
<div class="row">
|
10
|
+
<div class="col-xs-4">
|
11
|
+
<div class="form-group">
|
12
|
+
<%= f.label :name %>
|
13
|
+
<%= f.text_field :name, class: "form-control" %>
|
14
|
+
</div>
|
15
|
+
<div class="form-group">
|
16
|
+
<%= f.label :description %>
|
17
|
+
<%= f.text_area :description, placeholder: "Optional", style: "height: 80px;", class: "form-control" %>
|
18
|
+
</div>
|
19
|
+
<div class="form-group text-right">
|
20
|
+
<%= f.submit "For Enter Press", class: "hide" %>
|
21
|
+
<% if @query.persisted? %>
|
22
|
+
<%= link_to "Delete", query_path(@query), method: :delete, "data-confirm" => "Are you sure?", class: "btn btn-danger" %>
|
23
|
+
<%= f.submit "Fork", class: "btn btn-info" %>
|
24
|
+
<% end %>
|
25
|
+
<%= f.submit @query.persisted? ? "Update" : "Create", class: "btn btn-success" %>
|
26
|
+
</div>
|
27
|
+
<% if @query.persisted? %>
|
28
|
+
<% dashboards_count = @query.dashboards.count %>
|
29
|
+
<% checks_count = @query.checks.count %>
|
30
|
+
<% words = [] %>
|
31
|
+
<% words << pluralize(dashboards_count, "dashboard") if dashboards_count > 0 %>
|
32
|
+
<% words << pluralize(checks_count, "check") if checks_count > 0 %>
|
33
|
+
<% if words.any? %>
|
34
|
+
<div class="alert alert-info">
|
35
|
+
Part of <%= words.to_sentence %>. Be careful when editing.
|
36
|
+
</div>
|
37
|
+
<% end %>
|
38
|
+
<% end %>
|
39
|
+
</div>
|
40
|
+
</div>
|
41
|
+
<% end %>
|
42
|
+
|
43
|
+
<div id="results">
|
44
|
+
<p class="text-muted" v-if="running">Loading...</p>
|
45
|
+
<div id="results-html" v-if="!running" :class="{ 'query-error': error }"></div>
|
46
|
+
</div>
|
47
|
+
</div>
|
48
|
+
|
49
|
+
<%= javascript_tag nonce: true do %>
|
50
|
+
<%= blazer_js_var "variableParams", @variable_params %>
|
51
|
+
<%= blazer_js_var "previewStatement", Blazer.data_sources.to_h { |k, v| [k, (v.preview_statement rescue "")] } %>
|
52
|
+
|
53
|
+
var app = Vue.createApp({
|
54
|
+
data: function() {
|
55
|
+
return {
|
56
|
+
running: false,
|
57
|
+
results: "",
|
58
|
+
error: false
|
59
|
+
}
|
60
|
+
},
|
61
|
+
methods: {
|
62
|
+
showResults(data) {
|
63
|
+
Vue.nextTick(function () {
|
64
|
+
$("#results-html").html(data)
|
65
|
+
})
|
66
|
+
}
|
67
|
+
}
|
68
|
+
})
|
69
|
+
app.config.compilerOptions.whitespace = "preserve"
|
70
|
+
app.mount("#app")
|
71
|
+
<% end %>
|
72
|
+
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<div id=<%= dom_id(query, 'input') %> class="border-top" style="padding: 16px 16px 4px 16px;">
|
2
|
+
<%= form_with url: query_messages_path(query), method: :post, id: 'chat-form' do |f| %>
|
3
|
+
<%= f.hidden_field :query_id, value: query.id %>
|
4
|
+
<%= f.hidden_field :data_source, value: query.data_source || Blazer.data_sources.keys.first %>
|
5
|
+
<div class="field textarea border round" style="margin-bottom: 1em !important;">
|
6
|
+
<%= f.text_area :statement,
|
7
|
+
placeholder: 'How can I help?',
|
8
|
+
id: 'chat-textarea',
|
9
|
+
class: 'transparent',
|
10
|
+
style: 'resize: none; padding-right: 48px; min-height: 40px;' %>
|
11
|
+
<button class='primary circle small' type='submit' style='position: absolute; bottom: 8px; right: 8px; z-index: 10; pointer-events: auto;'>
|
12
|
+
<i>send</i>
|
13
|
+
</button>
|
14
|
+
</div>
|
15
|
+
<% end %>
|
16
|
+
</div>
|
17
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<div id=<%= dom_id(message) %> class="row small-margin">
|
2
|
+
<div class="max">
|
3
|
+
<% if message.creator.present? || message.creator_id&.zero? %>
|
4
|
+
<p class="bold primary-text"><%= message.creator.try(:name) || message.creator.try(:email) || "User" %> <small class="secondary-text"><%= message.created_at.strftime('%l:%M %p') %></small></p>
|
5
|
+
<% else %>
|
6
|
+
<p class="bold accent-text">System <small class="secondary-text"><%= message.created_at.strftime('%l:%M %p') %></small></p>
|
7
|
+
<% end %>
|
8
|
+
<p><%= simple_format(message.body) %></p>
|
9
|
+
</div>
|
10
|
+
</div>
|
11
|
+
<script>
|
12
|
+
setTimeout(() => {
|
13
|
+
<% if local_assigns[:stream_target_id] %>
|
14
|
+
const targetElement = document.getElementById('<%= stream_target_id %>') || document.getElementById('<%= dom_id(message) %>');
|
15
|
+
<% else %>
|
16
|
+
const targetElement = document.getElementById('<%= dom_id(message) %>');
|
17
|
+
<% end %>
|
18
|
+
if (targetElement) {
|
19
|
+
const messagesDiv = targetElement.closest('[id$="_messages"]');
|
20
|
+
if (messagesDiv) {
|
21
|
+
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
22
|
+
}
|
23
|
+
}
|
24
|
+
}, 10);
|
25
|
+
</script>
|
@@ -0,0 +1,10 @@
|
|
1
|
+
<div id=<%= dom_id(message) %> class="row small-margin">
|
2
|
+
<div class="max">
|
3
|
+
<% if message.creator.present? || message.creator_id&.zero? %>
|
4
|
+
<p class="bold primary-text"><%= message.creator.try(:name) || message.creator.try(:email) || "User" %> <small class="secondary-text"><%= message.created_at.strftime('%l:%M %p') %></small></p>
|
5
|
+
<% else %>
|
6
|
+
<p class="bold accent-text">System <small class="secondary-text"><%= message.created_at.strftime('%l:%M %p') %></small></p>
|
7
|
+
<% end %>
|
8
|
+
<p><%= simple_format(message.body) %></p>
|
9
|
+
</div>
|
10
|
+
</div>
|