nquery 0.1.0 → 0.1.2
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 +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +263 -24
- data/app/assets/builds/nquery/application.css +61 -4
- data/app/assets/builds/nquery/application.js +107 -12
- data/app/controllers/concerns/nquery/breadcrumbs.rb +5 -3
- data/app/controllers/concerns/nquery/chart_actions.rb +1 -0
- data/app/controllers/concerns/nquery/chart_results.rb +17 -3
- data/app/controllers/nquery/admin/data_sources_controller.rb +49 -2
- data/app/controllers/nquery/admin/groups_controller.rb +8 -2
- data/app/controllers/nquery/admin/users_controller.rb +6 -1
- data/app/controllers/nquery/application_controller.rb +45 -28
- data/app/controllers/nquery/dashboards_controller.rb +30 -0
- data/app/controllers/nquery/embed/charts_controller.rb +1 -0
- data/app/controllers/nquery/embed/dashboards_controller.rb +1 -0
- data/app/controllers/nquery/errors_controller.rb +11 -0
- data/app/controllers/nquery/home_controller.rb +1 -0
- data/app/controllers/nquery/onboarding/admins_controller.rb +42 -0
- data/app/controllers/nquery/onboarding/base_controller.rb +27 -0
- data/app/controllers/nquery/onboarding/companies_controller.rb +30 -0
- data/app/controllers/nquery/onboarding/confirmations_controller.rb +48 -0
- data/app/controllers/nquery/onboarding/congrats_controller.rb +15 -0
- data/app/controllers/nquery/queries_controller.rb +0 -1
- data/app/controllers/nquery/sessions_controller.rb +14 -17
- data/app/helpers/nquery/data_sources_helper.rb +13 -0
- data/app/helpers/nquery/groups_helper.rb +19 -0
- data/app/javascript/nquery/controllers/data_source_form_controller.js +25 -0
- data/app/mailers/nquery/application_mailer.rb +7 -0
- data/app/mailers/nquery/devise_mailer.rb +9 -0
- data/app/models/nquery/audit.rb +2 -2
- data/app/models/nquery/collection.rb +1 -1
- data/app/models/nquery/dashboard.rb +1 -1
- data/app/models/nquery/data_source.rb +139 -4
- data/app/models/nquery/organization.rb +38 -0
- data/app/models/nquery/query.rb +8 -0
- data/app/models/nquery/user.rb +45 -10
- data/app/views/nquery/admin/data_sources/_form.html.erb +121 -0
- data/app/views/nquery/admin/data_sources/edit.html.erb +6 -8
- data/app/views/nquery/admin/data_sources/new.html.erb +6 -7
- data/app/views/nquery/admin/groups/_form.html.erb +47 -0
- data/app/views/nquery/admin/groups/edit.html.erb +15 -6
- data/app/views/nquery/admin/groups/index.html.erb +25 -20
- data/app/views/nquery/admin/groups/new.html.erb +6 -6
- data/app/views/nquery/admin/groups/show.html.erb +98 -19
- data/app/views/nquery/admin/logs/index.html.erb +26 -30
- data/app/views/nquery/charts/_builder_form.html.erb +6 -4
- data/app/views/nquery/charts/edit.html.erb +2 -1
- data/app/views/nquery/dashboards/index.html.erb +6 -0
- data/app/views/nquery/dashboards/new.html.erb +34 -0
- data/app/views/nquery/devise_mailer/confirmation_instructions.html.erb +5 -0
- data/app/views/nquery/devise_mailer/confirmation_instructions.text.erb +5 -0
- data/app/views/nquery/errors/internal_server_error.html.erb +10 -0
- data/app/views/nquery/errors/not_found.html.erb +10 -0
- data/app/views/nquery/home/index.html.erb +7 -15
- data/app/views/nquery/onboarding/admins/new.html.erb +21 -0
- data/app/views/nquery/onboarding/companies/new.html.erb +25 -0
- data/app/views/nquery/onboarding/confirmations/show.html.erb +17 -0
- data/app/views/nquery/onboarding/congrats/show.html.erb +11 -0
- data/app/views/nquery/sessions/new.html.erb +8 -9
- data/app/views/nquery/shared/_auth_shell.html.erb +1 -1
- data/app/views/nquery/shared/_topbar.html.erb +0 -4
- data/config/routes.rb +26 -7
- data/db/migrate/20260721000001_create_nquery_tables.rb +1 -1
- data/db/migrate/20260722000001_add_key_to_nquery_data_sources.rb +8 -0
- data/db/migrate/20260722000002_create_nquery_organizations.rb +11 -0
- data/db/migrate/20260722000003_add_devise_to_nquery_users.rb +35 -0
- data/db/migrate/20260722000004_add_onboarding_completed_at_to_nquery_organizations.rb +7 -0
- data/lib/generators/nquery/install_generator.rb +9 -17
- data/lib/nquery/configuration.rb +7 -23
- data/lib/nquery/csv_importer.rb +3 -3
- data/lib/nquery/data_sources/adapter.rb +4 -0
- data/lib/nquery/data_sources/postgresql_adapter.rb +15 -1
- data/lib/nquery/data_sources/rails_adapter.rb +5 -0
- data/lib/nquery/data_sources/syncer.rb +50 -0
- data/lib/nquery/encryption.rb +50 -0
- data/lib/nquery/engine.rb +42 -3
- data/lib/nquery/onboarding/admin_provisioner.rb +40 -0
- data/lib/nquery/onboarding/password_confirmation.rb +46 -0
- data/lib/nquery/onboarding.rb +40 -0
- data/lib/nquery/permissions/resolver.rb +14 -4
- data/lib/nquery/query_runner.rb +5 -9
- data/lib/nquery/readonly_sql.rb +25 -0
- data/lib/nquery/seeder.rb +25 -34
- data/lib/nquery/setup.rb +75 -0
- data/lib/nquery/version.rb +1 -1
- data/lib/nquery.rb +9 -0
- data/lib/tasks/nquery.rake +6 -0
- data/nquery.gemspec +11 -5
- metadata +135 -26
- data/app/controllers/nquery/registrations_controller.rb +0 -31
- data/app/views/nquery/registrations/new.html.erb +0 -34
|
@@ -2,26 +2,161 @@
|
|
|
2
2
|
|
|
3
3
|
module Nquery
|
|
4
4
|
class DataSource < ApplicationRecord
|
|
5
|
-
|
|
5
|
+
ADAPTER_OPTIONS = [
|
|
6
|
+
["Application database", "rails"],
|
|
7
|
+
["PostgreSQL", "postgresql"],
|
|
8
|
+
["MySQL", "mysql"],
|
|
9
|
+
["SQLite", "sqlite"]
|
|
10
|
+
].freeze
|
|
11
|
+
ADAPTERS = ADAPTER_OPTIONS.map(&:last).freeze
|
|
12
|
+
REMOTE_ADAPTERS = %w[postgresql mysql].freeze
|
|
13
|
+
SSL_MODES = %w[disable require verify-ca verify-full].freeze
|
|
14
|
+
|
|
15
|
+
AR_ADAPTER_NAMES = {
|
|
16
|
+
"postgresql" => "postgresql",
|
|
17
|
+
"mysql" => "mysql2",
|
|
18
|
+
"sqlite" => "sqlite3"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
DEFAULT_PORTS = {
|
|
22
|
+
"postgresql" => 5432,
|
|
23
|
+
"mysql" => 3306
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
# serialize must be declared before encrypts for structured attributes.
|
|
27
|
+
serialize :connection_config, coder: JSON
|
|
28
|
+
encrypts :connection_config
|
|
6
29
|
|
|
7
30
|
has_many :data_permissions, class_name: "Nquery::DataPermission", dependent: :destroy
|
|
8
31
|
has_many :queries, class_name: "Nquery::Query", dependent: :nullify
|
|
9
32
|
|
|
33
|
+
attr_accessor :host, :port, :database, :username, :password, :database_path, :sslmode, :connection_fields_submitted
|
|
34
|
+
|
|
10
35
|
validates :name, presence: true
|
|
36
|
+
validates :key, uniqueness: true, allow_nil: true
|
|
11
37
|
validates :adapter, inclusion: { in: ADAPTERS }
|
|
38
|
+
validate :validate_connection_fields, if: :connection_fields_submitted?
|
|
39
|
+
|
|
40
|
+
before_validation :compose_connection_config
|
|
12
41
|
|
|
13
42
|
scope :active, -> { all }
|
|
14
43
|
|
|
44
|
+
def connection_fields_submitted?
|
|
45
|
+
ActiveModel::Type::Boolean.new.cast(connection_fields_submitted)
|
|
46
|
+
end
|
|
47
|
+
|
|
15
48
|
def connection_config_hash
|
|
16
|
-
|
|
49
|
+
value = connection_config
|
|
50
|
+
return {} if value.blank?
|
|
17
51
|
|
|
18
|
-
JSON.parse(
|
|
52
|
+
hash = value.is_a?(String) ? JSON.parse(value) : value
|
|
53
|
+
normalize_config_hash(hash)
|
|
19
54
|
rescue JSON::ParserError
|
|
20
55
|
{}
|
|
21
56
|
end
|
|
22
57
|
|
|
23
58
|
def connection_config_hash=(hash)
|
|
24
|
-
self.connection_config = hash.
|
|
59
|
+
self.connection_config = hash.presence
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def default_port
|
|
63
|
+
DEFAULT_PORTS.fetch(adapter, DEFAULT_PORTS.fetch("postgresql")).to_s
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def test_connection
|
|
67
|
+
compose_connection_config
|
|
68
|
+
validate_connection_fields if connection_fields_submitted?
|
|
69
|
+
return false if errors.any?
|
|
70
|
+
|
|
71
|
+
DataSources::Adapter.for(self).test_connection
|
|
72
|
+
true
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
errors.add(:base, e.message)
|
|
75
|
+
false
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def assign_connection_fields_from_config
|
|
79
|
+
hash = connection_config_hash
|
|
80
|
+
self.host = hash["host"]
|
|
81
|
+
self.port = hash["port"]&.to_s
|
|
82
|
+
self.database = hash["database"]
|
|
83
|
+
self.username = hash["username"]
|
|
84
|
+
self.database_path = hash["database_path"].presence || hash["database"]
|
|
85
|
+
self.sslmode = hash["sslmode"]
|
|
86
|
+
self.password = nil
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def compose_connection_config
|
|
92
|
+
if adapter == "rails"
|
|
93
|
+
self.connection_config = {}
|
|
94
|
+
return
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
assign_connection_config_from_fields if connection_fields_submitted?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate_connection_fields
|
|
101
|
+
case adapter
|
|
102
|
+
when *REMOTE_ADAPTERS
|
|
103
|
+
errors.add(:host, "can't be blank") if host.blank?
|
|
104
|
+
errors.add(:database, "can't be blank") if database.blank?
|
|
105
|
+
errors.add(:username, "can't be blank") if username.blank?
|
|
106
|
+
errors.add(:password, "can't be blank") if password.blank? && (new_record? || existing_password.blank?)
|
|
107
|
+
if sslmode.present? && !SSL_MODES.include?(sslmode)
|
|
108
|
+
errors.add(:sslmode, "is not included in the list")
|
|
109
|
+
end
|
|
110
|
+
when "sqlite"
|
|
111
|
+
errors.add(:database_path, "can't be blank") if database_path.blank?
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def assign_connection_config_from_fields
|
|
116
|
+
previous = persisted? ? load_persisted_connection_config : {}
|
|
117
|
+
|
|
118
|
+
self.connection_config = case adapter
|
|
119
|
+
when "sqlite"
|
|
120
|
+
{
|
|
121
|
+
"adapter" => AR_ADAPTER_NAMES.fetch(adapter),
|
|
122
|
+
"database" => database_path.to_s
|
|
123
|
+
}
|
|
124
|
+
when *REMOTE_ADAPTERS
|
|
125
|
+
config = {
|
|
126
|
+
"adapter" => AR_ADAPTER_NAMES.fetch(adapter),
|
|
127
|
+
"host" => host.to_s,
|
|
128
|
+
"port" => resolved_port,
|
|
129
|
+
"database" => database.to_s,
|
|
130
|
+
"username" => username.to_s
|
|
131
|
+
}
|
|
132
|
+
config["password"] = resolved_password(previous)
|
|
133
|
+
config["sslmode"] = sslmode if sslmode.present?
|
|
134
|
+
config
|
|
135
|
+
else
|
|
136
|
+
{}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def resolved_port
|
|
141
|
+
port.presence&.to_i || default_port.to_i
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def resolved_password(previous)
|
|
145
|
+
return password if password.present?
|
|
146
|
+
|
|
147
|
+
previous["password"]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def existing_password
|
|
151
|
+
connection_config_hash["password"]
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def load_persisted_connection_config
|
|
155
|
+
normalize_config_hash(self.class.find(id).connection_config.presence || {})
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def normalize_config_hash(hash)
|
|
159
|
+
hash.stringify_keys
|
|
25
160
|
end
|
|
26
161
|
end
|
|
27
162
|
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nquery
|
|
4
|
+
class Organization < ApplicationRecord
|
|
5
|
+
IMAGE_CONTENT_TYPES = %w[image/png image/jpeg image/jpg image/webp].freeze
|
|
6
|
+
MAX_IMAGE_BYTE_SIZE = 5.megabytes
|
|
7
|
+
|
|
8
|
+
has_one_attached :logo
|
|
9
|
+
has_one_attached :cover_image
|
|
10
|
+
|
|
11
|
+
validates :name, presence: true
|
|
12
|
+
validate :acceptable_logo
|
|
13
|
+
validate :acceptable_cover_image
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def acceptable_logo
|
|
18
|
+
validate_image_attachment(:logo)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def acceptable_cover_image
|
|
22
|
+
validate_image_attachment(:cover_image)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def validate_image_attachment(name)
|
|
26
|
+
attachment = public_send(name)
|
|
27
|
+
return unless attachment.attached?
|
|
28
|
+
|
|
29
|
+
unless attachment.blob.content_type.in?(IMAGE_CONTENT_TYPES)
|
|
30
|
+
errors.add(name, "must be a PNG, JPEG, or WebP")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return unless attachment.blob.byte_size > MAX_IMAGE_BYTE_SIZE
|
|
34
|
+
|
|
35
|
+
errors.add(name, "is too large (maximum is 5 MB)")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/app/models/nquery/query.rb
CHANGED
|
@@ -9,11 +9,19 @@ module Nquery
|
|
|
9
9
|
has_many :audits, class_name: "Nquery::Audit", dependent: :nullify
|
|
10
10
|
|
|
11
11
|
validates :name, presence: true, on: :update
|
|
12
|
+
validate :statement_must_be_readonly
|
|
12
13
|
|
|
13
14
|
before_validation :set_default_name, on: :create
|
|
14
15
|
|
|
15
16
|
def set_default_name
|
|
16
17
|
self.name ||= "Untitled query"
|
|
17
18
|
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def statement_must_be_readonly
|
|
23
|
+
message = ReadonlySql.error_message(statement, allow_blank: true)
|
|
24
|
+
errors.add(:statement, message) if message
|
|
25
|
+
end
|
|
18
26
|
end
|
|
19
27
|
end
|
data/app/models/nquery/user.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Nquery
|
|
4
4
|
class User < ApplicationRecord
|
|
5
|
-
|
|
5
|
+
devise :database_authenticatable, :confirmable, :validatable,
|
|
6
|
+
confirm_within: 1.hour
|
|
6
7
|
|
|
7
8
|
has_many :group_memberships, class_name: "Nquery::GroupMembership", dependent: :destroy
|
|
8
9
|
has_many :groups, through: :group_memberships, class_name: "Nquery::Group"
|
|
@@ -16,18 +17,32 @@ module Nquery
|
|
|
16
17
|
class_name: "Nquery::Collection", foreign_key: :owner_id, inverse_of: :owner, dependent: :destroy
|
|
17
18
|
|
|
18
19
|
validates :email, presence: true, uniqueness: true
|
|
19
|
-
validates :password, length: { minimum: 8 }, if: -> { password.present? }
|
|
20
20
|
|
|
21
21
|
scope :active, -> { where(deactivated_at: nil) }
|
|
22
22
|
|
|
23
|
+
# Set by #deliver_devise_notification so controllers can flash delivery failures.
|
|
24
|
+
attr_accessor :last_devise_notification_delivered
|
|
25
|
+
|
|
23
26
|
def name
|
|
24
27
|
[first_name, last_name].compact_blank.join(" ").presence || email
|
|
25
28
|
end
|
|
26
29
|
|
|
30
|
+
def name_with_email
|
|
31
|
+
"#{name} (#{email})"
|
|
32
|
+
end
|
|
33
|
+
|
|
27
34
|
def active?
|
|
28
35
|
deactivated_at.nil?
|
|
29
36
|
end
|
|
30
37
|
|
|
38
|
+
def active_for_authentication?
|
|
39
|
+
super && active?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inactive_message
|
|
43
|
+
active? ? super : :inactive
|
|
44
|
+
end
|
|
45
|
+
|
|
31
46
|
def deactivate!
|
|
32
47
|
update!(deactivated_at: Time.current)
|
|
33
48
|
end
|
|
@@ -36,14 +51,6 @@ module Nquery
|
|
|
36
51
|
Permissions::Resolver.new(self).admin?
|
|
37
52
|
end
|
|
38
53
|
|
|
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
54
|
def ensure_all_users_membership!
|
|
48
55
|
all_users = Group.find_by!(system_group: "all_users")
|
|
49
56
|
group_memberships.find_or_create_by!(group: all_users)
|
|
@@ -54,5 +61,33 @@ module Nquery
|
|
|
54
61
|
|
|
55
62
|
create_personal_collection!(name: "#{name}'s Personal Collection", kind: "personal")
|
|
56
63
|
end
|
|
64
|
+
|
|
65
|
+
def password_required?
|
|
66
|
+
return false if new_record? && password.blank?
|
|
67
|
+
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def confirmation_expired?
|
|
72
|
+
confirmation_period_expired?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Returns false when delivery fails so callers can flash a warning instead of 500ing.
|
|
76
|
+
def deliver_devise_notification(notification, *args)
|
|
77
|
+
Nquery::DeviseMailer.send(notification, self, *args).deliver_now
|
|
78
|
+
self.last_devise_notification_delivered = true
|
|
79
|
+
rescue StandardError => error
|
|
80
|
+
self.last_devise_notification_delivered = false
|
|
81
|
+
Rails.logger.error(
|
|
82
|
+
"[nquery] Failed to deliver Devise #{notification} to #{email}: #{error.class}: #{error.message}"
|
|
83
|
+
)
|
|
84
|
+
false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
protected
|
|
88
|
+
|
|
89
|
+
def send_devise_notification(notification, *args)
|
|
90
|
+
deliver_devise_notification(notification, *args)
|
|
91
|
+
end
|
|
57
92
|
end
|
|
58
93
|
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
<% cancel_path = local_assigns.fetch(:cancel_path) %>
|
|
2
|
+
<% submit_label = local_assigns.fetch(:submit_label) %>
|
|
3
|
+
|
|
4
|
+
<%= form_with model: data_source, url: data_source_form_url(data_source), method: data_source_form_method(data_source), class: "nq-form nq-card",
|
|
5
|
+
data: {
|
|
6
|
+
controller: "data-source-form",
|
|
7
|
+
data_source_form_adapter_value: data_source.adapter,
|
|
8
|
+
data_source_form_test_url_value: test_connection_admin_data_sources_path,
|
|
9
|
+
data_source_form_id_value: data_source.persisted? ? data_source.id : nil
|
|
10
|
+
} do |f| %>
|
|
11
|
+
<% if data_source.errors.any? %>
|
|
12
|
+
<div class="nq-form-errors" role="alert">
|
|
13
|
+
<p><strong>Please fix the following:</strong></p>
|
|
14
|
+
<ul>
|
|
15
|
+
<% data_source.errors.full_messages.each do |message| %>
|
|
16
|
+
<li><%= message %></li>
|
|
17
|
+
<% end %>
|
|
18
|
+
</ul>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<section class="nq-form-section">
|
|
23
|
+
<h2 class="nq-form-section-title">Details</h2>
|
|
24
|
+
|
|
25
|
+
<div class="nq-grid nq-grid-2 nq-form-grid">
|
|
26
|
+
<div class="nq-form-row">
|
|
27
|
+
<%= f.label :name %>
|
|
28
|
+
<%= f.text_field :name, class: "nq-input", placeholder: "e.g. Warehouse, Analytics", autofocus: !data_source.persisted? %>
|
|
29
|
+
<p class="nq-field-hint">A short label shown when picking a data source.</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="nq-form-row">
|
|
33
|
+
<%= f.label :adapter %>
|
|
34
|
+
<%= f.select :adapter,
|
|
35
|
+
options_for_select(Nquery::DataSource::ADAPTER_OPTIONS, data_source.adapter),
|
|
36
|
+
{},
|
|
37
|
+
class: "nq-input",
|
|
38
|
+
data: { action: "change->data-source-form#toggle", data_source_form_target: "adapter" } %>
|
|
39
|
+
<p class="nq-field-hint">Choose how nquery connects to the database.</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<p data-data-source-form-target="railsHint" class="nq-field-hint" hidden>
|
|
44
|
+
Uses the host Rails application database connection. No additional credentials are required.
|
|
45
|
+
</p>
|
|
46
|
+
</section>
|
|
47
|
+
|
|
48
|
+
<section class="nq-form-section" data-data-source-form-target="remoteFields" hidden>
|
|
49
|
+
<h2 class="nq-form-section-title">Connection</h2>
|
|
50
|
+
|
|
51
|
+
<div class="nq-grid nq-grid-2 nq-form-grid">
|
|
52
|
+
<div class="nq-form-row">
|
|
53
|
+
<%= f.label :host %>
|
|
54
|
+
<%= f.text_field :host, class: "nq-input", placeholder: "localhost", autocomplete: "off" %>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div class="nq-form-row">
|
|
58
|
+
<%= f.label :port %>
|
|
59
|
+
<%= f.number_field :port, class: "nq-input", placeholder: data_source.default_port, min: 1, max: 65_535,
|
|
60
|
+
data: { data_source_form_target: "port" } %>
|
|
61
|
+
<p class="nq-field-hint">Leave blank to use the default port.</p>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="nq-grid nq-grid-2 nq-form-grid">
|
|
66
|
+
<div class="nq-form-row">
|
|
67
|
+
<%= f.label :database %>
|
|
68
|
+
<%= f.text_field :database, class: "nq-input", autocomplete: "off" %>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div class="nq-form-row">
|
|
72
|
+
<%= f.label :sslmode, "SSL mode (optional)" %>
|
|
73
|
+
<%= f.select :sslmode,
|
|
74
|
+
options_for_select([["Default", ""]] + Nquery::DataSource::SSL_MODES.map { |mode| [mode, mode] }, data_source.sslmode),
|
|
75
|
+
{},
|
|
76
|
+
class: "nq-input" %>
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<div class="nq-grid nq-grid-2 nq-form-grid">
|
|
81
|
+
<div class="nq-form-row">
|
|
82
|
+
<%= f.label :username %>
|
|
83
|
+
<%= f.text_field :username, class: "nq-input", autocomplete: "off" %>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<div class="nq-form-row">
|
|
87
|
+
<%= f.label :password %>
|
|
88
|
+
<%= f.password_field :password, class: "nq-input", autocomplete: "new-password",
|
|
89
|
+
placeholder: data_source.persisted? ? "Leave blank to keep current password" : nil %>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
</section>
|
|
93
|
+
|
|
94
|
+
<section class="nq-form-section" data-data-source-form-target="sqliteFields" hidden>
|
|
95
|
+
<h2 class="nq-form-section-title">SQLite database</h2>
|
|
96
|
+
|
|
97
|
+
<div class="nq-form-row">
|
|
98
|
+
<%= f.label :database_path, "Database path" %>
|
|
99
|
+
<%= f.text_field :database_path, class: "nq-input", placeholder: "/path/to/database.sqlite3" %>
|
|
100
|
+
<p class="nq-field-hint">Absolute path to the SQLite file on the server.</p>
|
|
101
|
+
</div>
|
|
102
|
+
</section>
|
|
103
|
+
|
|
104
|
+
<% if data_source.persisted? %>
|
|
105
|
+
<section class="nq-form-section">
|
|
106
|
+
<div class="nq-form-row">
|
|
107
|
+
<label><%= f.check_box :active %> Active</label>
|
|
108
|
+
<p class="nq-field-hint">Inactive data sources are hidden from query and chart builders.</p>
|
|
109
|
+
</div>
|
|
110
|
+
</section>
|
|
111
|
+
<% end %>
|
|
112
|
+
|
|
113
|
+
<div class="nq-form-actions">
|
|
114
|
+
<button type="button" class="nq-btn nq-form-actions-start" data-data-source-form-target="testButton" data-manages-loading="true">
|
|
115
|
+
Test Connection
|
|
116
|
+
</button>
|
|
117
|
+
<%= link_to "Cancel", cancel_path, class: "nq-btn" %>
|
|
118
|
+
<%= f.submit submit_label, class: "nq-btn nq-btn-primary" %>
|
|
119
|
+
</div>
|
|
120
|
+
<p class="nq-connection-status" data-data-source-form-target="testStatus" role="status" hidden></p>
|
|
121
|
+
<% end %>
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
<div class="nq-page-header"
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<%= f.submit "Update", class: "nq-btn nq-btn-primary" %>
|
|
8
|
-
<% end %>
|
|
1
|
+
<div class="nq-page-header">
|
|
2
|
+
<h1>Edit data source</h1>
|
|
3
|
+
<p class="nq-muted nq-resource-meta">Update connection settings for <%= @data_source.name %>.</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= render "form", data_source: @data_source, cancel_path: admin_data_sources_path, submit_label: "Save changes" %>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
<div class="nq-page-header"
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
<% end %>
|
|
1
|
+
<div class="nq-page-header">
|
|
2
|
+
<h1>New data source</h1>
|
|
3
|
+
<p class="nq-muted nq-resource-meta">Connect a database so your team can query and chart data.</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= render "form", data_source: @data_source, cancel_path: admin_data_sources_path, submit_label: "Create data source" %>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<% cancel_path = local_assigns.fetch(:cancel_path) %>
|
|
2
|
+
<% submit_label = local_assigns.fetch(:submit_label) %>
|
|
3
|
+
<% form_url = group.persisted? ? admin_group_path(group) : admin_groups_path %>
|
|
4
|
+
<% form_method = group.persisted? ? :patch : :post %>
|
|
5
|
+
|
|
6
|
+
<%= form_with model: group, url: form_url, method: form_method, class: "nq-form nq-form-card" do |f| %>
|
|
7
|
+
<% if group.errors.any? %>
|
|
8
|
+
<div class="nq-form-errors" role="alert">
|
|
9
|
+
<p><strong>Please fix the following:</strong></p>
|
|
10
|
+
<ul>
|
|
11
|
+
<% group.errors.full_messages.each do |message| %>
|
|
12
|
+
<li><%= message %></li>
|
|
13
|
+
<% end %>
|
|
14
|
+
</ul>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<% if group.persisted? && group.system? %>
|
|
19
|
+
<%= nq_flash_card "This is a system group. The name is fixed, but you can update the description.", type: :info, toast: false %>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<section class="nq-form-section">
|
|
23
|
+
<h2 class="nq-form-section-title">Group details</h2>
|
|
24
|
+
|
|
25
|
+
<div class="nq-form-row">
|
|
26
|
+
<%= f.label :name %>
|
|
27
|
+
<% if group.system? %>
|
|
28
|
+
<%= f.text_field :name, class: "nq-input", disabled: true %>
|
|
29
|
+
<p class="nq-field-hint">System group names cannot be changed.</p>
|
|
30
|
+
<% else %>
|
|
31
|
+
<%= f.text_field :name, class: "nq-input", placeholder: "e.g. Finance, Engineering", autofocus: !group.persisted? %>
|
|
32
|
+
<p class="nq-field-hint">Choose a name members will recognize.</p>
|
|
33
|
+
<% end %>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="nq-form-row">
|
|
37
|
+
<%= f.label :description, "Description (optional)" %>
|
|
38
|
+
<%= f.text_area :description, class: "nq-input nq-textarea-prose", rows: 3, placeholder: "What this group is for and who should belong to it." %>
|
|
39
|
+
<p class="nq-field-hint">Shown on the group page and helps others understand its purpose.</p>
|
|
40
|
+
</div>
|
|
41
|
+
</section>
|
|
42
|
+
|
|
43
|
+
<div class="nq-form-actions">
|
|
44
|
+
<%= link_to "Cancel", cancel_path, class: "nq-btn" %>
|
|
45
|
+
<%= f.submit submit_label, class: "nq-btn nq-btn-primary" %>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
<div class="nq-page-header"
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<div class="nq-page-header">
|
|
2
|
+
<h1>Edit group</h1>
|
|
3
|
+
<p class="nq-muted nq-resource-meta">
|
|
4
|
+
<%= @group.name %>
|
|
5
|
+
· <%= group_type_label(@group) %>
|
|
6
|
+
· <%= pluralize(@group.users.size, "member") %>
|
|
7
|
+
</p>
|
|
8
|
+
<div class="nq-actions">
|
|
9
|
+
<%= link_to admin_group_path(@group), class: "nq-btn" do %>
|
|
10
|
+
<span>View members</span>
|
|
11
|
+
<% end %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<%= render "form", group: @group, cancel_path: admin_group_path(@group), submit_label: "Save changes" %>
|
|
@@ -1,21 +1,26 @@
|
|
|
1
|
-
<div class="nq-page-header"
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
<
|
|
7
|
-
|
|
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>
|
|
1
|
+
<div class="nq-page-header">
|
|
2
|
+
<h1>Groups</h1>
|
|
3
|
+
<p class="nq-muted nq-resource-meta">Organize users into groups to manage access and permissions.</p>
|
|
4
|
+
<%= link_to new_admin_group_path, class: "nq-btn nq-btn-primary" do %>
|
|
5
|
+
<%= nq_icon(:plus) %>
|
|
6
|
+
<span>New group</span>
|
|
7
|
+
<% end %>
|
|
21
8
|
</div>
|
|
9
|
+
|
|
10
|
+
<%= nq_card_table columns: ["Name", "Description", "Type", "Members", ""] do %>
|
|
11
|
+
<% @groups.each do |group| %>
|
|
12
|
+
<tr>
|
|
13
|
+
<td><%= link_to group.name, admin_group_path(group) %></td>
|
|
14
|
+
<td class="nq-muted"><%= group.description.presence || "—" %></td>
|
|
15
|
+
<td>
|
|
16
|
+
<span class="nq-badge <%= group_type_badge_class(group) %>"><%= group_type_label(group) %></span>
|
|
17
|
+
</td>
|
|
18
|
+
<td><%= group.users.size %></td>
|
|
19
|
+
<td>
|
|
20
|
+
<div class="nq-actions">
|
|
21
|
+
<%= link_to "Edit", edit_admin_group_path(group), class: "nq-btn" %>
|
|
22
|
+
</div>
|
|
23
|
+
</td>
|
|
24
|
+
</tr>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% end %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<div class="nq-page-header"
|
|
2
|
-
|
|
3
|
-
<
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
<div class="nq-page-header">
|
|
2
|
+
<h1>New group</h1>
|
|
3
|
+
<p class="nq-muted nq-resource-meta">Create a group to organize users and manage permissions together.</p>
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<%= render "form", group: @group, cancel_path: admin_groups_path, submit_label: "Create group" %>
|