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,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Admin
5
+ class DataSourcesController < BaseController
6
+ before_action :set_data_source, only: %i[edit update]
7
+
8
+ def index
9
+ @data_sources = DataSource.order(:name)
10
+ end
11
+
12
+ def new
13
+ @data_source = DataSource.new(adapter: "postgresql")
14
+ end
15
+
16
+ def create
17
+ @data_source = DataSource.new(data_source_params)
18
+ if @data_source.save
19
+ redirect_to admin_data_sources_path, notice: "Data source created."
20
+ else
21
+ render :new, status: :unprocessable_content
22
+ end
23
+ end
24
+
25
+ def edit
26
+ end
27
+
28
+ def update
29
+ if @data_source.update(data_source_params)
30
+ redirect_to admin_data_sources_path, notice: "Data source updated."
31
+ else
32
+ render :edit, status: :unprocessable_content
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def set_data_source
39
+ @data_source = DataSource.find(params[:id])
40
+ end
41
+
42
+ def data_source_params
43
+ params.require(:data_source).permit(:name, :adapter, :active, :connection_config)
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Admin
5
+ class GroupsController < BaseController
6
+ before_action :set_group, only: %i[show edit update add_member remove_member]
7
+
8
+ def index
9
+ @groups = Group.includes(:users).order(:name)
10
+ end
11
+
12
+ def show
13
+ @available_users = User.active.where.not(id: @group.user_ids).order(:email)
14
+ end
15
+
16
+ def new
17
+ @group = Group.new
18
+ end
19
+
20
+ def create
21
+ @group = Group.new(group_params.merge(system_group: "custom"))
22
+ if @group.save
23
+ redirect_to admin_groups_path, notice: "Group created."
24
+ else
25
+ render :new, status: :unprocessable_content
26
+ end
27
+ end
28
+
29
+ def edit
30
+ end
31
+
32
+ def update
33
+ if @group.update(group_params)
34
+ redirect_to admin_groups_path, notice: "Group updated."
35
+ else
36
+ render :edit, status: :unprocessable_content
37
+ end
38
+ end
39
+
40
+ def add_member
41
+ user = User.find(params[:user_id])
42
+ @group.group_memberships.find_or_create_by!(user: user)
43
+ redirect_to admin_group_path(@group), notice: "#{user.name} added to group."
44
+ end
45
+
46
+ def remove_member
47
+ membership = @group.group_memberships.find_by!(user_id: params[:user_id])
48
+ membership.destroy unless @group.system_group == "all_users"
49
+ redirect_to admin_group_path(@group), notice: "Member removed."
50
+ end
51
+
52
+ private
53
+
54
+ def set_group
55
+ @group = Group.includes(:users).find(params[:id])
56
+ end
57
+
58
+ def group_params
59
+ params.require(:group).permit(:name, :description)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Admin
5
+ class LogsController < BaseController
6
+ def index
7
+ @collections = Collection.order(:name)
8
+ @dashboards = Dashboard.order(:name)
9
+ @audits = filtered_audits
10
+ .includes(:user, query: [:collection, { chart: { dashboard_cards: :dashboard } }])
11
+ .recent
12
+ .limit(100)
13
+ end
14
+
15
+ private
16
+
17
+ def filtered_audits
18
+ scope = Audit.all
19
+ scope = scope.for_user(params[:user]) if params[:user].present?
20
+ scope = scope.for_collection(params[:collection]) if params[:collection].present?
21
+ scope = scope.for_dashboard(params[:dashboard]) if params[:dashboard].present?
22
+ scope = scope.since(params[:from]) if params[:from].present?
23
+ scope = scope.until_date(params[:to]) if params[:to].present?
24
+ scope
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Admin
5
+ class PermissionsController < BaseController
6
+ def index
7
+ redirect_to by_group_admin_permissions_path
8
+ end
9
+
10
+ def by_group
11
+ @view = "group"
12
+ @groups = Group.order(:name)
13
+ @selected_group = Group.find_by(id: params[:group_id]) || @groups.first
14
+ @collections = Collection.shared.order(:name)
15
+ @data_sources = DataSource.order(:name)
16
+ @warnings = permission_warnings(@selected_group)
17
+ end
18
+
19
+ def by_data_source
20
+ @view = "data_source"
21
+ @data_sources = DataSource.order(:name)
22
+ @selected_data_source = DataSource.find_by(id: params[:data_source_id]) || @data_sources.first
23
+ @groups = Group.order(:name)
24
+ end
25
+
26
+ def by_collection
27
+ @view = "collection"
28
+ @collections = Collection.shared.order(:name)
29
+ @selected_collection = Collection.find_by(id: params[:collection_id]) || @collections.first
30
+ @groups = Group.order(:name)
31
+ @warnings = @groups.filter_map do |group|
32
+ "All Users is more permissive than #{group.name}" if permission_resolver.all_users_more_permissive?(group, collection: @selected_collection)
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def permission_warnings(group)
39
+ return [] unless group
40
+
41
+ Collection.shared.filter_map do |collection|
42
+ next unless permission_resolver.all_users_more_permissive?(group, collection: collection)
43
+
44
+ "All Users has broader access than #{group.name} on #{collection.name}"
45
+ end
46
+ end
47
+
48
+ def permission_resolver
49
+ @permission_resolver ||= Permissions::Resolver.new(current_nquery_user)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Admin
5
+ class UsersController < BaseController
6
+ before_action :set_user, only: %i[show edit update deactivate destroy]
7
+
8
+ def index
9
+ @users = User.active.includes(:groups).order(:email)
10
+ end
11
+
12
+ def show
13
+ end
14
+
15
+ def new
16
+ @user = User.new
17
+ @groups = Group.order(:name)
18
+ end
19
+
20
+ def create
21
+ @user = User.new(user_params)
22
+ if @user.save
23
+ assign_groups(@user)
24
+ @user.ensure_all_users_membership!
25
+ @user.ensure_personal_collection!
26
+ redirect_to admin_users_path, notice: "User created."
27
+ else
28
+ @groups = Group.order(:name)
29
+ render :new, status: :unprocessable_content
30
+ end
31
+ end
32
+
33
+ def edit
34
+ @groups = Group.order(:name)
35
+ end
36
+
37
+ def update
38
+ if @user.update(user_params)
39
+ assign_groups(@user)
40
+ redirect_to admin_users_path, notice: "User updated."
41
+ else
42
+ @groups = Group.order(:name)
43
+ render :edit, status: :unprocessable_content
44
+ end
45
+ end
46
+
47
+ def deactivate
48
+ return redirect_to admin_users_path, alert: "You cannot disable your own account." if @user == current_nquery_user
49
+
50
+ @user.deactivate!
51
+ redirect_to admin_users_path, notice: "#{@user.name} has been disabled."
52
+ end
53
+
54
+ def destroy
55
+ return redirect_to admin_users_path, alert: "You cannot remove your own account." if @user == current_nquery_user
56
+
57
+ name = @user.name
58
+ @user.destroy!
59
+ redirect_to admin_users_path, notice: "#{name} has been removed."
60
+ end
61
+
62
+ private
63
+
64
+ def set_user
65
+ @user = User.find(params[:id])
66
+ end
67
+
68
+ def user_params
69
+ params.require(:user).permit(:email, :first_name, :last_name, :password, :password_confirmation)
70
+ end
71
+
72
+ def assign_groups(user)
73
+ group_ids = params[:group_ids]&.reject(&:blank?) || []
74
+ user.group_memberships.where.not(group_id: group_ids).destroy_all
75
+ group_ids.each { |gid| user.group_memberships.find_or_create_by!(group_id: gid) }
76
+ user.ensure_all_users_membership!
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class ApplicationController < ActionController::Base
5
+ include Nquery::AuthorizesCollection
6
+ include Nquery::Breadcrumbs
7
+ include Nquery::Engine.routes.url_helpers
8
+
9
+ protect_from_forgery with: :exception
10
+ layout "nquery/application"
11
+
12
+ helper_method :current_nquery_user, :permission_resolver
13
+
14
+ before_action :_authenticate!
15
+ before_action :set_current_user_context
16
+
17
+ private
18
+
19
+ def _authenticate!
20
+ if Nquery.configuration.authenticate
21
+ instance_eval(&Nquery.configuration.authenticate)
22
+ else
23
+ default_authenticate!
24
+ end
25
+ end
26
+
27
+ def default_authenticate!
28
+ return if current_nquery_user
29
+ return if auth_exempt?
30
+
31
+ redirect_to login_path, alert: "Please sign in to continue."
32
+ end
33
+
34
+ def auth_exempt?
35
+ controller_path.in?(%w[nquery/sessions nquery/registrations nquery/embed/charts nquery/embed/dashboards])
36
+ end
37
+
38
+ def current_nquery_user
39
+ @current_nquery_user ||= resolve_current_user
40
+ end
41
+
42
+ def resolve_current_user
43
+ if session[:nquery_user_id]
44
+ return User.active.find_by(id: session[:nquery_user_id])
45
+ end
46
+
47
+ if Nquery.configuration.authentication_mode.in?(%i[sso hybrid])
48
+ host_method = Nquery.configuration.current_user_method
49
+ host_user = host_method.is_a?(Symbol) ? send(host_method) : instance_eval(&host_method)
50
+ if host_user && Nquery.configuration.resolve_user
51
+ return instance_eval { Nquery.configuration.resolve_user.call(host_user) }
52
+ end
53
+ end
54
+
55
+ nil
56
+ end
57
+
58
+ def set_current_user_context
59
+ @permission_resolver = Permissions::Resolver.new(current_nquery_user) if current_nquery_user
60
+ end
61
+
62
+ def permission_resolver
63
+ @permission_resolver ||= Permissions::Resolver.new(current_nquery_user)
64
+ end
65
+
66
+ def require_admin!
67
+ redirect_to root_path, alert: "Admin access required." unless permission_resolver.admin?
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class ChartsController < ApplicationController
5
+ include ChartActions
6
+
7
+ before_action :set_root_collection, only: %i[new create]
8
+ before_action :authorize_chart_create!, only: %i[new create]
9
+
10
+ def new
11
+ @chart = Chart.new
12
+ @chart.build_query(statement: "SELECT 1 AS example")
13
+ load_chart_builder_assigns
14
+ end
15
+
16
+ def create
17
+ @chart = Chart.new(
18
+ chart_params.merge(
19
+ creator: current_nquery_user,
20
+ collection: @root_collection
21
+ )
22
+ )
23
+ @chart.name = @chart.name.presence || @chart.query&.name
24
+ @chart.visualization = { "type" => "table" } if @chart.visualization.blank?
25
+ @chart.query&.creator = current_nquery_user
26
+ @chart.query&.collection = @root_collection
27
+
28
+ if @chart.save
29
+ redirect_to edit_chart_path(@chart), notice: "Chart created."
30
+ else
31
+ load_chart_builder_assigns
32
+ render :new, status: :unprocessable_content
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ def set_root_collection
39
+ @root_collection = Collection.roots.first
40
+ redirect_to root_path, alert: "No collection available." unless @root_collection
41
+ end
42
+
43
+ def authorize_chart_create!
44
+ authorize_collection_access!(@root_collection, required: :curate)
45
+ end
46
+
47
+ def set_chart
48
+ @chart = Chart.find(params[:id])
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class Collection::DashboardsController < ApplicationController
5
+ include Browsable
6
+
7
+ before_action :set_collection
8
+ before_action :authorize_collection_curate!
9
+
10
+ def new
11
+ @dashboard = Dashboard.new(collection: @collection)
12
+ end
13
+
14
+ def create
15
+ @dashboard = Dashboard.new(dashboard_params.merge(collection: @collection, creator: current_nquery_user))
16
+
17
+ if @dashboard.save
18
+ redirect_to dashboard_path(@dashboard), notice: "Dashboard created."
19
+ else
20
+ render :new, status: :unprocessable_content
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def set_collection
27
+ @collection = Collection.find(params[:collection_id])
28
+ end
29
+
30
+ def authorize_collection_curate!
31
+ authorize_collection_access!(@collection, required: :curate)
32
+ end
33
+
34
+ def dashboard_params
35
+ params.require(:dashboard).permit(:name, :description, settings: {})
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class CollectionsController < ApplicationController
5
+ include Browsable
6
+
7
+ before_action :set_collection, only: %i[show edit update destroy archive unarchive]
8
+ before_action :set_parent_collection, only: %i[new create]
9
+ before_action :authorize_collection_view!, only: %i[show]
10
+ before_action :authorize_collection_curate!, only: %i[edit update destroy archive unarchive]
11
+ before_action :authorize_parent_curate!, only: %i[new create]
12
+
13
+ before_action :prevent_root_deletion, only: %i[destroy]
14
+ before_action :prevent_root_archival, only: %i[archive]
15
+
16
+ def index
17
+ @collections = viewable_collections
18
+ end
19
+
20
+ def show
21
+ load_collection_contents(@collection)
22
+ end
23
+
24
+ def new
25
+ @collection = Collection.new(kind: "standard", parent: @parent_collection)
26
+ @parent_collections = assignable_collections
27
+ end
28
+
29
+ def create
30
+ @collection = Collection.new(collection_params.merge(kind: "standard", parent: @parent_collection))
31
+ @parent_collections = assignable_collections
32
+
33
+ if @collection.save
34
+ redirect_to collection_path(@collection), notice: "Collection created."
35
+ else
36
+ render :new, status: :unprocessable_content
37
+ end
38
+ end
39
+
40
+ def edit
41
+ @parent_collections = assignable_collections.reject { |c| c.id == @collection.id }
42
+ end
43
+
44
+ def update
45
+ @parent_collections = assignable_collections.reject { |c| c.id == @collection.id }
46
+ if collection_params[:parent_id].present?
47
+ parent = Collection.find_by(id: collection_params[:parent_id])
48
+ authorize_collection_access!(parent, required: :curate) if parent
49
+ end
50
+
51
+ if @collection.update(collection_params)
52
+ redirect_to collection_path(@collection), notice: "Collection updated."
53
+ else
54
+ render :edit, status: :unprocessable_content
55
+ end
56
+ end
57
+
58
+ def destroy
59
+ @collection.destroy
60
+ redirect_to collections_path, notice: "Collection deleted."
61
+ end
62
+
63
+ def archive
64
+ @collection.archive!
65
+ redirect_to collections_path, notice: "Collection archived."
66
+ end
67
+
68
+ def unarchive
69
+ @collection.unarchive!
70
+ redirect_to collection_path(@collection), notice: "Collection unarchived."
71
+ end
72
+
73
+ private
74
+
75
+ def set_collection
76
+ @collection = Collection.find(params[:id])
77
+ end
78
+
79
+ def set_parent_collection
80
+ return if params[:collection_id].blank?
81
+
82
+ @parent_collection = Collection.find(params[:collection_id])
83
+ end
84
+
85
+ def authorize_parent_curate!
86
+ parent_id = params.dig(:collection, :parent_id)
87
+ parent = @parent_collection || Collection.find_by(id: parent_id)
88
+ return if parent.nil?
89
+
90
+ authorize_collection_access!(parent, required: :curate)
91
+ end
92
+
93
+ def authorize_collection_view!
94
+ authorize_collection_access!(@collection, required: :view)
95
+ end
96
+
97
+ def authorize_collection_curate!
98
+ authorize_collection_access!(@collection, required: :curate)
99
+ end
100
+
101
+ def prevent_root_deletion
102
+ return unless @collection.kind == "root"
103
+
104
+ redirect_to collections_path, alert: "Root collections cannot be deleted."
105
+ end
106
+
107
+ def prevent_root_archival
108
+ return unless @collection.kind == "root"
109
+
110
+ redirect_to collections_path, alert: "Root collections cannot be archived."
111
+ end
112
+
113
+ def collection_params
114
+ params.require(:collection).permit(:name, :parent_id)
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class Dashboard::ChartsController < ApplicationController
5
+ include ChartActions
6
+
7
+ prepend_before_action :set_dashboard
8
+ before_action :authorize_dashboard_view!, only: %i[show embed]
9
+ before_action :authorize_dashboard_curate!, except: %i[show embed]
10
+
11
+ def new
12
+ @chart = Chart.new
13
+ @chart.build_query(statement: "SELECT 1 AS example")
14
+ load_chart_builder_assigns
15
+ end
16
+
17
+ def create
18
+ @chart = Chart.new(
19
+ chart_params.merge(
20
+ creator: current_nquery_user,
21
+ collection: @dashboard.collection
22
+ )
23
+ )
24
+ @chart.name = @chart.name.presence || @chart.query&.name
25
+ @chart.visualization = { "type" => "table" } if @chart.visualization.blank?
26
+ @chart.query&.creator = current_nquery_user
27
+ @chart.query&.collection = @dashboard.collection
28
+
29
+ if @chart.save
30
+ @dashboard.dashboard_cards.create!(
31
+ chart: @chart,
32
+ pos_x: 0,
33
+ pos_y: 0,
34
+ width: 6,
35
+ height: 4
36
+ )
37
+ redirect_to edit_dashboard_chart_path(@dashboard, @chart), notice: "Chart created."
38
+ else
39
+ load_chart_builder_assigns
40
+ render :new, status: :unprocessable_content
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def set_dashboard
47
+ @dashboard = Dashboard.find(params[:dashboard_id])
48
+ end
49
+
50
+ def set_chart
51
+ @chart = @dashboard.charts.find(params[:id])
52
+ end
53
+
54
+ def authorize_dashboard_view!
55
+ authorize_collection_access!(@dashboard.collection, required: :view)
56
+ end
57
+
58
+ def authorize_dashboard_curate!
59
+ authorize_collection_access!(@dashboard.collection, required: :curate)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class DashboardsController < ApplicationController
5
+ include ChartResults
6
+ include Browsable
7
+
8
+ before_action :set_dashboard, only: %i[show edit update destroy archive unarchive update_layout]
9
+ before_action :authorize_dashboard_view!, only: %i[show]
10
+ before_action :authorize_dashboard_curate!, only: %i[edit update destroy archive unarchive update_layout]
11
+
12
+ def index
13
+ @dashboards = filter_viewable_dashboards(Dashboard.active.includes(:collection).order(:name))
14
+ end
15
+
16
+ def show
17
+ @dashboard_cards = @dashboard.dashboard_cards
18
+ .joins(:chart)
19
+ .merge(Chart.active)
20
+ .includes(:chart)
21
+ @card_results = @dashboard_cards.index_with { |card| chart_result(card.chart) }
22
+ end
23
+
24
+ def edit
25
+ @collections = assignable_collections
26
+ end
27
+
28
+ def update
29
+ @collections = assignable_collections
30
+ authorize_collection_access!(Collection.find(dashboard_params[:collection_id]), required: :curate) if dashboard_params[:collection_id].present?
31
+
32
+ if @dashboard.update(dashboard_params)
33
+ redirect_to dashboard_path(@dashboard), notice: "Dashboard updated."
34
+ else
35
+ render :edit, status: :unprocessable_content
36
+ end
37
+ end
38
+
39
+ def destroy
40
+ @dashboard.destroy
41
+ redirect_to dashboards_path, notice: "Dashboard removed."
42
+ end
43
+
44
+ def archive
45
+ @dashboard.archive!
46
+ redirect_to dashboards_path, notice: "Dashboard archived."
47
+ end
48
+
49
+ def unarchive
50
+ @dashboard.unarchive!
51
+ redirect_to dashboard_path(@dashboard), notice: "Dashboard unarchived."
52
+ end
53
+
54
+ def update_layout
55
+ card_layouts.each do |card_id, layout|
56
+ card = @dashboard.dashboard_cards.find(card_id)
57
+ card.update(pos_x: layout[:x], pos_y: layout[:y], width: layout[:w], height: layout[:h])
58
+ end
59
+ head :ok
60
+ end
61
+
62
+ private
63
+
64
+ def set_dashboard
65
+ @dashboard = if action_name.in?(%w[show edit])
66
+ Dashboard.includes(:creator, dashboard_cards: :chart).find(params[:id])
67
+ else
68
+ Dashboard.find(params[:id])
69
+ end
70
+ end
71
+
72
+ def authorize_dashboard_view!
73
+ authorize_collection_access!(@dashboard.collection, required: :view)
74
+ end
75
+
76
+ def authorize_dashboard_curate!
77
+ authorize_collection_access!(@dashboard.collection, required: :curate)
78
+ end
79
+
80
+ def card_layouts
81
+ params.permit(cards: {}).fetch(:cards, {}).transform_values do |layout|
82
+ layout.permit(:x, :y, :w, :h).to_h.symbolize_keys
83
+ end
84
+ end
85
+
86
+ def dashboard_params
87
+ params.require(:dashboard).permit(:name, :description, :collection_id, settings: {})
88
+ end
89
+ end
90
+ end