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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0460bb4897b3a7fbd9516dc0c8e39a69eb397fb4ab221bc2c27f5879a40fc04e
|
|
4
|
+
data.tar.gz: ffd6e74059060bb039706b611a1ee6b193b7d71a6a18a02ea01f88d58c8e3a79
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b2631cd177c4840a24d2c81f07669a02b168bd0a15f9ac90f80e0cb7f027348624521807f64c50783e8d8e66cb74554f01dfc8ac06941d70cfe9b8b768a0122d
|
|
7
|
+
data.tar.gz: 182717511432fa142146e3fcc097667ae8fbda4f923447aa6ebfabd3e39a23ce0ca23b61c8b4a3f006ad6f00d1a37d50335308222697443d4e3e50767d9a32a5
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.2] - 2026-09-07
|
|
4
|
+
|
|
5
|
+
### Changed
|
|
6
|
+
- Publish homepage URL https://github.com/caiohenrique12/nquery (RubyGems does not allow repushing 0.1.1)
|
|
7
|
+
|
|
8
|
+
## [0.1.1] - 2026-09-07
|
|
9
|
+
|
|
10
|
+
Host apps should `bundle update nquery` and run `rails db:migrate`. Re-run `rails generate nquery:install` if you installed 0.1.0 and need the updated initializer.
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- PostgreSQL data sources with a Test Connection action on the admin form
|
|
14
|
+
- Dashboard creation from the home page, with dashboards belonging to a collection
|
|
15
|
+
- Post-install setup (`nquery:setup`) and first-admin onboarding via Devise
|
|
16
|
+
- Encrypted data source credentials and improved admin data source forms
|
|
17
|
+
- GitHub Actions CI with a shared `bin/ci` suite
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Hide the home New dashboard CTA when the user cannot curate
|
|
21
|
+
- Tighten Devise (`>= 5.0.4`) and Puma (`>= 7.2.1`) minimums for known CVEs
|
|
22
|
+
- Bump sqlite3 to `>= 2.9.6` (GHSA-mwm8-39rw-8826)
|
|
23
|
+
- Point gem homepage and source URLs to https://github.com/caiohenrique12/nquery
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- PostgreSQL adapter connections
|
|
27
|
+
- Read-only SQL enforcement on query create/save
|
|
28
|
+
- JSON error when a test-connection data source id is missing
|
|
29
|
+
|
|
30
|
+
## [0.1.0] - 2026-07-22
|
|
31
|
+
|
|
32
|
+
Initial public release.
|
data/README.md
CHANGED
|
@@ -1,52 +1,291 @@
|
|
|
1
1
|
# nquery
|
|
2
2
|
|
|
3
|
-
SQL charts, dashboards, and data visualization for Rails — mountable engine gem.
|
|
3
|
+
SQL charts, dashboards, and data visualization for Rails — a mountable engine gem.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
nquery lets a host Rails app expose a self-contained analytics UI. It:
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
* Ships as a Rails engine you mount at a path (for example `/nquery`)
|
|
8
|
+
* Provides query builders, charts, dashboards, collections, groups, and permissions
|
|
9
|
+
* Provisions groups and data sources via `rails nquery:setup`
|
|
10
|
+
* Creates the first administrator through a one-time onboarding wizard (no public signup)
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## Table of contents
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
- [Requirements](#requirements)
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Usage](#usage)
|
|
17
|
+
- [What `nquery:setup` does](#what-nquerysetup-does)
|
|
18
|
+
- [After onboarding](#after-onboarding)
|
|
19
|
+
- [Configuration](#configuration)
|
|
20
|
+
- [Authentication](#authentication)
|
|
21
|
+
- [First admin onboarding](#first-admin-onboarding)
|
|
22
|
+
- [Mail and SMTP](#mail-and-smtp)
|
|
23
|
+
- [Data sources](#data-sources)
|
|
24
|
+
- [Active Storage](#active-storage)
|
|
25
|
+
- [Development](#development)
|
|
26
|
+
- [Contributing](#contributing)
|
|
27
|
+
- [License](#license)
|
|
17
28
|
|
|
18
|
-
|
|
29
|
+
## Requirements
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
* Ruby `>= 3.2`
|
|
32
|
+
* Rails `>= 7.1`, `< 9`
|
|
33
|
+
* Active Storage (for organization logo / cover uploads)
|
|
21
34
|
|
|
22
|
-
##
|
|
35
|
+
## Installation
|
|
23
36
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
37
|
+
Install the gem and add it to your application's Gemfile by executing:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
bundle add nquery
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
If Bundler is not being used to manage dependencies, install the gem by executing:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
gem install nquery
|
|
47
|
+
```
|
|
30
48
|
|
|
31
|
-
|
|
49
|
+
Or add it manually:
|
|
32
50
|
|
|
33
51
|
```ruby
|
|
34
52
|
# Gemfile
|
|
35
53
|
gem "nquery"
|
|
54
|
+
```
|
|
36
55
|
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
```bash
|
|
57
|
+
bundle install
|
|
39
58
|
```
|
|
40
59
|
|
|
60
|
+
Then run the install generator and complete setup:
|
|
61
|
+
|
|
41
62
|
```bash
|
|
42
63
|
rails generate nquery:install
|
|
64
|
+
rails active_storage:install # required for organization logo/cover
|
|
65
|
+
rails db:migrate # engine migrations load from the gem
|
|
66
|
+
rails nquery:setup
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Mount the engine in `config/routes.rb`:
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
mount Nquery::Engine, at: "/nquery"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Edit `config/initializers/nquery.rb` to set `mailer_sender` and `smtp` (required for confirmation email). Visit `/nquery` and complete the [first admin onboarding](#first-admin-onboarding) wizard.
|
|
76
|
+
|
|
77
|
+
| Task | When to use | Creates users? | Creates demo charts? |
|
|
78
|
+
|------|-------------|----------------|----------------------|
|
|
79
|
+
| `rails nquery:setup` | Host / production install | No | No |
|
|
80
|
+
| `rails nquery:seed` | Local demo only (`make up`) | Yes | Yes |
|
|
81
|
+
|
|
82
|
+
Prefer `nquery:setup` in host apps. `nquery:seed` calls setup first, then adds sample content for the Docker demo.
|
|
83
|
+
|
|
84
|
+
## Usage
|
|
85
|
+
|
|
86
|
+
### What `nquery:setup` does
|
|
87
|
+
|
|
88
|
+
`Nquery::Setup.run!` (invoked by `rails nquery:setup`) is idempotent and safe to re-run. It:
|
|
89
|
+
|
|
90
|
+
1. Ensures system groups `administrators` and `all_users`
|
|
91
|
+
2. Syncs `config.data_sources` into `Nquery::DataSource` rows (via `Nquery::DataSources::Syncer`)
|
|
92
|
+
3. Ensures the root collection (`Our analytics`)
|
|
93
|
+
4. Seeds default collection, data, and application permissions for those groups
|
|
94
|
+
|
|
95
|
+
It intentionally does **not** create an organization, users, or demo charts. Those belong to onboarding (first admin) or `nquery:seed` (demo only).
|
|
96
|
+
|
|
97
|
+
### After onboarding
|
|
98
|
+
|
|
99
|
+
1. Sign in at `/nquery/login` (or your mount path + `/login`)
|
|
100
|
+
2. Browse **Collections**, build queries/charts, assemble dashboards
|
|
101
|
+
3. Manage **Users**, **Groups**, **Permissions**, and **Data sources** as an admin
|
|
102
|
+
|
|
103
|
+
Default permissions from setup give administrators full application features and query access on the default data source; `all_users` gets view-oriented defaults on the root collection.
|
|
104
|
+
|
|
105
|
+
## Configuration
|
|
106
|
+
|
|
107
|
+
`rails generate nquery:install` copies `config/initializers/nquery.rb`. Example:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
Nquery.configure do |config|
|
|
111
|
+
config.mailer_sender = "noreply@example.com"
|
|
112
|
+
config.smtp = {
|
|
113
|
+
address: "smtp.example.com",
|
|
114
|
+
port: 587,
|
|
115
|
+
domain: "example.com",
|
|
116
|
+
user_name: "smtp-user",
|
|
117
|
+
password: "smtp-password",
|
|
118
|
+
authentication: :plain,
|
|
119
|
+
enable_starttls_auto: true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
config.data_sources = {
|
|
123
|
+
main: { adapter: :rails, name: "Application database" }
|
|
124
|
+
}
|
|
125
|
+
config.default_data_source = :main
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
| Option | Default | Description |
|
|
130
|
+
|--------|---------|-------------|
|
|
131
|
+
| `mailer_sender` | `nil` | From address for Devise / app mail |
|
|
132
|
+
| `smtp` | `{}` | Passed to `ActionMailer::Base.smtp_settings` when present |
|
|
133
|
+
| `data_sources` | `{ main: { adapter: :rails, name: "Application database" } }` | Identity-only map of configured sources |
|
|
134
|
+
| `default_data_source` | `:main` | Key of the default source used by setup permissions |
|
|
135
|
+
| `query_timeout` | `15` | Query timeout (seconds) |
|
|
136
|
+
| `query_row_limit` | `10_000` | Max rows returned by a query |
|
|
137
|
+
| `embed_secret` | `nil` | Override for embed token signing (falls back to `secret_key_base`) |
|
|
138
|
+
|
|
139
|
+
**Not supported in config:** storing database credentials for data sources, SSO/hybrid auth hooks, or public self-registration.
|
|
140
|
+
|
|
141
|
+
## Authentication
|
|
142
|
+
|
|
143
|
+
Authentication is handled by [Devise](https://github.com/heartcombo/devise) end-to-end:
|
|
144
|
+
|
|
145
|
+
* **Sessions** — `Devise::SessionsController` (sign in / sign out via Warden)
|
|
146
|
+
* **Passwords** — Devise `:database_authenticatable` + `:validatable` on `Nquery::User`
|
|
147
|
+
* **Confirmation** — Devise `:confirmable` (first admin sets a password from the confirmation link; tokens expire after 1 hour)
|
|
148
|
+
|
|
149
|
+
There is **no** public signup route (`:registerable` is not enabled). Password reset (`:recoverable`) and remember-me (`:rememberable`) are not enabled. The first admin is created through [onboarding](#first-admin-onboarding); additional users are invited by an administrator under **Admin → Users** (confirmation email; invitee sets their password via the confirmation link).
|
|
150
|
+
|
|
151
|
+
Login and logout use Devise routes (`/login`, `/logout`) backed by Warden. Authentication is Devise-only; custom session cookie auth and SSO/hybrid hooks are not supported.
|
|
152
|
+
|
|
153
|
+
## First admin onboarding
|
|
154
|
+
|
|
155
|
+
After install + `nquery:setup`, the first person to open the engine creates the organization and the first administrator. There is no seed admin in a production-style install.
|
|
156
|
+
|
|
157
|
+
### When the wizard runs
|
|
158
|
+
|
|
159
|
+
Onboarding is required until:
|
|
160
|
+
|
|
161
|
+
* An `Nquery::Organization` exists
|
|
162
|
+
* Onboarding is complete (a confirmed admin exists in `administrators`, or onboarding has been latched via `onboarding_completed_at`)
|
|
163
|
+
|
|
164
|
+
Until then, visiting the engine redirects into the wizard instead of the normal login page. `nquery:setup` alone never completes onboarding — it only prepares groups and data sources.
|
|
165
|
+
|
|
166
|
+
### Wizard steps
|
|
167
|
+
|
|
168
|
+
Paths assume `mount Nquery::Engine, at: "/nquery"`. Drop the `/nquery` prefix when mounted at `/`.
|
|
169
|
+
|
|
170
|
+
| Step | Path | What the user does | Result |
|
|
171
|
+
|------|------|--------------------|--------|
|
|
172
|
+
| 1. Company | `GET /nquery/onboarding/company/new` | Name, optional website, logo, cover | Creates `Organization`; advances to admin step |
|
|
173
|
+
| 2. Admin | `GET /nquery/onboarding/admin/new` | First name, last name, email | Provisions admin into `administrators` + `all_users`; sends Devise confirmation email |
|
|
174
|
+
| 3. Congrats | `GET /nquery/onboarding/congrats` | Read instructions | Explains that a confirmation email was sent |
|
|
175
|
+
| 4. Confirm | `GET /nquery/onboarding/confirm?confirmation_token=…` | Choose password | Confirms the user, signs them in, locks the wizard |
|
|
176
|
+
|
|
177
|
+
### Flow
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
Visit /nquery
|
|
181
|
+
→ Company form
|
|
182
|
+
→ Admin form (email + name)
|
|
183
|
+
→ Congrats (“check your email”)
|
|
184
|
+
→ Open confirmation link
|
|
185
|
+
→ Set password
|
|
186
|
+
→ Signed in as admin; wizard locked
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Mail must work for step 2 → 4. If SMTP is misconfigured, the admin user is still created and the wizard continues to congrats with a warning flash — fix mail settings and resend or use the confirmation token before continuing. The Docker demo writes mail to `server/tmp/mail` (`:file` delivery) instead of SMTP.
|
|
190
|
+
|
|
191
|
+
### After the first admin
|
|
192
|
+
|
|
193
|
+
* Revisiting onboarding routes redirects away (wizard locked)
|
|
194
|
+
* `/signup` is not routed; the login page has no “Create an account” link
|
|
195
|
+
* New users: **Admin → Users**
|
|
196
|
+
|
|
197
|
+
### Checklist before first visit (host app)
|
|
198
|
+
|
|
199
|
+
1. `rails nquery:setup` succeeded
|
|
200
|
+
2. Engine mounted
|
|
201
|
+
3. `mailer_sender` + `smtp` configured
|
|
202
|
+
4. Action Mailer default URL options set in the host app (so confirmation links resolve)
|
|
203
|
+
5. Active Storage installed if you want logo/cover uploads during company setup
|
|
204
|
+
|
|
205
|
+
## Mail and SMTP
|
|
206
|
+
|
|
207
|
+
Confirmation mail uses `Nquery::DeviseMailer` and `Nquery.configuration.mailer_sender`.
|
|
208
|
+
|
|
209
|
+
Also configure the host app’s Action Mailer URL options so links in email are absolute:
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
# config/environments/production.rb (example)
|
|
213
|
+
config.action_mailer.default_url_options = { host: "analytics.example.com", protocol: "https" }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
In development, Letter Opener or `:test` / `:file` delivery works as long as confirmation URLs use a reachable host.
|
|
217
|
+
|
|
218
|
+
## Data sources
|
|
219
|
+
|
|
220
|
+
`config.data_sources` describes **identity** for sources that setup syncs into the database:
|
|
221
|
+
|
|
222
|
+
```ruby
|
|
223
|
+
config.data_sources = {
|
|
224
|
+
main: { adapter: :rails, name: "Application database" }
|
|
225
|
+
}
|
|
226
|
+
config.default_data_source = :main
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
* Keys become the unique `key` on `nquery_data_sources` (e.g. `"main"`)
|
|
230
|
+
* For `adapter: :rails`, connection config is empty — nquery uses the host app’s Active Record connection
|
|
231
|
+
* Do **not** put database credentials in `Nquery.configure`; external adapters are managed in the admin UI after install
|
|
232
|
+
* `Nquery::DataSources::Syncer` ignores credential-like keys if they appear in the hash
|
|
233
|
+
|
|
234
|
+
After setup, review sources under **Admin → Data sources**.
|
|
235
|
+
|
|
236
|
+
## Active Storage
|
|
237
|
+
|
|
238
|
+
Required for organization `logo` and `cover_image` during company onboarding (and later branding):
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
rails active_storage:install
|
|
43
242
|
rails db:migrate
|
|
44
243
|
```
|
|
45
244
|
|
|
245
|
+
Configure `config.active_storage.service` in each environment (e.g. `:local` in development).
|
|
246
|
+
|
|
247
|
+
## Development
|
|
248
|
+
|
|
249
|
+
This repository includes a thin demo Rails app under `server/` (SQLite) for local UI work. Host apps install nquery migrations into their own database; the demo app is for contributors and exploration.
|
|
250
|
+
|
|
251
|
+
After checking out the repo:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
cp .env.example .env
|
|
255
|
+
make up
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
`make up` builds the Docker image, runs migrations, seeds demo data (`nquery:seed`), and starts the server at http://localhost:3000.
|
|
259
|
+
|
|
260
|
+
| Email | Password |
|
|
261
|
+
|-------|----------|
|
|
262
|
+
| `admin@nquery.dev` | `password123` |
|
|
263
|
+
|
|
264
|
+
The demo seeds an organization + confirmed admin so you skip the first-run wizard and can sign in with Devise immediately.
|
|
265
|
+
|
|
266
|
+
| Command | Description |
|
|
267
|
+
|---------|-------------|
|
|
268
|
+
| `make up` | Build, migrate, seed, and start the demo server |
|
|
269
|
+
| `make setup` | Migrate and seed only (requires a running `nquery` service) |
|
|
270
|
+
| `make tests` | Run RSpec via `docker compose exec` (requires running `nquery`) |
|
|
271
|
+
| `make console` | Rails console for the demo app |
|
|
272
|
+
| `make bash` | Shell inside the `nquery` container |
|
|
273
|
+
|
|
274
|
+
| Path | Purpose |
|
|
275
|
+
|------|---------|
|
|
276
|
+
| `lib/` + `app/` | Mountable engine gem |
|
|
277
|
+
| `spec/` | RSpec suite for the gem |
|
|
278
|
+
| `server/` | Thin demo Rails app |
|
|
279
|
+
| Docker + Makefile | Local development (recommended) |
|
|
280
|
+
|
|
281
|
+
To install this gem onto your local machine from the checkout, use your usual Bundler path / gem build workflow against `nquery.gemspec`.
|
|
282
|
+
|
|
46
283
|
## Contributing
|
|
47
284
|
|
|
48
|
-
Bug reports and pull requests are welcome
|
|
285
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/caiohenrique12/nquery.
|
|
286
|
+
|
|
287
|
+
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for the workflow (issue discussion, focused PRs, tests, and development setup). AI / agent contributors should follow [AGENTS.md](AGENTS.md).
|
|
49
288
|
|
|
50
289
|
## License
|
|
51
290
|
|
|
52
|
-
MIT
|
|
291
|
+
The gem is available as open source under the terms of the [MIT License](LICENSE).
|
|
@@ -28,7 +28,7 @@ body.nq-body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe
|
|
|
28
28
|
.nq-breadcrumb-item a[aria-current="page"] { color: var(--nq-text); font-weight: 500; }
|
|
29
29
|
.nq-nav-section { font-size: 0.75rem; text-transform: uppercase; color: #94a3b8; margin: 1rem 0 0.5rem; }
|
|
30
30
|
.nq-main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
|
31
|
-
.nq-topbar { display: flex; justify-content:
|
|
31
|
+
.nq-topbar { display: flex; justify-content: flex-end; align-items: center; padding: 0.75rem 1.5rem; background: var(--nq-card); border-bottom: 1px solid var(--nq-border); }
|
|
32
32
|
.nq-content { padding: 1.5rem; flex: 1; }
|
|
33
33
|
.nq-page-header { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "title actions" "meta meta"; gap: 0.375rem 1rem; align-items: start; margin-bottom: 1.5rem; }
|
|
34
34
|
.nq-page-header h1 { grid-area: title; margin: 0; font-size: 1.5rem; }
|
|
@@ -134,6 +134,26 @@ button.nq-menu-item-button.is-loading::before { content: ""; width: 1em; height:
|
|
|
134
134
|
.nq-textarea { font-family: ui-monospace, monospace; resize: vertical; }
|
|
135
135
|
.nq-form-row { margin-bottom: 1rem; }
|
|
136
136
|
.nq-form-row label { display: block; margin-bottom: 0.25rem; font-weight: 500; font-size: 0.875rem; }
|
|
137
|
+
.nq-form-grid { margin-bottom: 1rem; }
|
|
138
|
+
.nq-form-grid .nq-form-row { margin-bottom: 0; }
|
|
139
|
+
.nq-form-card { max-width: 40rem; }
|
|
140
|
+
.nq-form-section { margin-bottom: 0.5rem; }
|
|
141
|
+
.nq-form-section-title { margin: 0 0 1rem; font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--nq-muted); }
|
|
142
|
+
.nq-form-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: flex-end; align-items: center; margin-top: 1.5rem; padding-top: 1.25rem; border-top: 1px solid var(--nq-border); }
|
|
143
|
+
.nq-form-actions-start { margin-right: auto; }
|
|
144
|
+
.nq-connection-status { margin: 0.75rem 0 0; font-size: 0.875rem; line-height: 1.4; }
|
|
145
|
+
.nq-connection-status.is-success { color: #166534; }
|
|
146
|
+
.nq-connection-status.is-error { color: #991b1b; }
|
|
147
|
+
.nq-form-errors { margin-bottom: 1.25rem; padding: 0.875rem 1rem; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; font-size: 0.875rem; }
|
|
148
|
+
.nq-form-errors p { margin: 0 0 0.5rem; }
|
|
149
|
+
.nq-form-errors ul { margin: 0; padding-left: 1.25rem; }
|
|
150
|
+
.nq-field-hint { margin: 0.375rem 0 0; font-size: 0.8125rem; color: var(--nq-muted); line-height: 1.4; }
|
|
151
|
+
.nq-textarea-prose { font-family: inherit; resize: vertical; min-height: 5rem; }
|
|
152
|
+
.nq-badge-system { background: #dbeafe; color: #1d4ed8; }
|
|
153
|
+
.nq-badge-custom { background: #e2e8f0; color: #334155; }
|
|
154
|
+
.nq-group-description p { margin: 0; line-height: 1.5; color: var(--nq-text); }
|
|
155
|
+
.nq-group-layout { align-items: start; }
|
|
156
|
+
.nq-add-member-form .nq-form-row:last-of-type { margin-bottom: 1.25rem; }
|
|
137
157
|
.nq-table { width: 100%; border-collapse: collapse; }
|
|
138
158
|
.nq-table th, .nq-table td { text-align: left; padding: 0.75rem; border-bottom: 1px solid var(--nq-border); }
|
|
139
159
|
.nq-card-table { padding: 0; overflow: hidden; }
|
|
@@ -148,6 +168,28 @@ button.nq-menu-item-button.is-loading::before { content: ""; width: 1em; height:
|
|
|
148
168
|
.nq-card-table .nq-table td .nq-actions { justify-content: flex-end; }
|
|
149
169
|
.nq-card-table .nq-table td .nq-actions form { display: inline-flex; margin: 0; }
|
|
150
170
|
.nq-card-table .nq-table th:last-child { text-align: right; }
|
|
171
|
+
.nq-card-table.nq-logs-table { overflow-x: auto; overflow-y: hidden; -webkit-overflow-scrolling: touch; }
|
|
172
|
+
.nq-logs-table .nq-table { width: 100%; table-layout: fixed; }
|
|
173
|
+
.nq-logs-table .nq-table th,
|
|
174
|
+
.nq-logs-table .nq-table td { vertical-align: top; }
|
|
175
|
+
.nq-logs-table .nq-table th:nth-child(1),
|
|
176
|
+
.nq-logs-table .nq-table td:nth-child(1) { width: 18%; }
|
|
177
|
+
.nq-logs-table .nq-table th:nth-child(2),
|
|
178
|
+
.nq-logs-table .nq-table td:nth-child(2) { width: 34%; }
|
|
179
|
+
.nq-logs-table .nq-table th:nth-child(3),
|
|
180
|
+
.nq-logs-table .nq-table td:nth-child(3),
|
|
181
|
+
.nq-logs-table .nq-table th:nth-child(4),
|
|
182
|
+
.nq-logs-table .nq-table td:nth-child(4) { width: 14%; }
|
|
183
|
+
.nq-logs-table .nq-table th:nth-child(5),
|
|
184
|
+
.nq-logs-table .nq-table td:nth-child(5) { width: 6.5rem; }
|
|
185
|
+
.nq-logs-table .nq-table th:nth-child(6),
|
|
186
|
+
.nq-logs-table .nq-table td:nth-child(6) { width: 8.5rem; }
|
|
187
|
+
.nq-logs-table .nq-table td { overflow: hidden; text-overflow: ellipsis; }
|
|
188
|
+
.nq-logs-table .nq-table td:nth-child(5),
|
|
189
|
+
.nq-logs-table .nq-table td:nth-child(6) { overflow: visible; text-overflow: unset; white-space: nowrap; }
|
|
190
|
+
.nq-logs-query { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 0.75rem; line-height: 1.4; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; background: #f8fafc; padding: 0.25rem 0.5rem; border-radius: 4px; }
|
|
191
|
+
.nq-badge-success { background: #dcfce7; color: #15803d; }
|
|
192
|
+
.nq-badge-error { background: #fee2e2; color: #b91c1c; }
|
|
151
193
|
.nq-list { list-style: none; padding: 0; margin: 0; }
|
|
152
194
|
.nq-list li { padding: 0.5rem 0; border-bottom: 1px solid var(--nq-border); }
|
|
153
195
|
.nq-badge { display: inline-block; padding: 0.125rem 0.5rem; background: #e2e8f0; border-radius: 999px; font-size: 0.75rem; }
|
|
@@ -229,7 +271,6 @@ button.nq-menu-item-button.is-loading::before { content: ""; width: 1em; height:
|
|
|
229
271
|
.nq-auth-footer { display: flex; flex-wrap: wrap; justify-content: center; align-items: center; gap: 0.375rem; margin-top: 1.5rem; padding-top: 1.5rem; border-top: 1px solid var(--nq-border); font-size: 0.875rem; }
|
|
230
272
|
.nq-auth-link { color: var(--nq-primary); font-weight: 600; text-decoration: none; }
|
|
231
273
|
.nq-auth-link:hover { color: var(--nq-primary-dark); text-decoration: underline; }
|
|
232
|
-
.nq-search .nq-input { width: 320px; }
|
|
233
274
|
.nq-topbar-actions { display: flex; align-items: center; gap: 1rem; }
|
|
234
275
|
.nq-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; justify-content: flex-end; }
|
|
235
276
|
.nq-actions form { display: inline-flex; margin: 0; }
|
|
@@ -292,14 +333,13 @@ button.nq-menu-item-button.is-loading::before { content: ""; width: 1em; height:
|
|
|
292
333
|
.nq-home-subtitle { margin: 0.5rem 0 0; font-size: 0.9375rem; }
|
|
293
334
|
.nq-home-hero-actions { display: flex; align-items: center; gap: 0.75rem; flex-shrink: 0; }
|
|
294
335
|
.nq-home-hero-cta { flex-shrink: 0; }
|
|
295
|
-
.nq-home-actions { display: grid; grid-template-columns: repeat(
|
|
336
|
+
.nq-home-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
|
|
296
337
|
.nq-home-action-card { display: flex; flex-direction: column; gap: 0.5rem; padding: 1.25rem; border: 1px solid var(--nq-border); border-radius: 8px; background: var(--nq-card); text-decoration: none; color: inherit; transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; }
|
|
297
338
|
.nq-home-action-card:hover { border-color: var(--nq-primary); box-shadow: 0 8px 24px rgba(80, 158, 227, 0.12); transform: translateY(-1px); }
|
|
298
339
|
.nq-home-action-icon { display: inline-flex; align-items: center; justify-content: center; width: 2.5rem; height: 2.5rem; border-radius: 10px; }
|
|
299
340
|
.nq-home-action-icon .nq-icon { width: 1.25rem; height: 1.25rem; }
|
|
300
341
|
.nq-home-action-icon-folder { background: #fef3c7; color: #b45309; }
|
|
301
342
|
.nq-home-action-icon-dashboard { background: #dcfce7; color: #15803d; }
|
|
302
|
-
.nq-home-action-icon-chart { background: #dbeafe; color: #1d4ed8; }
|
|
303
343
|
.nq-home-action-label { font-size: 1rem; font-weight: 600; color: var(--nq-text); }
|
|
304
344
|
.nq-home-action-desc { font-size: 0.8125rem; line-height: 1.4; }
|
|
305
345
|
.nq-home-recents { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; align-items: start; }
|
|
@@ -336,4 +376,21 @@ button.nq-menu-item-button.is-loading::before { content: ""; width: 1em; height:
|
|
|
336
376
|
.nq-home-actions, .nq-home-recents { grid-template-columns: 1fr; }
|
|
337
377
|
.nq-home-list-time { display: none; }
|
|
338
378
|
.nq-auth-form-row-split { grid-template-columns: 1fr; }
|
|
379
|
+
.nq-logs-table .nq-table { min-width: 0; table-layout: auto; }
|
|
380
|
+
.nq-logs-table .nq-table thead { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; }
|
|
381
|
+
.nq-logs-table .nq-table,
|
|
382
|
+
.nq-logs-table .nq-table tbody,
|
|
383
|
+
.nq-logs-table .nq-table tr,
|
|
384
|
+
.nq-logs-table .nq-table td { display: block; width: 100%; }
|
|
385
|
+
.nq-logs-table .nq-table th:nth-child(n),
|
|
386
|
+
.nq-logs-table .nq-table td:nth-child(n) { width: 100%; overflow: visible; text-overflow: unset; white-space: normal; }
|
|
387
|
+
.nq-logs-table .nq-table tr { padding: 1rem 1.25rem; border-bottom: 1px solid var(--nq-border); }
|
|
388
|
+
.nq-logs-table .nq-table tbody tr:last-child { border-bottom: none; }
|
|
389
|
+
.nq-logs-table .nq-table td { display: grid; grid-template-columns: 6.5rem minmax(0, 1fr); align-items: start; gap: 0.35rem 0.75rem; padding: 0.35rem 0; border-bottom: none; text-align: left; }
|
|
390
|
+
.nq-logs-table .nq-table td::before { content: attr(data-label); font-size: 0.75rem; font-weight: 600; color: var(--nq-muted); text-transform: uppercase; letter-spacing: 0.04em; }
|
|
391
|
+
.nq-logs-table .nq-table th:last-child,
|
|
392
|
+
.nq-logs-table .nq-table td:last-child { text-align: left; }
|
|
393
|
+
.nq-logs-query { max-width: 100%; white-space: normal; overflow: visible; text-overflow: unset; word-break: break-word; }
|
|
394
|
+
.nq-logs-empty td { display: block; }
|
|
395
|
+
.nq-logs-empty td::before { content: none; }
|
|
339
396
|
}
|
|
@@ -808,6 +808,23 @@ function initChartBuilders() {
|
|
|
808
808
|
selectWorkspaceTab("output")
|
|
809
809
|
}
|
|
810
810
|
|
|
811
|
+
const applyResult = (data, { outputTab } = {}) => {
|
|
812
|
+
if (!data?.columns) return
|
|
813
|
+
|
|
814
|
+
currentResult = data
|
|
815
|
+
if (outputTab) currentOutputTab = outputTab
|
|
816
|
+
populateAxisSelects(data.columns, data.rows)
|
|
817
|
+
renderTable(data)
|
|
818
|
+
if (meta) {
|
|
819
|
+
const parts = []
|
|
820
|
+
if (data.row_count != null) parts.push(`${data.row_count} rows`)
|
|
821
|
+
if (data.duration_ms != null) parts.push(`${data.duration_ms} ms`)
|
|
822
|
+
meta.textContent = parts.join(" · ")
|
|
823
|
+
meta.removeAttribute("hidden")
|
|
824
|
+
}
|
|
825
|
+
showResults()
|
|
826
|
+
}
|
|
827
|
+
|
|
811
828
|
const runQuery = async (button) => {
|
|
812
829
|
syncStatementField()
|
|
813
830
|
setButtonLoading(button, true)
|
|
@@ -824,18 +841,7 @@ function initChartBuilders() {
|
|
|
824
841
|
return
|
|
825
842
|
}
|
|
826
843
|
|
|
827
|
-
|
|
828
|
-
currentOutputTab = "table"
|
|
829
|
-
populateAxisSelects(data.columns, data.rows)
|
|
830
|
-
renderTable(data)
|
|
831
|
-
if (meta) {
|
|
832
|
-
const parts = []
|
|
833
|
-
if (data.row_count != null) parts.push(`${data.row_count} rows`)
|
|
834
|
-
if (data.duration_ms != null) parts.push(`${data.duration_ms} ms`)
|
|
835
|
-
meta.textContent = parts.join(" · ")
|
|
836
|
-
meta.removeAttribute("hidden")
|
|
837
|
-
}
|
|
838
|
-
showResults()
|
|
844
|
+
applyResult(data, { outputTab: "table" })
|
|
839
845
|
} catch (e) {
|
|
840
846
|
showError(e.message)
|
|
841
847
|
} finally {
|
|
@@ -935,6 +941,21 @@ function initChartBuilders() {
|
|
|
935
941
|
|
|
936
942
|
xAxis?.addEventListener("change", updateMapping)
|
|
937
943
|
yAxis?.addEventListener("change", updateMapping)
|
|
944
|
+
|
|
945
|
+
// Saved charts: seed Output with the persisted query result and plot the saved viz.
|
|
946
|
+
const initialResultRaw = root.dataset.initialResult
|
|
947
|
+
if (initialResultRaw) {
|
|
948
|
+
try {
|
|
949
|
+
const initialResult = JSON.parse(initialResultRaw)
|
|
950
|
+
if (initialResult.error) {
|
|
951
|
+
showError(initialResult.error)
|
|
952
|
+
} else {
|
|
953
|
+
applyResult(initialResult)
|
|
954
|
+
}
|
|
955
|
+
} catch (_error) {
|
|
956
|
+
// Ignore malformed bootstrap payloads and keep the empty Output state.
|
|
957
|
+
}
|
|
958
|
+
}
|
|
938
959
|
})
|
|
939
960
|
}
|
|
940
961
|
|
|
@@ -966,6 +987,79 @@ function initQueryEditors() {
|
|
|
966
987
|
})
|
|
967
988
|
}
|
|
968
989
|
|
|
990
|
+
function initDataSourceForms() {
|
|
991
|
+
document.querySelectorAll("[data-controller='data-source-form']").forEach(root => {
|
|
992
|
+
if (root.dataset.dataSourceFormInitialized === "true") return
|
|
993
|
+
root.dataset.dataSourceFormInitialized = "true"
|
|
994
|
+
|
|
995
|
+
const adapterField = root.querySelector("[data-data-source-form-target='adapter']")
|
|
996
|
+
const remoteFields = root.querySelector("[data-data-source-form-target='remoteFields']")
|
|
997
|
+
const sqliteFields = root.querySelector("[data-data-source-form-target='sqliteFields']")
|
|
998
|
+
const railsHint = root.querySelector("[data-data-source-form-target='railsHint']")
|
|
999
|
+
const portField = root.querySelector("[data-data-source-form-target='port']")
|
|
1000
|
+
const testButton = root.querySelector("[data-data-source-form-target='testButton']")
|
|
1001
|
+
const testStatus = root.querySelector("[data-data-source-form-target='testStatus']")
|
|
1002
|
+
|
|
1003
|
+
const toggle = () => {
|
|
1004
|
+
const adapter = adapterField?.value || root.dataset.dataSourceFormAdapterValue || "postgresql"
|
|
1005
|
+
const isRemote = adapter === "postgresql" || adapter === "mysql"
|
|
1006
|
+
const isSqlite = adapter === "sqlite"
|
|
1007
|
+
const isRails = adapter === "rails"
|
|
1008
|
+
|
|
1009
|
+
remoteFields?.toggleAttribute("hidden", !isRemote)
|
|
1010
|
+
sqliteFields?.toggleAttribute("hidden", !isSqlite)
|
|
1011
|
+
railsHint?.toggleAttribute("hidden", !isRails)
|
|
1012
|
+
|
|
1013
|
+
if (portField) {
|
|
1014
|
+
portField.placeholder = adapter === "mysql" ? "3306" : "5432"
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
adapterField?.addEventListener("change", toggle)
|
|
1019
|
+
toggle()
|
|
1020
|
+
|
|
1021
|
+
testButton?.addEventListener("click", async (event) => {
|
|
1022
|
+
event.preventDefault()
|
|
1023
|
+
const csrf = document.querySelector('meta[name="csrf-token"]')?.content
|
|
1024
|
+
const formData = new FormData(root)
|
|
1025
|
+
formData.delete("_method")
|
|
1026
|
+
const dataSourceId = root.dataset.dataSourceFormIdValue
|
|
1027
|
+
if (dataSourceId) formData.append("data_source_id", dataSourceId)
|
|
1028
|
+
|
|
1029
|
+
setButtonLoading(testButton, true)
|
|
1030
|
+
if (testStatus) {
|
|
1031
|
+
testStatus.hidden = true
|
|
1032
|
+
testStatus.textContent = ""
|
|
1033
|
+
testStatus.classList.remove("is-success", "is-error")
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
try {
|
|
1037
|
+
const response = await fetch(root.dataset.dataSourceFormTestUrlValue, {
|
|
1038
|
+
method: "POST",
|
|
1039
|
+
headers: { Accept: "application/json", "X-CSRF-Token": csrf },
|
|
1040
|
+
body: formData
|
|
1041
|
+
})
|
|
1042
|
+
const data = await response.json()
|
|
1043
|
+
const success = response.ok && data.ok
|
|
1044
|
+
if (testStatus) {
|
|
1045
|
+
testStatus.textContent = success ? (data.message || "Connection successful.") : (data.error || "Connection failed.")
|
|
1046
|
+
testStatus.classList.toggle("is-success", success)
|
|
1047
|
+
testStatus.classList.toggle("is-error", !success)
|
|
1048
|
+
testStatus.hidden = false
|
|
1049
|
+
}
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
if (testStatus) {
|
|
1052
|
+
testStatus.textContent = error.message || "Connection failed."
|
|
1053
|
+
testStatus.classList.add("is-error")
|
|
1054
|
+
testStatus.hidden = false
|
|
1055
|
+
}
|
|
1056
|
+
} finally {
|
|
1057
|
+
setButtonLoading(testButton, false)
|
|
1058
|
+
}
|
|
1059
|
+
})
|
|
1060
|
+
})
|
|
1061
|
+
}
|
|
1062
|
+
|
|
969
1063
|
function initPage() {
|
|
970
1064
|
initButtonLoaders()
|
|
971
1065
|
initToastEvents()
|
|
@@ -974,6 +1068,7 @@ function initPage() {
|
|
|
974
1068
|
initQueryEditors()
|
|
975
1069
|
initChartBuilders()
|
|
976
1070
|
initChartPreviews()
|
|
1071
|
+
initDataSourceForms()
|
|
977
1072
|
}
|
|
978
1073
|
|
|
979
1074
|
function bootPage() {
|
|
@@ -56,6 +56,8 @@ module Nquery
|
|
|
56
56
|
when "edit"
|
|
57
57
|
append_resource_breadcrumb(crumbs, breadcrumb_dashboard, path: dashboard_path(breadcrumb_dashboard), link: true)
|
|
58
58
|
append_terminal_breadcrumb(crumbs, "Edit")
|
|
59
|
+
when "new"
|
|
60
|
+
append_terminal_breadcrumb(crumbs, "New dashboard")
|
|
59
61
|
end
|
|
60
62
|
when "nquery/collection/dashboards"
|
|
61
63
|
append_section_breadcrumb(crumbs, "Collections", collections_path)
|
|
@@ -169,11 +171,11 @@ module Nquery
|
|
|
169
171
|
def breadcrumb_parent_collection
|
|
170
172
|
@breadcrumb_parent_collection ||= if @parent_collection
|
|
171
173
|
@parent_collection
|
|
172
|
-
|
|
174
|
+
elsif @collection&.persisted?
|
|
173
175
|
@collection
|
|
174
|
-
|
|
176
|
+
elsif params[:collection_id].present?
|
|
175
177
|
Collection.find_by(id: params[:collection_id])
|
|
176
|
-
|
|
178
|
+
end
|
|
177
179
|
end
|
|
178
180
|
|
|
179
181
|
def breadcrumb_dashboard
|
|
@@ -95,6 +95,7 @@ module Nquery
|
|
|
95
95
|
def load_chart_builder_assigns
|
|
96
96
|
@data_sources = DataSource.active.order(:name)
|
|
97
97
|
@schema = schema_for(@chart&.query&.data_source)
|
|
98
|
+
@result = chart_builder_result(@chart) if @chart&.persisted? && @chart.query&.statement.present?
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
def schema_for(data_source = nil)
|