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,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class Chart < ApplicationRecord
5
+ CHART_TYPES = %w[table number bar line area pie scatter].freeze
6
+
7
+ belongs_to :query, class_name: "Nquery::Query", optional: true
8
+ belongs_to :collection, class_name: "Nquery::Collection", optional: true
9
+ belongs_to :creator, class_name: "Nquery::User", optional: true
10
+ has_many :dashboard_cards, class_name: "Nquery::DashboardCard", dependent: :destroy
11
+ has_many :embed_tokens, -> { where(resource_type: "Nquery::Chart") },
12
+ class_name: "Nquery::EmbedToken", foreign_key: :resource_id, dependent: :destroy
13
+
14
+ accepts_nested_attributes_for :query
15
+
16
+ scope :active, -> { where(archived_at: nil) }
17
+ scope :archived, -> { where.not(archived_at: nil) }
18
+
19
+ validates :name, presence: true
20
+
21
+ def archived?
22
+ archived_at.present?
23
+ end
24
+
25
+ def archive!
26
+ update!(archived_at: Time.current)
27
+ end
28
+
29
+ def unarchive!
30
+ update!(archived_at: nil)
31
+ end
32
+
33
+ def chart_type
34
+ visualization["type"] || "bar"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class Collection < ApplicationRecord
5
+ KINDS = %w[root personal standard].freeze
6
+
7
+ belongs_to :parent, class_name: "Nquery::Collection", optional: true
8
+ belongs_to :owner, class_name: "Nquery::User", optional: true
9
+ has_many :children, class_name: "Nquery::Collection", foreign_key: :parent_id, dependent: :destroy
10
+ has_many :collection_permissions, class_name: "Nquery::CollectionPermission", dependent: :destroy
11
+ has_many :queries, class_name: "Nquery::Query", dependent: :nullify
12
+ has_many :charts, class_name: "Nquery::Chart", dependent: :nullify
13
+ has_many :dashboards, class_name: "Nquery::Dashboard", dependent: :nullify
14
+
15
+ validates :name, presence: true
16
+ validates :kind, inclusion: { in: KINDS }
17
+
18
+ scope :roots, -> { where(kind: "root") }
19
+ scope :shared, -> { where(kind: %w[root standard]) }
20
+ scope :active, -> { where(archived_at: nil) }
21
+ scope :archived, -> { where.not(archived_at: nil) }
22
+
23
+ def archived?
24
+ archived_at.present?
25
+ end
26
+
27
+ def archive!
28
+ update!(archived_at: Time.current)
29
+ end
30
+
31
+ def unarchive!
32
+ update!(archived_at: nil)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class CollectionPermission < ApplicationRecord
5
+ ACCESS_LEVELS = %w[no_access view curate].freeze
6
+
7
+ belongs_to :group, class_name: "Nquery::Group"
8
+ belongs_to :collection, class_name: "Nquery::Collection"
9
+
10
+ validates :access_level, inclusion: { in: ACCESS_LEVELS }
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class CsvUpload < ApplicationRecord
5
+ STATUSES = %w[pending processing completed failed].freeze
6
+
7
+ belongs_to :creator, class_name: "Nquery::User", optional: true
8
+
9
+ validates :status, inclusion: { in: STATUSES }
10
+ end
11
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class Dashboard < ApplicationRecord
5
+ belongs_to :collection, class_name: "Nquery::Collection", optional: true
6
+ belongs_to :creator, class_name: "Nquery::User", optional: true
7
+ has_many :dashboard_cards, class_name: "Nquery::DashboardCard", dependent: :destroy
8
+ has_many :charts, through: :dashboard_cards, class_name: "Nquery::Chart"
9
+
10
+ scope :active, -> { where(archived_at: nil) }
11
+ scope :archived, -> { where.not(archived_at: nil) }
12
+
13
+ validates :name, presence: true
14
+
15
+ def archived?
16
+ archived_at.present?
17
+ end
18
+
19
+ def archive!
20
+ update!(archived_at: Time.current)
21
+ end
22
+
23
+ def unarchive!
24
+ update!(archived_at: nil)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class DashboardCard < ApplicationRecord
5
+ belongs_to :dashboard, class_name: "Nquery::Dashboard"
6
+ belongs_to :chart, class_name: "Nquery::Chart"
7
+ end
8
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class DataPermission < ApplicationRecord
5
+ PERMISSION_TYPES = %w[view_data create_queries download_results manage_database].freeze
6
+
7
+ belongs_to :group, class_name: "Nquery::Group"
8
+ belongs_to :data_source, class_name: "Nquery::DataSource"
9
+
10
+ validates :permission_type, inclusion: { in: PERMISSION_TYPES }
11
+ validates :access_level, presence: true
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class DataSource < ApplicationRecord
5
+ ADAPTERS = %w[rails postgresql mysql sqlite].freeze
6
+
7
+ has_many :data_permissions, class_name: "Nquery::DataPermission", dependent: :destroy
8
+ has_many :queries, class_name: "Nquery::Query", dependent: :nullify
9
+
10
+ validates :name, presence: true
11
+ validates :adapter, inclusion: { in: ADAPTERS }
12
+
13
+ scope :active, -> { all }
14
+
15
+ def connection_config_hash
16
+ return {} if connection_config.blank?
17
+
18
+ JSON.parse(connection_config)
19
+ rescue JSON::ParserError
20
+ {}
21
+ end
22
+
23
+ def connection_config_hash=(hash)
24
+ self.connection_config = hash.to_json
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class EmbedToken < ApplicationRecord
5
+ belongs_to :creator, class_name: "Nquery::User", optional: true
6
+
7
+ validates :token, presence: true, uniqueness: true
8
+ validates :resource_type, presence: true
9
+ validates :resource_id, presence: true
10
+
11
+ scope :active, -> { where(active: true).where("expires_at IS NULL OR expires_at > ?", Time.current) }
12
+
13
+ def resource
14
+ resource_type.constantize.find_by(id: resource_id)
15
+ end
16
+
17
+ def expired?
18
+ expires_at.present? && expires_at <= Time.current
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class Group < ApplicationRecord
5
+ SYSTEM_GROUPS = %w[administrators all_users custom].freeze
6
+
7
+ has_many :group_memberships, class_name: "Nquery::GroupMembership", dependent: :destroy
8
+ has_many :users, through: :group_memberships, class_name: "Nquery::User"
9
+ has_many :data_permissions, class_name: "Nquery::DataPermission", dependent: :destroy
10
+ has_many :collection_permissions, class_name: "Nquery::CollectionPermission", dependent: :destroy
11
+ has_many :application_permissions, class_name: "Nquery::ApplicationPermission", dependent: :destroy
12
+
13
+ validates :name, presence: true
14
+ validates :system_group, inclusion: { in: SYSTEM_GROUPS }
15
+
16
+ scope :custom, -> { where(system_group: "custom") }
17
+ scope :system, -> { where.not(system_group: "custom") }
18
+
19
+ def system?
20
+ system_group != "custom"
21
+ end
22
+
23
+ def deletable?
24
+ system_group == "custom"
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class GroupMembership < ApplicationRecord
5
+ belongs_to :user, class_name: "Nquery::User"
6
+ belongs_to :group, class_name: "Nquery::Group"
7
+
8
+ validates :user_id, uniqueness: { scope: :group_id }
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class Query < ApplicationRecord
5
+ belongs_to :data_source, class_name: "Nquery::DataSource", optional: true
6
+ belongs_to :creator, class_name: "Nquery::User", optional: true
7
+ belongs_to :collection, class_name: "Nquery::Collection", optional: true
8
+ has_one :chart, class_name: "Nquery::Chart", dependent: :destroy
9
+ has_many :audits, class_name: "Nquery::Audit", dependent: :nullify
10
+
11
+ validates :name, presence: true, on: :update
12
+
13
+ before_validation :set_default_name, on: :create
14
+
15
+ def set_default_name
16
+ self.name ||= "Untitled query"
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class User < ApplicationRecord
5
+ has_secure_password validations: false
6
+
7
+ has_many :group_memberships, class_name: "Nquery::GroupMembership", dependent: :destroy
8
+ has_many :groups, through: :group_memberships, class_name: "Nquery::Group"
9
+ has_many :queries, class_name: "Nquery::Query", foreign_key: :creator_id, inverse_of: :creator, dependent: :nullify
10
+ has_many :charts, class_name: "Nquery::Chart", foreign_key: :creator_id, inverse_of: :creator, dependent: :nullify
11
+ has_many :dashboards, class_name: "Nquery::Dashboard", foreign_key: :creator_id, inverse_of: :creator, dependent: :nullify
12
+ has_many :audits, class_name: "Nquery::Audit", dependent: :nullify
13
+ has_many :csv_uploads, class_name: "Nquery::CsvUpload", foreign_key: :creator_id, inverse_of: :creator, dependent: :nullify
14
+ has_many :embed_tokens, class_name: "Nquery::EmbedToken", foreign_key: :creator_id, inverse_of: :creator, dependent: :nullify
15
+ has_one :personal_collection, -> { where(kind: "personal") },
16
+ class_name: "Nquery::Collection", foreign_key: :owner_id, inverse_of: :owner, dependent: :destroy
17
+
18
+ validates :email, presence: true, uniqueness: true
19
+ validates :password, length: { minimum: 8 }, if: -> { password.present? }
20
+
21
+ scope :active, -> { where(deactivated_at: nil) }
22
+
23
+ def name
24
+ [first_name, last_name].compact_blank.join(" ").presence || email
25
+ end
26
+
27
+ def active?
28
+ deactivated_at.nil?
29
+ end
30
+
31
+ def deactivate!
32
+ update!(deactivated_at: Time.current)
33
+ end
34
+
35
+ def admin?
36
+ Permissions::Resolver.new(self).admin?
37
+ end
38
+
39
+ def self.find_or_create_from_sso!(host_user)
40
+ external_id = host_user.id.to_s
41
+ find_or_create_by!(external_id: external_id) do |user|
42
+ user.email = host_user.try(:email) || "#{external_id}@sso.local"
43
+ user.first_name = host_user.try(:first_name) || host_user.try(:name)
44
+ end.tap(&:ensure_all_users_membership!)
45
+ end
46
+
47
+ def ensure_all_users_membership!
48
+ all_users = Group.find_by!(system_group: "all_users")
49
+ group_memberships.find_or_create_by!(group: all_users)
50
+ end
51
+
52
+ def ensure_personal_collection!
53
+ return personal_collection if personal_collection
54
+
55
+ create_personal_collection!(name: "#{name}'s Personal Collection", kind: "personal")
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,32 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>nquery</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+ <%= stylesheet_link_tag "nquery/application", "data-turbo-track": "reload" %>
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.css">
10
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/theme/eclipse.min.css">
11
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/lib/codemirror.min.js"></script>
13
+ <script src="https://cdn.jsdelivr.net/npm/codemirror@5.65.16/mode/sql/sql.min.js"></script>
14
+ <script src="https://cdn.jsdelivr.net/npm/sql-formatter@15.4.2/dist/sql-formatter.min.js"></script>
15
+ <%= javascript_include_tag "nquery/application", defer: true %>
16
+ </head>
17
+ <body class="nq-body">
18
+ <div class="nq-layout">
19
+ <%= render "nquery/shared/sidebar" %>
20
+ <div class="nq-main">
21
+ <%= render "nquery/shared/topbar" %>
22
+ <main class="nq-content" id="main_content">
23
+ <%= render "nquery/shared/breadcrumbs" %>
24
+ <%= yield %>
25
+ </main>
26
+ </div>
27
+ </div>
28
+ <div id="flash" class="nq-toast-stack" aria-live="polite" aria-atomic="false">
29
+ <%= render "nquery/shared/flash" %>
30
+ </div>
31
+ </body>
32
+ </html>
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>nquery</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= stylesheet_link_tag "nquery/application" %>
8
+ <%= javascript_include_tag "nquery/application", defer: true %>
9
+ </head>
10
+ <body class="nq-body nq-auth-body">
11
+ <%= yield %>
12
+ <div id="flash" class="nq-toast-stack" aria-live="polite" aria-atomic="false">
13
+ <%= render "nquery/shared/flash" %>
14
+ </div>
15
+ </body>
16
+ </html>
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>nquery embed</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= stylesheet_link_tag "nquery/application" %>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
8
+ </head>
9
+ <body class="nq-embed-body">
10
+ <%= yield %>
11
+ </body>
12
+ </html>
@@ -0,0 +1,8 @@
1
+ <div class="nq-page-header"><h1>Edit data source</h1></div>
2
+ <%= form_with model: @data_source, url: admin_data_source_path(@data_source), method: :patch, class: "nq-form nq-card" do |f| %>
3
+ <div class="nq-form-row"><%= f.label :name %><%= f.text_field :name, class: "nq-input" %></div>
4
+ <div class="nq-form-row"><%= f.label :adapter %><%= f.select :adapter, Nquery::DataSource::ADAPTERS, {}, class: "nq-input" %></div>
5
+ <div class="nq-form-row"><%= f.label :connection_config %><%= f.text_area :connection_config, class: "nq-textarea", rows: 4 %></div>
6
+ <div class="nq-form-row"><label><%= f.check_box :active %> Active</label></div>
7
+ <%= f.submit "Update", class: "nq-btn nq-btn-primary" %>
8
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <div class="nq-page-header"><h1>Data sources</h1><%= link_to "Add data source", new_admin_data_source_path, class: "nq-btn nq-btn-primary" %></div>
2
+
3
+ <div class="nq-card nq-card-table">
4
+ <table class="nq-table">
5
+ <thead><tr><th>Name</th><th>Adapter</th><th>Active</th><th></th></tr></thead>
6
+ <tbody>
7
+ <% @data_sources.each do |ds| %>
8
+ <tr>
9
+ <td><%= ds.name %></td>
10
+ <td><%= ds.adapter %></td>
11
+ <td><%= ds.active? ? "Yes" : "No" %></td>
12
+ <td>
13
+ <div class="nq-actions">
14
+ <%= link_to "Edit", edit_admin_data_source_path(ds), class: "nq-btn" %>
15
+ </div>
16
+ </td>
17
+ </tr>
18
+ <% end %>
19
+ </tbody>
20
+ </table>
21
+ </div>
@@ -0,0 +1,7 @@
1
+ <div class="nq-page-header"><h1>New data source</h1></div>
2
+ <%= form_with model: @data_source, url: admin_data_sources_path, class: "nq-form nq-card" do |f| %>
3
+ <div class="nq-form-row"><%= f.label :name %><%= f.text_field :name, class: "nq-input" %></div>
4
+ <div class="nq-form-row"><%= f.label :adapter %><%= f.select :adapter, Nquery::DataSource::ADAPTERS, {}, class: "nq-input" %></div>
5
+ <div class="nq-form-row"><%= f.label :connection_config, "Connection (JSON)" %><%= f.text_area :connection_config, class: "nq-textarea", rows: 4, placeholder: '{"host":"localhost","database":"mydb"}' %></div>
6
+ <%= f.submit "Create", class: "nq-btn nq-btn-primary" %>
7
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <div class="nq-page-header"><h1>Edit group</h1></div>
2
+ <%= form_with model: @group, url: admin_group_path(@group), method: :patch, class: "nq-form nq-card" do |f| %>
3
+ <div class="nq-form-row"><%= f.label :name %><%= f.text_field :name, class: "nq-input", disabled: @group.system? %></div>
4
+ <div class="nq-form-row"><%= f.label :description %><%= f.text_area :description, class: "nq-textarea" %></div>
5
+ <%= f.submit "Update", class: "nq-btn nq-btn-primary" unless @group.system? && @group.system_group != "custom" %>
6
+ <% end %>
@@ -0,0 +1,21 @@
1
+ <div class="nq-page-header"><h1>Groups</h1><%= link_to "New group", new_admin_group_path, class: "nq-btn nq-btn-primary" %></div>
2
+
3
+ <div class="nq-card nq-card-table">
4
+ <table class="nq-table">
5
+ <thead><tr><th>Name</th><th>Type</th><th>Members</th><th></th></tr></thead>
6
+ <tbody>
7
+ <% @groups.each do |group| %>
8
+ <tr>
9
+ <td><%= link_to group.name, admin_group_path(group) %></td>
10
+ <td><span class="nq-badge"><%= group.system_group %></span></td>
11
+ <td><%= group.users.count %></td>
12
+ <td>
13
+ <div class="nq-actions">
14
+ <%= link_to "Edit", edit_admin_group_path(group), class: "nq-btn" unless group.system? && group.system_group != "custom" %>
15
+ </div>
16
+ </td>
17
+ </tr>
18
+ <% end %>
19
+ </tbody>
20
+ </table>
21
+ </div>
@@ -0,0 +1,6 @@
1
+ <div class="nq-page-header"><h1>New group</h1></div>
2
+ <%= form_with model: @group, url: admin_groups_path, class: "nq-form nq-card" do |f| %>
3
+ <div class="nq-form-row"><%= f.label :name %><%= f.text_field :name, class: "nq-input" %></div>
4
+ <div class="nq-form-row"><%= f.label :description %><%= f.text_area :description, class: "nq-textarea" %></div>
5
+ <%= f.submit "Create", class: "nq-btn nq-btn-primary" %>
6
+ <% end %>
@@ -0,0 +1,24 @@
1
+ <div class="nq-page-header"><h1><%= @group.name %></h1></div>
2
+ <div class="nq-grid nq-grid-2">
3
+ <section class="nq-card">
4
+ <h2>Members</h2>
5
+ <ul class="nq-list">
6
+ <% @group.users.each do |user| %>
7
+ <li>
8
+ <%= user.name %> (<%= user.email %>)
9
+ <% unless @group.system_group == "all_users" %>
10
+ <%= button_to "Remove", remove_member_admin_group_path(@group, user_id: user.id), method: :delete, class: "nq-btn nq-btn-ghost" %>
11
+ <% end %>
12
+ </li>
13
+ <% end %>
14
+ </ul>
15
+ </section>
16
+ <% if @available_users.any? %>
17
+ <section class="nq-card">
18
+ <h2>Add member</h2>
19
+ <% @available_users.limit(10).each do |user| %>
20
+ <%= button_to "Add #{user.email}", add_member_admin_group_path(@group, user_id: user.id), method: :post, class: "nq-btn" %>
21
+ <% end %>
22
+ </section>
23
+ <% end %>
24
+ </div>
@@ -0,0 +1,74 @@
1
+ <div class="nq-page-header">
2
+ <h1>Query logs</h1>
3
+ <p class="nq-muted">Search query execution history</p>
4
+ </div>
5
+
6
+ <section class="nq-card">
7
+ <%= form_with url: admin_logs_path, method: :get, class: "nq-form" do %>
8
+ <div class="nq-grid nq-grid-3">
9
+ <div class="nq-form-row">
10
+ <label for="user">User</label>
11
+ <%= text_field_tag :user, params[:user], class: "nq-input", placeholder: "Name or email" %>
12
+ </div>
13
+ <div class="nq-form-row">
14
+ <label for="collection">Collection</label>
15
+ <%= select_tag :collection,
16
+ options_from_collection_for_select(@collections, :id, :name, params[:collection]),
17
+ include_blank: "All collections",
18
+ class: "nq-input" %>
19
+ </div>
20
+ <div class="nq-form-row">
21
+ <label for="dashboard">Dashboard</label>
22
+ <%= select_tag :dashboard,
23
+ options_from_collection_for_select(@dashboards, :id, :name, params[:dashboard]),
24
+ include_blank: "All dashboards",
25
+ class: "nq-input" %>
26
+ </div>
27
+ <div class="nq-form-row">
28
+ <label for="from">From</label>
29
+ <%= date_field_tag :from, params[:from], class: "nq-input" %>
30
+ </div>
31
+ <div class="nq-form-row">
32
+ <label for="to">To</label>
33
+ <%= date_field_tag :to, params[:to], class: "nq-input" %>
34
+ </div>
35
+ </div>
36
+ <div style="display: flex; gap: 0.5rem;">
37
+ <%= submit_tag "Search", class: "nq-btn nq-btn-primary" %>
38
+ <%= link_to "Clear", admin_logs_path, class: "nq-btn nq-btn-ghost" %>
39
+ </div>
40
+ <% end %>
41
+ </section>
42
+
43
+ <section class="nq-card nq-mt">
44
+ <table class="nq-table">
45
+ <thead>
46
+ <tr>
47
+ <th>User</th>
48
+ <th>Query</th>
49
+ <th>Collection</th>
50
+ <th>Dashboard</th>
51
+ <th>Status</th>
52
+ <th>When</th>
53
+ </tr>
54
+ </thead>
55
+ <tbody>
56
+ <% if @audits.any? %>
57
+ <% @audits.each do |audit| %>
58
+ <tr>
59
+ <td><%= audit.user&.name || "—" %></td>
60
+ <td><%= truncate(audit.statement.to_s, length: 60) %></td>
61
+ <td><%= audit.query&.collection&.name || "—" %></td>
62
+ <td><%= audit.query&.chart&.dashboard_cards&.first&.dashboard&.name || "—" %></td>
63
+ <td><span class="nq-badge"><%= audit.status %></span></td>
64
+ <td><%= time_ago_in_words(audit.created_at) %> ago</td>
65
+ </tr>
66
+ <% end %>
67
+ <% else %>
68
+ <tr>
69
+ <td colspan="6" class="nq-muted">No query logs found.</td>
70
+ </tr>
71
+ <% end %>
72
+ </tbody>
73
+ </table>
74
+ </section>
@@ -0,0 +1,25 @@
1
+ <div class="nq-card nq-mt">
2
+ <form method="get" class="nq-form-row">
3
+ <label>Collection</label>
4
+ <select name="collection_id" class="nq-input" onchange="this.form.submit()">
5
+ <% @collections.each do |collection| %>
6
+ <option value="<%= collection.id %>" <%= "selected" if collection == @selected_collection %>><%= collection.name %></option>
7
+ <% end %>
8
+ </select>
9
+ </form>
10
+
11
+ <% if @selected_collection %>
12
+ <table class="nq-table">
13
+ <thead><tr><th>Group</th><th>Access</th></tr></thead>
14
+ <tbody>
15
+ <% @groups.each do |group| %>
16
+ <% perm = Nquery::CollectionPermission.find_by(group: group, collection: @selected_collection) %>
17
+ <tr>
18
+ <td><%= group.name %></td>
19
+ <td><span class="nq-badge"><%= perm&.access_level || "no_access" %></span></td>
20
+ </tr>
21
+ <% end %>
22
+ </tbody>
23
+ </table>
24
+ <% end %>
25
+ </div>
@@ -0,0 +1,27 @@
1
+ <div class="nq-card nq-mt">
2
+ <form method="get" class="nq-form-row">
3
+ <label>Data source</label>
4
+ <select name="data_source_id" class="nq-input" onchange="this.form.submit()">
5
+ <% @data_sources.each do |ds| %>
6
+ <option value="<%= ds.id %>" <%= "selected" if ds == @selected_data_source %>><%= ds.name %></option>
7
+ <% end %>
8
+ </select>
9
+ </form>
10
+
11
+ <% if @selected_data_source %>
12
+ <table class="nq-table">
13
+ <thead><tr><th>Group</th><th>View data</th><th>Create queries</th></tr></thead>
14
+ <tbody>
15
+ <% @groups.each do |group| %>
16
+ <% view = Nquery::DataPermission.find_by(group: group, data_source: @selected_data_source, permission_type: "view_data") %>
17
+ <% create = Nquery::DataPermission.find_by(group: group, data_source: @selected_data_source, permission_type: "create_queries") %>
18
+ <tr>
19
+ <td><%= group.name %></td>
20
+ <td><span class="nq-badge"><%= view&.access_level || "blocked" %></span></td>
21
+ <td><span class="nq-badge"><%= create&.access_level || "no" %></span></td>
22
+ </tr>
23
+ <% end %>
24
+ </tbody>
25
+ </table>
26
+ <% end %>
27
+ </div>