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,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,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,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>
|