nquery 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 +7 -0
- data/LICENSE +21 -0
- data/README.md +52 -0
- data/Rakefile +9 -0
- data/app/assets/builds/nquery/application.css +339 -0
- data/app/assets/builds/nquery/application.js +989 -0
- data/app/assets/config/nquery_manifest.js +2 -0
- data/app/components/nquery/_card_component.html.erb +6 -0
- data/app/components/nquery/_card_table_component.html.erb +14 -0
- data/app/components/nquery/_flash_card_component.html.erb +32 -0
- data/app/components/nquery/_flash_cards_component.html.erb +3 -0
- data/app/components/nquery/_infinite_scroll_component.html.erb +11 -0
- data/app/components/nquery/card_component.rb +18 -0
- data/app/components/nquery/card_table_component.rb +19 -0
- data/app/components/nquery/component.rb +26 -0
- data/app/components/nquery/flash_card_component.rb +51 -0
- data/app/components/nquery/flash_cards_component.rb +20 -0
- data/app/components/nquery/infinite_scroll_component.rb +19 -0
- data/app/controllers/concerns/nquery/breadcrumbs.rb +238 -0
- data/app/controllers/concerns/nquery/browsable.rb +56 -0
- data/app/controllers/concerns/nquery/chart_actions.rb +104 -0
- data/app/controllers/concerns/nquery/chart_results.rb +30 -0
- data/app/controllers/nquery/admin/base_controller.rb +9 -0
- data/app/controllers/nquery/admin/data_sources_controller.rb +47 -0
- data/app/controllers/nquery/admin/groups_controller.rb +63 -0
- data/app/controllers/nquery/admin/logs_controller.rb +28 -0
- data/app/controllers/nquery/admin/permissions_controller.rb +53 -0
- data/app/controllers/nquery/admin/users_controller.rb +80 -0
- data/app/controllers/nquery/application_controller.rb +70 -0
- data/app/controllers/nquery/charts_controller.rb +51 -0
- data/app/controllers/nquery/collection/dashboards_controller.rb +38 -0
- data/app/controllers/nquery/collections_controller.rb +117 -0
- data/app/controllers/nquery/dashboard/charts_controller.rb +62 -0
- data/app/controllers/nquery/dashboards_controller.rb +90 -0
- data/app/controllers/nquery/embed/charts_controller.rb +24 -0
- data/app/controllers/nquery/embed/dashboards_controller.rb +17 -0
- data/app/controllers/nquery/home_controller.rb +16 -0
- data/app/controllers/nquery/imports_controller.rb +41 -0
- data/app/controllers/nquery/queries_controller.rb +105 -0
- data/app/controllers/nquery/registrations_controller.rb +31 -0
- data/app/controllers/nquery/sessions_controller.rb +28 -0
- data/app/helpers/nquery/charts_helper.rb +33 -0
- data/app/helpers/nquery/collections_helper.rb +13 -0
- data/app/helpers/nquery/components_helper.rb +29 -0
- data/app/helpers/nquery/home_helper.rb +23 -0
- data/app/helpers/nquery/icon_helper.rb +9 -0
- data/app/helpers/nquery/navigation_helper.rb +10 -0
- data/app/javascript/nquery/controllers/chart_preview_controller.js +125 -0
- data/app/javascript/nquery/controllers/csv_mapper_controller.js +23 -0
- data/app/javascript/nquery/controllers/dashboard_grid_controller.js +25 -0
- data/app/javascript/nquery/controllers/query_editor_controller.js +41 -0
- data/app/models/nquery/application_permission.rb +12 -0
- data/app/models/nquery/application_record.rb +7 -0
- data/app/models/nquery/audit.rb +38 -0
- data/app/models/nquery/chart.rb +37 -0
- data/app/models/nquery/collection.rb +35 -0
- data/app/models/nquery/collection_permission.rb +12 -0
- data/app/models/nquery/csv_upload.rb +11 -0
- data/app/models/nquery/dashboard.rb +27 -0
- data/app/models/nquery/dashboard_card.rb +8 -0
- data/app/models/nquery/data_permission.rb +13 -0
- data/app/models/nquery/data_source.rb +27 -0
- data/app/models/nquery/embed_token.rb +21 -0
- data/app/models/nquery/group.rb +27 -0
- data/app/models/nquery/group_membership.rb +10 -0
- data/app/models/nquery/query.rb +19 -0
- data/app/models/nquery/user.rb +58 -0
- data/app/views/layouts/nquery/application.html.erb +32 -0
- data/app/views/layouts/nquery/auth.html.erb +16 -0
- data/app/views/layouts/nquery/embed.html.erb +12 -0
- data/app/views/nquery/admin/data_sources/edit.html.erb +8 -0
- data/app/views/nquery/admin/data_sources/index.html.erb +21 -0
- data/app/views/nquery/admin/data_sources/new.html.erb +7 -0
- data/app/views/nquery/admin/groups/edit.html.erb +6 -0
- data/app/views/nquery/admin/groups/index.html.erb +21 -0
- data/app/views/nquery/admin/groups/new.html.erb +6 -0
- data/app/views/nquery/admin/groups/show.html.erb +24 -0
- data/app/views/nquery/admin/logs/index.html.erb +74 -0
- data/app/views/nquery/admin/permissions/_collection.html.erb +25 -0
- data/app/views/nquery/admin/permissions/_data_source.html.erb +27 -0
- data/app/views/nquery/admin/permissions/_group.html.erb +42 -0
- data/app/views/nquery/admin/permissions/by_collection.html.erb +10 -0
- data/app/views/nquery/admin/permissions/by_data_source.html.erb +7 -0
- data/app/views/nquery/admin/permissions/by_group.html.erb +12 -0
- data/app/views/nquery/admin/users/edit.html.erb +17 -0
- data/app/views/nquery/admin/users/index.html.erb +34 -0
- data/app/views/nquery/admin/users/new.html.erb +14 -0
- data/app/views/nquery/admin/users/show.html.erb +6 -0
- data/app/views/nquery/charts/_builder_form.html.erb +185 -0
- data/app/views/nquery/charts/_new_header.html.erb +31 -0
- data/app/views/nquery/charts/_schema_tree.html.erb +25 -0
- data/app/views/nquery/charts/edit.html.erb +6 -0
- data/app/views/nquery/charts/embed.html.erb +15 -0
- data/app/views/nquery/charts/new.html.erb +5 -0
- data/app/views/nquery/charts/show.html.erb +15 -0
- data/app/views/nquery/charts/update.turbo_stream.erb +1 -0
- data/app/views/nquery/collection/dashboards/new.html.erb +13 -0
- data/app/views/nquery/collections/_dashboard_card.html.erb +57 -0
- data/app/views/nquery/collections/edit.html.erb +15 -0
- data/app/views/nquery/collections/index.html.erb +41 -0
- data/app/views/nquery/collections/new.html.erb +15 -0
- data/app/views/nquery/collections/show.html.erb +100 -0
- data/app/views/nquery/dashboard/charts/new.html.erb +6 -0
- data/app/views/nquery/dashboards/_chart_card.html.erb +16 -0
- data/app/views/nquery/dashboards/edit.html.erb +34 -0
- data/app/views/nquery/dashboards/index.html.erb +35 -0
- data/app/views/nquery/dashboards/show.html.erb +46 -0
- data/app/views/nquery/embed/charts/show.html.erb +9 -0
- data/app/views/nquery/embed/dashboards/show.html.erb +7 -0
- data/app/views/nquery/home/index.html.erb +143 -0
- data/app/views/nquery/imports/new.html.erb +20 -0
- data/app/views/nquery/queries/edit.html.erb +40 -0
- data/app/views/nquery/queries/new.html.erb +37 -0
- data/app/views/nquery/registrations/new.html.erb +34 -0
- data/app/views/nquery/sessions/new.html.erb +29 -0
- data/app/views/nquery/shared/_auth_shell.html.erb +22 -0
- data/app/views/nquery/shared/_breadcrumbs.html.erb +15 -0
- data/app/views/nquery/shared/_chart_actions_menu.html.erb +27 -0
- data/app/views/nquery/shared/_chart_row_actions.html.erb +16 -0
- data/app/views/nquery/shared/_flash.html.erb +1 -0
- data/app/views/nquery/shared/_icon.html.erb +50 -0
- data/app/views/nquery/shared/_sidebar.html.erb +16 -0
- data/app/views/nquery/shared/_topbar.html.erb +12 -0
- data/config/importmap.rb +9 -0
- data/config/routes.rb +78 -0
- data/db/migrate/20260721000001_create_nquery_tables.rb +146 -0
- data/db/migrate/20260721180000_add_archived_at_to_nquery_dashboards.rb +8 -0
- data/db/migrate/20260721190000_add_archived_at_to_nquery_charts.rb +8 -0
- data/db/migrate/20260721200000_add_archived_at_to_nquery_collections.rb +8 -0
- data/lib/generators/nquery/install_generator.rb +38 -0
- data/lib/nquery/authorizes_collection.rb +50 -0
- data/lib/nquery/configuration.rb +44 -0
- data/lib/nquery/csv_importer.rb +68 -0
- data/lib/nquery/data_sources/adapter.rb +34 -0
- data/lib/nquery/data_sources/mysql_adapter.rb +24 -0
- data/lib/nquery/data_sources/postgresql_adapter.rb +46 -0
- data/lib/nquery/data_sources/rails_adapter.rb +54 -0
- data/lib/nquery/data_sources/sqlite_adapter.rb +8 -0
- data/lib/nquery/embed_token_service.rb +83 -0
- data/lib/nquery/engine.rb +55 -0
- data/lib/nquery/permissions/resolver.rb +83 -0
- data/lib/nquery/query_runner.rb +69 -0
- data/lib/nquery/sample_data/ecommerce.rb +157 -0
- data/lib/nquery/schema_explorer.rb +28 -0
- data/lib/nquery/seeder.rb +131 -0
- data/lib/nquery/version.rb +5 -0
- data/lib/nquery.rb +36 -0
- data/lib/tasks/nquery.rake +9 -0
- data/nquery.gemspec +40 -0
- metadata +395 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<div class="<%= component.css_class %>">
|
|
2
|
+
<table class="<%= component.table_class %>">
|
|
3
|
+
<thead>
|
|
4
|
+
<tr>
|
|
5
|
+
<% component.columns.each do |column| %>
|
|
6
|
+
<th><%= column %></th>
|
|
7
|
+
<% end %>
|
|
8
|
+
</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
<%= content %>
|
|
12
|
+
</tbody>
|
|
13
|
+
</table>
|
|
14
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="<%= component.css_class %>"
|
|
3
|
+
role="<%= component.role %>"
|
|
4
|
+
data-controller="flash-card"
|
|
5
|
+
data-flash-card-auto-dismiss-value="<%= component.auto_dismiss? %>"
|
|
6
|
+
data-flash-card-delay-value="<%= component.delay %>"
|
|
7
|
+
data-flash-card-toast-value="<%= component.toast? %>"
|
|
8
|
+
<%= "hidden" if component.toast? %>
|
|
9
|
+
>
|
|
10
|
+
<div class="nq-flash-card-icon" aria-hidden="true">
|
|
11
|
+
<% case component.type %>
|
|
12
|
+
<% when :notice %>
|
|
13
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.5"/><path d="M6 10l2.5 2.5L14 7" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
14
|
+
<% when :alert %>
|
|
15
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 6v5M10 14h.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.5"/></svg>
|
|
16
|
+
<% when :warning %>
|
|
17
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M10 7v4M10 14h.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M3.5 16h13L10 4 3.5 16z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>
|
|
18
|
+
<% when :info %>
|
|
19
|
+
<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><circle cx="10" cy="10" r="9" stroke="currentColor" stroke-width="1.5"/><path d="M10 9v5M10 6h.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
20
|
+
<% end %>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="nq-flash-card-body">
|
|
23
|
+
<% component.messages.each do |message| %>
|
|
24
|
+
<p><%= message %></p>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
<% if component.dismissible? %>
|
|
28
|
+
<button type="button" class="nq-flash-card-dismiss" aria-label="Dismiss" data-action="flash-card#dismiss">
|
|
29
|
+
<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
|
30
|
+
</button>
|
|
31
|
+
<% end %>
|
|
32
|
+
</div>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div
|
|
2
|
+
class="<%= component.css_class %>"
|
|
3
|
+
data-controller="infinite-scroll"
|
|
4
|
+
data-infinite-scroll-url-value="<%= component.url %>"
|
|
5
|
+
data-infinite-scroll-target-value="<%= component.target %>"
|
|
6
|
+
>
|
|
7
|
+
<div id="<%= component.target %>" class="nq-infinite-scroll-list">
|
|
8
|
+
<%= content %>
|
|
9
|
+
</div>
|
|
10
|
+
<div class="nq-infinite-scroll-sentinel" data-infinite-scroll-target="sentinel" aria-hidden="true"></div>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
class CardComponent < Component
|
|
5
|
+
attr_reader :title, :html_class
|
|
6
|
+
|
|
7
|
+
def initialize(title: nil, class: nil)
|
|
8
|
+
@title = title
|
|
9
|
+
@html_class = binding.local_variable_get(:class)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def css_class
|
|
13
|
+
classes = ["nq-card"]
|
|
14
|
+
classes << html_class if html_class.present?
|
|
15
|
+
classes.join(" ")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
class CardTableComponent < Component
|
|
5
|
+
attr_reader :columns, :html_class, :table_class
|
|
6
|
+
|
|
7
|
+
def initialize(columns:, class: nil, table_class: "nq-table")
|
|
8
|
+
@columns = columns
|
|
9
|
+
@html_class = binding.local_variable_get(:class)
|
|
10
|
+
@table_class = table_class
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def css_class
|
|
14
|
+
classes = ["nq-card", "nq-card-table"]
|
|
15
|
+
classes << html_class if html_class.present?
|
|
16
|
+
classes.join(" ")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
class Component
|
|
5
|
+
def self.render_in(view_context, **options, &block)
|
|
6
|
+
new(**options).render_in(view_context, &block)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def render_in(view_context, &block)
|
|
10
|
+
locals = template_locals(view_context, &block)
|
|
11
|
+
view_context.render(partial: partial_name, locals: locals)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def partial_name
|
|
17
|
+
"nquery/#{self.class.name.demodulize.underscore}"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def template_locals(view_context, &block)
|
|
21
|
+
locals = { component: self }
|
|
22
|
+
locals[:content] = view_context.capture(&block) if block
|
|
23
|
+
locals
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
class FlashCardComponent < Component
|
|
5
|
+
TYPES = %i[notice alert warning info].freeze
|
|
6
|
+
DEFAULT_DELAY = 5000
|
|
7
|
+
|
|
8
|
+
attr_reader :type, :dismissible, :html_class, :auto_dismiss, :toast, :delay
|
|
9
|
+
|
|
10
|
+
def initialize(message: nil, messages: nil, type: :notice, dismissible: true, toast: true,
|
|
11
|
+
delay: DEFAULT_DELAY, auto_dismiss: nil, class: nil)
|
|
12
|
+
@messages = Array(messages).presence || Array(message).compact
|
|
13
|
+
@type = type.to_sym
|
|
14
|
+
@toast = toast
|
|
15
|
+
@delay = delay
|
|
16
|
+
@dismissible = dismissible
|
|
17
|
+
@auto_dismiss = auto_dismiss.nil? ? toast : auto_dismiss
|
|
18
|
+
@html_class = binding.local_variable_get(:class)
|
|
19
|
+
|
|
20
|
+
raise ArgumentError, "invalid flash type: #{@type}" unless TYPES.include?(@type)
|
|
21
|
+
raise ArgumentError, "message is required" if @messages.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def messages
|
|
25
|
+
@messages
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def role
|
|
29
|
+
type == :alert ? "alert" : "status"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def css_class
|
|
33
|
+
classes = ["nq-flash-card", "nq-flash-card-#{type}"]
|
|
34
|
+
classes << (toast ? "nq-flash-card-toast" : "nq-flash-card-inline")
|
|
35
|
+
classes << html_class if html_class.present?
|
|
36
|
+
classes.join(" ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def dismissible?
|
|
40
|
+
dismissible
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def auto_dismiss?
|
|
44
|
+
auto_dismiss
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def toast?
|
|
48
|
+
toast
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
class FlashCardsComponent < Component
|
|
5
|
+
def initialize(flash:)
|
|
6
|
+
@flash = flash
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def cards
|
|
10
|
+
[].tap do |list|
|
|
11
|
+
list << FlashCardComponent.new(message: @flash[:notice], type: :notice) if @flash[:notice].present?
|
|
12
|
+
list << FlashCardComponent.new(message: @flash[:alert], type: :alert) if @flash[:alert].present?
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def any?
|
|
17
|
+
cards.any?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
class InfiniteScrollComponent < Component
|
|
5
|
+
attr_reader :url, :target, :html_class
|
|
6
|
+
|
|
7
|
+
def initialize(url:, target: "items", class: nil)
|
|
8
|
+
@url = url
|
|
9
|
+
@target = target
|
|
10
|
+
@html_class = binding.local_variable_get(:class)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def css_class
|
|
14
|
+
classes = ["nq-infinite-scroll"]
|
|
15
|
+
classes << html_class if html_class.present?
|
|
16
|
+
classes.join(" ")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
module Breadcrumbs
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
append_before_action :set_breadcrumbs, unless: :skip_breadcrumbs?
|
|
9
|
+
helper_method :breadcrumbs
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def set_breadcrumbs
|
|
13
|
+
@breadcrumbs = build_breadcrumbs
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def breadcrumbs
|
|
17
|
+
@breadcrumbs || []
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def skip_breadcrumbs?
|
|
23
|
+
auth_exempt? || controller_path == "nquery/home"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_breadcrumbs
|
|
27
|
+
crumbs = [{ label: "Home", path: root_path }]
|
|
28
|
+
|
|
29
|
+
case controller_path
|
|
30
|
+
when "nquery/collections"
|
|
31
|
+
append_section_breadcrumb(crumbs, "Collections", collections_path)
|
|
32
|
+
|
|
33
|
+
case action_name
|
|
34
|
+
when "show"
|
|
35
|
+
append_resource_breadcrumb(crumbs, breadcrumb_collection)
|
|
36
|
+
when "edit"
|
|
37
|
+
append_resource_breadcrumb(crumbs, breadcrumb_collection, path: collection_path(breadcrumb_collection), link: true)
|
|
38
|
+
append_terminal_breadcrumb(crumbs, "Edit")
|
|
39
|
+
when "new"
|
|
40
|
+
if breadcrumb_parent_collection
|
|
41
|
+
append_resource_breadcrumb(
|
|
42
|
+
crumbs,
|
|
43
|
+
breadcrumb_parent_collection,
|
|
44
|
+
path: collection_path(breadcrumb_parent_collection),
|
|
45
|
+
link: true
|
|
46
|
+
)
|
|
47
|
+
end
|
|
48
|
+
append_terminal_breadcrumb(crumbs, "New")
|
|
49
|
+
end
|
|
50
|
+
when "nquery/dashboards"
|
|
51
|
+
append_section_breadcrumb(crumbs, "Dashboards", dashboards_path)
|
|
52
|
+
|
|
53
|
+
case action_name
|
|
54
|
+
when "show"
|
|
55
|
+
append_resource_breadcrumb(crumbs, breadcrumb_dashboard)
|
|
56
|
+
when "edit"
|
|
57
|
+
append_resource_breadcrumb(crumbs, breadcrumb_dashboard, path: dashboard_path(breadcrumb_dashboard), link: true)
|
|
58
|
+
append_terminal_breadcrumb(crumbs, "Edit")
|
|
59
|
+
end
|
|
60
|
+
when "nquery/collection/dashboards"
|
|
61
|
+
append_section_breadcrumb(crumbs, "Collections", collections_path)
|
|
62
|
+
append_resource_breadcrumb(
|
|
63
|
+
crumbs,
|
|
64
|
+
breadcrumb_parent_collection,
|
|
65
|
+
path: collection_path(breadcrumb_parent_collection),
|
|
66
|
+
link: true
|
|
67
|
+
)
|
|
68
|
+
append_terminal_breadcrumb(crumbs, "New dashboard")
|
|
69
|
+
when "nquery/dashboard/charts"
|
|
70
|
+
append_section_breadcrumb(crumbs, "Dashboards", dashboards_path)
|
|
71
|
+
append_resource_breadcrumb(
|
|
72
|
+
crumbs,
|
|
73
|
+
breadcrumb_dashboard,
|
|
74
|
+
path: dashboard_path(breadcrumb_dashboard),
|
|
75
|
+
link: true
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
case action_name
|
|
79
|
+
when "show"
|
|
80
|
+
append_resource_breadcrumb(crumbs, breadcrumb_chart)
|
|
81
|
+
when "edit", "embed"
|
|
82
|
+
append_resource_breadcrumb(crumbs, breadcrumb_chart, path: dashboard_chart_path(breadcrumb_dashboard, breadcrumb_chart), link: true)
|
|
83
|
+
append_terminal_breadcrumb(crumbs, action_name.titleize)
|
|
84
|
+
when "new"
|
|
85
|
+
append_terminal_breadcrumb(crumbs, "New chart")
|
|
86
|
+
end
|
|
87
|
+
when "nquery/queries"
|
|
88
|
+
append_section_breadcrumb(crumbs, "Queries", nil)
|
|
89
|
+
append_terminal_breadcrumb(crumbs, query_breadcrumb_label)
|
|
90
|
+
when "nquery/charts"
|
|
91
|
+
append_section_breadcrumb(crumbs, "Collections", collections_path)
|
|
92
|
+
if breadcrumb_chart&.collection
|
|
93
|
+
append_resource_breadcrumb(
|
|
94
|
+
crumbs,
|
|
95
|
+
breadcrumb_chart.collection,
|
|
96
|
+
path: collection_path(breadcrumb_chart.collection),
|
|
97
|
+
link: true
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
case action_name
|
|
102
|
+
when "show"
|
|
103
|
+
append_resource_breadcrumb(crumbs, breadcrumb_chart)
|
|
104
|
+
when "edit", "embed"
|
|
105
|
+
append_resource_breadcrumb(crumbs, breadcrumb_chart, path: chart_path(breadcrumb_chart), link: true)
|
|
106
|
+
append_terminal_breadcrumb(crumbs, action_name.titleize)
|
|
107
|
+
when "new"
|
|
108
|
+
append_terminal_breadcrumb(crumbs, "New chart")
|
|
109
|
+
end
|
|
110
|
+
when "nquery/imports"
|
|
111
|
+
append_terminal_breadcrumb(crumbs, "Import CSV")
|
|
112
|
+
when "nquery/admin/logs"
|
|
113
|
+
append_section_breadcrumb(crumbs, "Logs", admin_logs_path)
|
|
114
|
+
when "nquery/admin/users"
|
|
115
|
+
append_section_breadcrumb(crumbs, "Users", admin_users_path)
|
|
116
|
+
|
|
117
|
+
case action_name
|
|
118
|
+
when "show"
|
|
119
|
+
append_resource_breadcrumb(crumbs, breadcrumb_user)
|
|
120
|
+
when "edit"
|
|
121
|
+
append_resource_breadcrumb(crumbs, breadcrumb_user, path: admin_user_path(breadcrumb_user), link: true)
|
|
122
|
+
append_terminal_breadcrumb(crumbs, "Edit user")
|
|
123
|
+
when "new"
|
|
124
|
+
append_terminal_breadcrumb(crumbs, "Invite user")
|
|
125
|
+
end
|
|
126
|
+
when "nquery/admin/groups"
|
|
127
|
+
append_section_breadcrumb(crumbs, "Groups", admin_groups_path)
|
|
128
|
+
|
|
129
|
+
case action_name
|
|
130
|
+
when "show"
|
|
131
|
+
append_resource_breadcrumb(crumbs, breadcrumb_group)
|
|
132
|
+
when "edit"
|
|
133
|
+
append_resource_breadcrumb(crumbs, breadcrumb_group, path: admin_group_path(breadcrumb_group), link: true)
|
|
134
|
+
append_terminal_breadcrumb(crumbs, "Edit group")
|
|
135
|
+
when "new"
|
|
136
|
+
append_terminal_breadcrumb(crumbs, "New group")
|
|
137
|
+
end
|
|
138
|
+
when "nquery/admin/data_sources"
|
|
139
|
+
append_section_breadcrumb(crumbs, "Data sources", admin_data_sources_path)
|
|
140
|
+
|
|
141
|
+
case action_name
|
|
142
|
+
when "edit"
|
|
143
|
+
append_resource_breadcrumb(
|
|
144
|
+
crumbs,
|
|
145
|
+
breadcrumb_data_source,
|
|
146
|
+
path: edit_admin_data_source_path(breadcrumb_data_source),
|
|
147
|
+
link: true
|
|
148
|
+
)
|
|
149
|
+
append_terminal_breadcrumb(crumbs, "Edit data source")
|
|
150
|
+
when "new"
|
|
151
|
+
append_terminal_breadcrumb(crumbs, "New data source")
|
|
152
|
+
end
|
|
153
|
+
when "nquery/admin/permissions"
|
|
154
|
+
append_section_breadcrumb(crumbs, "Permissions", by_group_admin_permissions_path)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
mark_terminal_section!(crumbs)
|
|
158
|
+
crumbs
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def breadcrumb_collection
|
|
162
|
+
@breadcrumb_collection ||= if @collection&.persisted?
|
|
163
|
+
@collection
|
|
164
|
+
elsif params[:id].present?
|
|
165
|
+
Collection.find_by(id: params[:id])
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def breadcrumb_parent_collection
|
|
170
|
+
@breadcrumb_parent_collection ||= if @parent_collection
|
|
171
|
+
@parent_collection
|
|
172
|
+
elsif @collection&.persisted?
|
|
173
|
+
@collection
|
|
174
|
+
elsif params[:collection_id].present?
|
|
175
|
+
Collection.find_by(id: params[:collection_id])
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def breadcrumb_dashboard
|
|
180
|
+
@breadcrumb_dashboard ||= if @dashboard&.persisted?
|
|
181
|
+
@dashboard
|
|
182
|
+
elsif params[:dashboard_id].present?
|
|
183
|
+
Dashboard.find_by(id: params[:dashboard_id])
|
|
184
|
+
elsif controller_path == "nquery/dashboards" && params[:id].present?
|
|
185
|
+
Dashboard.find_by(id: params[:id])
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def breadcrumb_chart
|
|
190
|
+
@breadcrumb_chart ||= if @chart&.persisted?
|
|
191
|
+
@chart
|
|
192
|
+
elsif params[:id].present?
|
|
193
|
+
Chart.find_by(id: params[:id])
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def breadcrumb_user
|
|
198
|
+
@breadcrumb_user ||= @user || User.find_by(id: params[:id])
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def breadcrumb_group
|
|
202
|
+
@breadcrumb_group ||= @group || Group.find_by(id: params[:id])
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def breadcrumb_data_source
|
|
206
|
+
@breadcrumb_data_source ||= @data_source || DataSource.find_by(id: params[:id])
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def query_breadcrumb_label
|
|
210
|
+
case action_name
|
|
211
|
+
when "new" then "New SQL query"
|
|
212
|
+
when "edit" then "Edit query: #{@query&.name.presence || 'Untitled'}"
|
|
213
|
+
else @query&.name.presence || "Query"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def append_section_breadcrumb(crumbs, label, path)
|
|
218
|
+
crumbs << { label: label, path: path, section: true }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def append_resource_breadcrumb(crumbs, resource, path: nil, link: false)
|
|
222
|
+
return unless resource&.name.present?
|
|
223
|
+
|
|
224
|
+
crumbs << { label: resource.name, path: link ? path : nil }
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def append_terminal_breadcrumb(crumbs, label)
|
|
228
|
+
crumbs << { label: label, path: nil }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def mark_terminal_section!(crumbs)
|
|
232
|
+
last_crumb = crumbs.last
|
|
233
|
+
return unless last_crumb[:section]
|
|
234
|
+
|
|
235
|
+
last_crumb[:path] = nil
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
module Browsable
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def load_collection_contents(collection)
|
|
10
|
+
@child_collections = filter_viewable_collections(collection.children.active.order(:name))
|
|
11
|
+
@charts = filter_viewable_charts(collection.charts.active.includes(:collection).order(:name))
|
|
12
|
+
@dashboards = filter_viewable_dashboards(
|
|
13
|
+
collection.dashboards.active.includes(:creator, dashboard_cards: :chart).order(:name)
|
|
14
|
+
)
|
|
15
|
+
assigned_chart_ids = @dashboards.flat_map { |dashboard| dashboard.dashboard_cards.map(&:chart_id) }
|
|
16
|
+
@standalone_charts = @charts.reject { |chart| assigned_chart_ids.include?(chart.id) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def viewable_collections
|
|
20
|
+
scope = Collection.active.includes(:parent).order(:name)
|
|
21
|
+
return scope if permission_resolver.admin?
|
|
22
|
+
|
|
23
|
+
scope.select { |collection| collection_listable?(collection) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def assignable_collections
|
|
27
|
+
Collection.shared.active.order(:name).select do |collection|
|
|
28
|
+
permission_resolver.admin? || permission_resolver.collection_access(collection) == :curate
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def filter_viewable_collections(relation)
|
|
33
|
+
return relation if permission_resolver.admin?
|
|
34
|
+
|
|
35
|
+
relation.select { |collection| viewable_collection?(collection) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def filter_viewable_charts(relation)
|
|
39
|
+
return relation if permission_resolver.admin?
|
|
40
|
+
|
|
41
|
+
relation.select { |chart| viewable_collection?(chart.collection) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def filter_viewable_dashboards(relation)
|
|
45
|
+
return relation if permission_resolver.admin?
|
|
46
|
+
|
|
47
|
+
relation.select { |dashboard| viewable_collection?(dashboard.collection) }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def collection_listable?(collection)
|
|
51
|
+
return false if collection.kind == "personal" && collection.owner_id != current_nquery_user&.id
|
|
52
|
+
|
|
53
|
+
viewable_collection?(collection)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
module ChartActions
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
include ChartResults
|
|
9
|
+
before_action :set_chart, only: %i[show edit update embed destroy archive]
|
|
10
|
+
before_action :authorize_chart_view!, only: %i[show embed]
|
|
11
|
+
before_action :authorize_chart_curate!, only: %i[edit update destroy archive]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def show
|
|
15
|
+
@result = chart_result(@chart)
|
|
16
|
+
render "nquery/charts/show"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def edit
|
|
20
|
+
load_chart_builder_assigns
|
|
21
|
+
render "nquery/charts/edit"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def update
|
|
25
|
+
if @chart.update(chart_params)
|
|
26
|
+
respond_to do |format|
|
|
27
|
+
format.html { redirect_to after_chart_save_path, notice: "Chart updated." }
|
|
28
|
+
format.turbo_stream do
|
|
29
|
+
flash.now[:notice] = "Chart updated."
|
|
30
|
+
render template: "nquery/charts/update"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
load_chart_builder_assigns
|
|
35
|
+
render "nquery/charts/edit", status: :unprocessable_content
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def destroy
|
|
40
|
+
@chart.destroy
|
|
41
|
+
redirect_to after_chart_action_path, notice: "Chart removed."
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def archive
|
|
45
|
+
@chart.archive!
|
|
46
|
+
redirect_to after_chart_action_path, notice: "Chart archived."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def embed
|
|
50
|
+
@embed_token = EmbedToken.active.find_by(resource_type: "Nquery::Chart", resource_id: @chart.id)
|
|
51
|
+
@embed_url = @embed_token ? embed_public_chart_url(token: EmbedTokenService.signed_token_for(@embed_token)) : nil
|
|
52
|
+
render "nquery/charts/embed"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def authorize_chart_view!
|
|
58
|
+
authorize_collection_access!(@chart.collection, required: :view)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def authorize_chart_curate!
|
|
62
|
+
authorize_collection_access!(@chart.collection, required: :curate)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def after_chart_save_path
|
|
66
|
+
if chart_dashboard
|
|
67
|
+
edit_dashboard_chart_path(chart_dashboard, @chart)
|
|
68
|
+
else
|
|
69
|
+
edit_chart_path(@chart)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def after_chart_action_path
|
|
74
|
+
if chart_dashboard
|
|
75
|
+
dashboard_path(chart_dashboard)
|
|
76
|
+
elsif @chart.collection
|
|
77
|
+
collection_path(@chart.collection)
|
|
78
|
+
else
|
|
79
|
+
root_path
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def chart_dashboard
|
|
84
|
+
@dashboard if defined?(@dashboard) && @dashboard.present?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def chart_params
|
|
88
|
+
params.require(:chart).permit(
|
|
89
|
+
:name,
|
|
90
|
+
visualization: {},
|
|
91
|
+
query_attributes: %i[id name statement data_source_id]
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def load_chart_builder_assigns
|
|
96
|
+
@data_sources = DataSource.active.order(:name)
|
|
97
|
+
@schema = schema_for(@chart&.query&.data_source)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def schema_for(data_source = nil)
|
|
101
|
+
Nquery::SchemaExplorer.tables_for(data_source || DataSource.first)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
module ChartResults
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def chart_result(chart)
|
|
10
|
+
return demo_result unless chart.query&.statement.present?
|
|
11
|
+
|
|
12
|
+
QueryRunner.new(
|
|
13
|
+
data_source: chart.query.data_source || DataSource.first,
|
|
14
|
+
statement: chart.query.statement,
|
|
15
|
+
user: current_nquery_user,
|
|
16
|
+
query: chart.query
|
|
17
|
+
).run
|
|
18
|
+
rescue StandardError
|
|
19
|
+
demo_result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def demo_result
|
|
23
|
+
{
|
|
24
|
+
columns: %w[month revenue],
|
|
25
|
+
rows: [%w[Jan 1200], %w[Feb 1800], %w[Mar 2400], %w[Apr 2100]],
|
|
26
|
+
row_count: 4
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|