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.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +52 -0
  4. data/Rakefile +9 -0
  5. data/app/assets/builds/nquery/application.css +339 -0
  6. data/app/assets/builds/nquery/application.js +989 -0
  7. data/app/assets/config/nquery_manifest.js +2 -0
  8. data/app/components/nquery/_card_component.html.erb +6 -0
  9. data/app/components/nquery/_card_table_component.html.erb +14 -0
  10. data/app/components/nquery/_flash_card_component.html.erb +32 -0
  11. data/app/components/nquery/_flash_cards_component.html.erb +3 -0
  12. data/app/components/nquery/_infinite_scroll_component.html.erb +11 -0
  13. data/app/components/nquery/card_component.rb +18 -0
  14. data/app/components/nquery/card_table_component.rb +19 -0
  15. data/app/components/nquery/component.rb +26 -0
  16. data/app/components/nquery/flash_card_component.rb +51 -0
  17. data/app/components/nquery/flash_cards_component.rb +20 -0
  18. data/app/components/nquery/infinite_scroll_component.rb +19 -0
  19. data/app/controllers/concerns/nquery/breadcrumbs.rb +238 -0
  20. data/app/controllers/concerns/nquery/browsable.rb +56 -0
  21. data/app/controllers/concerns/nquery/chart_actions.rb +104 -0
  22. data/app/controllers/concerns/nquery/chart_results.rb +30 -0
  23. data/app/controllers/nquery/admin/base_controller.rb +9 -0
  24. data/app/controllers/nquery/admin/data_sources_controller.rb +47 -0
  25. data/app/controllers/nquery/admin/groups_controller.rb +63 -0
  26. data/app/controllers/nquery/admin/logs_controller.rb +28 -0
  27. data/app/controllers/nquery/admin/permissions_controller.rb +53 -0
  28. data/app/controllers/nquery/admin/users_controller.rb +80 -0
  29. data/app/controllers/nquery/application_controller.rb +70 -0
  30. data/app/controllers/nquery/charts_controller.rb +51 -0
  31. data/app/controllers/nquery/collection/dashboards_controller.rb +38 -0
  32. data/app/controllers/nquery/collections_controller.rb +117 -0
  33. data/app/controllers/nquery/dashboard/charts_controller.rb +62 -0
  34. data/app/controllers/nquery/dashboards_controller.rb +90 -0
  35. data/app/controllers/nquery/embed/charts_controller.rb +24 -0
  36. data/app/controllers/nquery/embed/dashboards_controller.rb +17 -0
  37. data/app/controllers/nquery/home_controller.rb +16 -0
  38. data/app/controllers/nquery/imports_controller.rb +41 -0
  39. data/app/controllers/nquery/queries_controller.rb +105 -0
  40. data/app/controllers/nquery/registrations_controller.rb +31 -0
  41. data/app/controllers/nquery/sessions_controller.rb +28 -0
  42. data/app/helpers/nquery/charts_helper.rb +33 -0
  43. data/app/helpers/nquery/collections_helper.rb +13 -0
  44. data/app/helpers/nquery/components_helper.rb +29 -0
  45. data/app/helpers/nquery/home_helper.rb +23 -0
  46. data/app/helpers/nquery/icon_helper.rb +9 -0
  47. data/app/helpers/nquery/navigation_helper.rb +10 -0
  48. data/app/javascript/nquery/controllers/chart_preview_controller.js +125 -0
  49. data/app/javascript/nquery/controllers/csv_mapper_controller.js +23 -0
  50. data/app/javascript/nquery/controllers/dashboard_grid_controller.js +25 -0
  51. data/app/javascript/nquery/controllers/query_editor_controller.js +41 -0
  52. data/app/models/nquery/application_permission.rb +12 -0
  53. data/app/models/nquery/application_record.rb +7 -0
  54. data/app/models/nquery/audit.rb +38 -0
  55. data/app/models/nquery/chart.rb +37 -0
  56. data/app/models/nquery/collection.rb +35 -0
  57. data/app/models/nquery/collection_permission.rb +12 -0
  58. data/app/models/nquery/csv_upload.rb +11 -0
  59. data/app/models/nquery/dashboard.rb +27 -0
  60. data/app/models/nquery/dashboard_card.rb +8 -0
  61. data/app/models/nquery/data_permission.rb +13 -0
  62. data/app/models/nquery/data_source.rb +27 -0
  63. data/app/models/nquery/embed_token.rb +21 -0
  64. data/app/models/nquery/group.rb +27 -0
  65. data/app/models/nquery/group_membership.rb +10 -0
  66. data/app/models/nquery/query.rb +19 -0
  67. data/app/models/nquery/user.rb +58 -0
  68. data/app/views/layouts/nquery/application.html.erb +32 -0
  69. data/app/views/layouts/nquery/auth.html.erb +16 -0
  70. data/app/views/layouts/nquery/embed.html.erb +12 -0
  71. data/app/views/nquery/admin/data_sources/edit.html.erb +8 -0
  72. data/app/views/nquery/admin/data_sources/index.html.erb +21 -0
  73. data/app/views/nquery/admin/data_sources/new.html.erb +7 -0
  74. data/app/views/nquery/admin/groups/edit.html.erb +6 -0
  75. data/app/views/nquery/admin/groups/index.html.erb +21 -0
  76. data/app/views/nquery/admin/groups/new.html.erb +6 -0
  77. data/app/views/nquery/admin/groups/show.html.erb +24 -0
  78. data/app/views/nquery/admin/logs/index.html.erb +74 -0
  79. data/app/views/nquery/admin/permissions/_collection.html.erb +25 -0
  80. data/app/views/nquery/admin/permissions/_data_source.html.erb +27 -0
  81. data/app/views/nquery/admin/permissions/_group.html.erb +42 -0
  82. data/app/views/nquery/admin/permissions/by_collection.html.erb +10 -0
  83. data/app/views/nquery/admin/permissions/by_data_source.html.erb +7 -0
  84. data/app/views/nquery/admin/permissions/by_group.html.erb +12 -0
  85. data/app/views/nquery/admin/users/edit.html.erb +17 -0
  86. data/app/views/nquery/admin/users/index.html.erb +34 -0
  87. data/app/views/nquery/admin/users/new.html.erb +14 -0
  88. data/app/views/nquery/admin/users/show.html.erb +6 -0
  89. data/app/views/nquery/charts/_builder_form.html.erb +185 -0
  90. data/app/views/nquery/charts/_new_header.html.erb +31 -0
  91. data/app/views/nquery/charts/_schema_tree.html.erb +25 -0
  92. data/app/views/nquery/charts/edit.html.erb +6 -0
  93. data/app/views/nquery/charts/embed.html.erb +15 -0
  94. data/app/views/nquery/charts/new.html.erb +5 -0
  95. data/app/views/nquery/charts/show.html.erb +15 -0
  96. data/app/views/nquery/charts/update.turbo_stream.erb +1 -0
  97. data/app/views/nquery/collection/dashboards/new.html.erb +13 -0
  98. data/app/views/nquery/collections/_dashboard_card.html.erb +57 -0
  99. data/app/views/nquery/collections/edit.html.erb +15 -0
  100. data/app/views/nquery/collections/index.html.erb +41 -0
  101. data/app/views/nquery/collections/new.html.erb +15 -0
  102. data/app/views/nquery/collections/show.html.erb +100 -0
  103. data/app/views/nquery/dashboard/charts/new.html.erb +6 -0
  104. data/app/views/nquery/dashboards/_chart_card.html.erb +16 -0
  105. data/app/views/nquery/dashboards/edit.html.erb +34 -0
  106. data/app/views/nquery/dashboards/index.html.erb +35 -0
  107. data/app/views/nquery/dashboards/show.html.erb +46 -0
  108. data/app/views/nquery/embed/charts/show.html.erb +9 -0
  109. data/app/views/nquery/embed/dashboards/show.html.erb +7 -0
  110. data/app/views/nquery/home/index.html.erb +143 -0
  111. data/app/views/nquery/imports/new.html.erb +20 -0
  112. data/app/views/nquery/queries/edit.html.erb +40 -0
  113. data/app/views/nquery/queries/new.html.erb +37 -0
  114. data/app/views/nquery/registrations/new.html.erb +34 -0
  115. data/app/views/nquery/sessions/new.html.erb +29 -0
  116. data/app/views/nquery/shared/_auth_shell.html.erb +22 -0
  117. data/app/views/nquery/shared/_breadcrumbs.html.erb +15 -0
  118. data/app/views/nquery/shared/_chart_actions_menu.html.erb +27 -0
  119. data/app/views/nquery/shared/_chart_row_actions.html.erb +16 -0
  120. data/app/views/nquery/shared/_flash.html.erb +1 -0
  121. data/app/views/nquery/shared/_icon.html.erb +50 -0
  122. data/app/views/nquery/shared/_sidebar.html.erb +16 -0
  123. data/app/views/nquery/shared/_topbar.html.erb +12 -0
  124. data/config/importmap.rb +9 -0
  125. data/config/routes.rb +78 -0
  126. data/db/migrate/20260721000001_create_nquery_tables.rb +146 -0
  127. data/db/migrate/20260721180000_add_archived_at_to_nquery_dashboards.rb +8 -0
  128. data/db/migrate/20260721190000_add_archived_at_to_nquery_charts.rb +8 -0
  129. data/db/migrate/20260721200000_add_archived_at_to_nquery_collections.rb +8 -0
  130. data/lib/generators/nquery/install_generator.rb +38 -0
  131. data/lib/nquery/authorizes_collection.rb +50 -0
  132. data/lib/nquery/configuration.rb +44 -0
  133. data/lib/nquery/csv_importer.rb +68 -0
  134. data/lib/nquery/data_sources/adapter.rb +34 -0
  135. data/lib/nquery/data_sources/mysql_adapter.rb +24 -0
  136. data/lib/nquery/data_sources/postgresql_adapter.rb +46 -0
  137. data/lib/nquery/data_sources/rails_adapter.rb +54 -0
  138. data/lib/nquery/data_sources/sqlite_adapter.rb +8 -0
  139. data/lib/nquery/embed_token_service.rb +83 -0
  140. data/lib/nquery/engine.rb +55 -0
  141. data/lib/nquery/permissions/resolver.rb +83 -0
  142. data/lib/nquery/query_runner.rb +69 -0
  143. data/lib/nquery/sample_data/ecommerce.rb +157 -0
  144. data/lib/nquery/schema_explorer.rb +28 -0
  145. data/lib/nquery/seeder.rb +131 -0
  146. data/lib/nquery/version.rb +5 -0
  147. data/lib/nquery.rb +36 -0
  148. data/lib/tasks/nquery.rake +9 -0
  149. data/nquery.gemspec +40 -0
  150. metadata +395 -0
@@ -0,0 +1,2 @@
1
+ //= link nquery/application.css
2
+ //= link nquery/application.js
@@ -0,0 +1,6 @@
1
+ <div class="<%= component.css_class %>">
2
+ <% if component.title.present? %>
3
+ <h2 class="nq-card-title"><%= component.title %></h2>
4
+ <% end %>
5
+ <%= content %>
6
+ </div>
@@ -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,3 @@
1
+ <% component.cards.each do |card| %>
2
+ <%= render_nquery_component(card) %>
3
+ <% end %>
@@ -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
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Admin
5
+ class BaseController < ApplicationController
6
+ before_action :require_admin!
7
+ end
8
+ end
9
+ end