nquery 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +19 -0
  3. data/README.md +54 -0
  4. data/app/assets/builds/nquery/application.css +22 -2
  5. data/app/assets/builds/nquery/application.js +48 -34
  6. data/app/controllers/concerns/nquery/breadcrumbs.rb +2 -13
  7. data/app/controllers/concerns/nquery/chart_actions.rb +4 -4
  8. data/app/controllers/concerns/nquery/chart_results.rb +18 -4
  9. data/app/controllers/concerns/nquery/sharing_page.rb +38 -0
  10. data/app/controllers/nquery/application_controller.rb +2 -0
  11. data/app/controllers/nquery/dashboards_controller.rb +8 -3
  12. data/app/controllers/nquery/embed/charts_controller.rb +15 -13
  13. data/app/controllers/nquery/embed/dashboards_controller.rb +18 -8
  14. data/app/controllers/nquery/public/charts_controller.rb +19 -0
  15. data/app/controllers/nquery/public/dashboards_controller.rb +20 -0
  16. data/app/controllers/nquery/public_resources_controller.rb +79 -0
  17. data/app/controllers/nquery/queries_controller.rb +5 -49
  18. data/app/controllers/nquery/sharing/base_controller.rb +89 -0
  19. data/app/controllers/nquery/sharing/embed_tokens_controller.rb +41 -0
  20. data/app/controllers/nquery/sharing/embeddings_controller.rb +17 -0
  21. data/app/controllers/nquery/sharing/public_links_controller.rb +21 -0
  22. data/app/helpers/nquery/charts_helper.rb +13 -0
  23. data/app/helpers/nquery/sharing_helper.rb +67 -0
  24. data/app/javascript/nquery/controllers/chart_preview_controller.js +19 -1
  25. data/app/javascript/nquery/controllers/clipboard_controller.js +23 -0
  26. data/app/models/concerns/nquery/shareable.rb +43 -0
  27. data/app/models/nquery/chart.rb +14 -2
  28. data/app/models/nquery/dashboard.rb +2 -0
  29. data/app/models/nquery/data_source.rb +1 -1
  30. data/app/models/nquery/query.rb +4 -0
  31. data/app/views/layouts/nquery/embed.html.erb +3 -1
  32. data/app/views/nquery/charts/_builder_form.html.erb +1 -6
  33. data/app/views/nquery/charts/embed.html.erb +5 -11
  34. data/app/views/nquery/charts/new.html.erb +2 -1
  35. data/app/views/nquery/dashboard/charts/new.html.erb +2 -1
  36. data/app/views/nquery/dashboards/embed.html.erb +9 -0
  37. data/app/views/nquery/dashboards/show.html.erb +4 -0
  38. data/app/views/nquery/embed/charts/show.html.erb +7 -8
  39. data/app/views/nquery/embed/dashboards/show.html.erb +12 -6
  40. data/app/views/nquery/embed/unavailable.html.erb +1 -0
  41. data/app/views/nquery/shared/_icon.html.erb +6 -0
  42. data/app/views/nquery/shared/_shared_chart_card.html.erb +20 -0
  43. data/app/views/nquery/shared/_sharing_panel.html.erb +94 -0
  44. data/config/routes.rb +20 -1
  45. data/db/migrate/20260907120000_add_sharing_to_nquery_charts_and_dashboards.rb +19 -0
  46. data/db/migrate/20260907130000_remove_embedding_params_from_nquery_charts_and_dashboards.rb +8 -0
  47. data/lib/nquery/boolean.rb +9 -0
  48. data/lib/nquery/configuration.rb +6 -0
  49. data/lib/nquery/embed_token_service.rb +10 -5
  50. data/lib/nquery/encryption.rb +1 -1
  51. data/lib/nquery/seeder.rb +20 -6
  52. data/lib/nquery/version.rb +1 -1
  53. data/lib/nquery.rb +1 -0
  54. metadata +20 -5
  55. data/app/javascript/nquery/controllers/query_editor_controller.js +0 -41
  56. data/app/views/nquery/queries/edit.html.erb +0 -40
  57. data/app/views/nquery/queries/new.html.erb +0 -37
@@ -10,6 +10,10 @@
10
10
  <%= nq_icon(:chart) %>
11
11
  <span>New chart</span>
12
12
  <% end %>
13
+ <%= link_to dashboard_embed_path(@dashboard), class: "nq-btn" do %>
14
+ <%= nq_icon(:share) %>
15
+ <span>Embed</span>
16
+ <% end %>
13
17
  <%= link_to edit_dashboard_path(@dashboard), class: "nq-btn" do %>
14
18
  <%= nq_icon(:edit) %>
15
19
  <span>Edit</span>
@@ -1,9 +1,8 @@
1
- <h1><%= @chart.name %></h1>
1
+ <% if embed_titled? %>
2
+ <h1><%= @chart.name %></h1>
3
+ <% end %>
2
4
 
3
- <div class="nq-card nq-chart-preview" data-controller="chart-preview"
4
- data-chart-preview-type-value="<%= @chart.chart_type %>"
5
- data-chart-preview-x-value="<%= @chart.visualization.to_h["x"] %>"
6
- data-chart-preview-y-value="<%= @chart.visualization.to_h["y"] %>"
7
- data-chart-preview-data-value="<%= @result.to_json %>">
8
- <canvas data-chart-preview-target="canvas" height="120"></canvas>
9
- </div>
5
+ <%= render "nquery/shared/shared_chart_card",
6
+ chart: @chart,
7
+ result: @result,
8
+ show_title: false %>
@@ -1,7 +1,13 @@
1
- <h1><%= @dashboard.name %></h1>
2
-
3
- <% @dashboard.dashboard_cards.each do |card| %>
4
- <section>
5
- <h2><%= card.chart.name %></h2>
6
- </section>
1
+ <% if embed_titled? %>
2
+ <h1><%= @dashboard.name %></h1>
7
3
  <% end %>
4
+
5
+ <div class="nq-dashboard-grid nq-embed-grid">
6
+ <% @dashboard_cards.each do |card| %>
7
+ <%= render "nquery/shared/shared_chart_card",
8
+ chart: card.chart,
9
+ result: @card_results[card],
10
+ width: card.width,
11
+ height: card.height %>
12
+ <% end %>
13
+ </div>
@@ -0,0 +1 @@
1
+ <p class="nq-chart-error"><%= @unavailable_message %></p>
@@ -41,6 +41,12 @@
41
41
  <% when "arrow-right" %>
42
42
  <path d="M5 12h14" />
43
43
  <path d="m12 5 7 7-7 7" />
44
+ <% when "share" %>
45
+ <circle cx="18" cy="5" r="3" />
46
+ <circle cx="6" cy="12" r="3" />
47
+ <circle cx="18" cy="19" r="3" />
48
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
49
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
44
50
  <% when "format" %>
45
51
  <path d="M21 10H3" />
46
52
  <path d="M21 6H3" />
@@ -0,0 +1,20 @@
1
+ <% width = local_assigns.fetch(:width, 12)
2
+ height = local_assigns.fetch(:height, 4)
3
+ show_title = local_assigns.fetch(:show_title, true)
4
+ titled = !respond_to?(:embed_titled?) || embed_titled?
5
+ %>
6
+ <div class="nq-dashboard-card"
7
+ style="grid-column: span <%= width %>; grid-row: span <%= height %>;">
8
+ <% if show_title && titled %>
9
+ <header class="nq-dashboard-card-header">
10
+ <h3 class="nq-dashboard-card-title"><%= chart.name %></h3>
11
+ </header>
12
+ <% end %>
13
+ <div class="nq-chart-preview" data-controller="chart-preview"
14
+ data-chart-preview-type-value="<%= chart.chart_type %>"
15
+ data-chart-preview-x-value="<%= chart.visualization.to_h["x"] %>"
16
+ data-chart-preview-y-value="<%= chart.visualization.to_h["y"] %>"
17
+ data-chart-preview-data-value="<%= result.to_json %>">
18
+ <canvas data-chart-preview-target="canvas" height="80"></canvas>
19
+ </div>
20
+ </div>
@@ -0,0 +1,94 @@
1
+ <% shareable = local_assigns.fetch(:shareable)
2
+ public_url = local_assigns[:public_url]
3
+ embed_url = local_assigns[:embed_url]
4
+ embed_token = local_assigns[:embed_token]
5
+ can_curate = can_curate_shareable?(shareable)
6
+ iframe_height = share_iframe_height(shareable)
7
+ public_sharing_enabled = Nquery.configuration.public_sharing_enabled
8
+ static_embedding_enabled = Nquery.configuration.static_embedding_enabled
9
+ %>
10
+
11
+ <section class="nq-card nq-sharing-section">
12
+ <h2>Public link</h2>
13
+ <% unless public_sharing_enabled %>
14
+ <p class="nq-muted">Public sharing is disabled. Set <code>config.public_sharing_enabled = true</code> in the host app initializer.</p>
15
+ <% end %>
16
+
17
+ <% if public_url %>
18
+ <p class="nq-muted">Anyone with this link can view the visualization without signing in.</p>
19
+ <div class="nq-copy-field">
20
+ <pre class="nq-code" id="public-share-url"><%= public_url %></pre>
21
+ <button type="button" class="nq-btn" data-copy-button data-copy-target="#public-share-url">Copy</button>
22
+ </div>
23
+ <h3>Iframe snippet</h3>
24
+ <div class="nq-copy-field">
25
+ <pre class="nq-code" id="public-share-iframe"><%= share_iframe_snippet(public_url, height: iframe_height) %></pre>
26
+ <button type="button" class="nq-btn" data-copy-button data-copy-target="#public-share-iframe">Copy</button>
27
+ </div>
28
+ <% if can_curate %>
29
+ <div class="nq-actions">
30
+ <%= button_to "Remove public link", shareable_public_link_path(shareable), method: :delete, class: "nq-btn nq-btn-danger", form: { data: { turbo_confirm: "Remove the public link? The public URL will stop working. Embed tokens are revoked separately." } } %>
31
+ </div>
32
+ <% end %>
33
+ <% elsif can_curate && public_sharing_enabled %>
34
+ <p class="nq-muted">Create an unguessable public URL. Anyone with the link can view this visualization.</p>
35
+ <div class="nq-actions">
36
+ <%= button_to "Enable public link", shareable_public_link_path(shareable), method: :post, class: "nq-btn nq-btn-primary" %>
37
+ </div>
38
+ <% elsif can_curate %>
39
+ <p class="nq-muted">Enable public sharing in configuration, then return here to create a link.</p>
40
+ <% else %>
41
+ <p class="nq-muted">No public link yet.</p>
42
+ <% end %>
43
+ </section>
44
+
45
+ <section class="nq-card nq-sharing-section">
46
+ <h2>Static embed</h2>
47
+ <% unless static_embedding_enabled %>
48
+ <p class="nq-muted">Static embedding is disabled. Set <code>config.static_embedding_enabled = true</code> in the host app initializer.</p>
49
+ <% end %>
50
+
51
+ <% if shareable.enable_embedding? %>
52
+ <p class="nq-muted">Embedding is enabled for this visualization.</p>
53
+ <% if can_curate && static_embedding_enabled %>
54
+ <%= button_to "Disable embedding", shareable_embedding_path(shareable), method: :patch, params: { enable_embedding: false }, class: "nq-btn" %>
55
+ <% end %>
56
+ <% elsif can_curate && static_embedding_enabled %>
57
+ <p class="nq-muted">Enable embedding to publish a signed iframe URL.</p>
58
+ <%= button_to "Enable embedding", shareable_embedding_path(shareable), method: :patch, params: { enable_embedding: true }, class: "nq-btn nq-btn-primary" %>
59
+ <% end %>
60
+
61
+ <% if embed_token && embed_url %>
62
+ <p class="nq-muted">Signed embed token (expires <%= embed_token.expires_at&.to_fs(:long) || "never" %>)</p>
63
+ <div class="nq-copy-field">
64
+ <pre class="nq-code" id="static-embed-url"><%= embed_url %></pre>
65
+ <button type="button" class="nq-btn" data-copy-button data-copy-target="#static-embed-url">Copy</button>
66
+ </div>
67
+ <h3>Iframe snippet</h3>
68
+ <div class="nq-copy-field">
69
+ <pre class="nq-code" id="static-embed-iframe"><%= share_iframe_snippet(embed_url, height: iframe_height) %></pre>
70
+ <button type="button" class="nq-btn" data-copy-button data-copy-target="#static-embed-iframe">Copy</button>
71
+ </div>
72
+ <% if can_curate && static_embedding_enabled %>
73
+ <div class="nq-actions">
74
+ <%= button_to "Revoke token", shareable_embed_token_path(shareable, embed_token), method: :delete, class: "nq-btn nq-btn-danger", form: { data: { turbo_confirm: "Revoke this embed token?" } } %>
75
+ </div>
76
+ <% end %>
77
+ <% elsif can_curate && !static_embedding_enabled %>
78
+ <p class="nq-muted">Enable static embedding in configuration, then return here to generate a token.</p>
79
+ <% elsif !can_curate %>
80
+ <p class="nq-muted">No embed token yet.</p>
81
+ <% end %>
82
+
83
+ <% if can_curate && static_embedding_enabled %>
84
+ <%= form_with url: shareable_embed_tokens_path(shareable), method: :post, class: "nq-sharing-token-form" do %>
85
+ <label class="nq-form-row">
86
+ <span>Expires</span>
87
+ <%= select_tag :expires_in, options_for_select(Nquery::SharingHelper::EMBED_EXPIRY_OPTIONS, "1.hour"), class: "nq-input" %>
88
+ </label>
89
+ <div class="nq-actions">
90
+ <%= submit_tag embed_token ? "Regenerate" : "Generate token", class: "nq-btn nq-btn-primary" %>
91
+ </div>
92
+ <% end %>
93
+ <% end %>
94
+ </section>
data/config/routes.rb CHANGED
@@ -23,16 +23,25 @@ Nquery::Engine.routes.draw do
23
23
  resources :dashboards, only: %i[new create], module: :collection
24
24
  end
25
25
 
26
- resources :queries, only: %i[new create edit update show] do
26
+ resources :queries, only: %i[update] do
27
27
  post :run, on: :collection
28
28
  get :schema, on: :collection
29
29
  end
30
30
 
31
+ concern :shareable do
32
+ scope module: :sharing do
33
+ resource :public_link, only: %i[create destroy]
34
+ resource :embedding, only: %i[update]
35
+ resources :embed_tokens, only: %i[create destroy]
36
+ end
37
+ end
38
+
31
39
  resources :charts, only: %i[new create show edit update destroy] do
32
40
  member do
33
41
  get :embed
34
42
  patch :archive
35
43
  end
44
+ concerns :shareable
36
45
  end
37
46
 
38
47
  resources :dashboards do
@@ -42,12 +51,17 @@ Nquery::Engine.routes.draw do
42
51
  patch :archive
43
52
  end
44
53
  end
54
+ resources :charts, only: [] do
55
+ concerns :shareable
56
+ end
45
57
 
46
58
  member do
59
+ get :embed
47
60
  patch :update_layout
48
61
  patch :archive
49
62
  patch :unarchive
50
63
  end
64
+ concerns :shareable
51
65
  end
52
66
 
53
67
  resources :imports, only: %i[new create]
@@ -93,5 +107,10 @@ Nquery::Engine.routes.draw do
93
107
  get "dashboards/show", to: "dashboards#show", as: :public_dashboard
94
108
  end
95
109
 
110
+ scope module: :public, path: "public", as: :public do
111
+ get "charts/:uuid", to: "charts#show", as: :chart
112
+ get "dashboards/:uuid", to: "dashboards#show", as: :dashboard
113
+ end
114
+
96
115
  match "*unmatched", to: "errors#not_found", via: :all
97
116
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddSharingToNqueryChartsAndDashboards < ActiveRecord::Migration[8.1]
4
+ def change
5
+ add_sharing_columns :nquery_charts
6
+ add_sharing_columns :nquery_dashboards
7
+ end
8
+
9
+ private
10
+
11
+ def add_sharing_columns(table)
12
+ add_column table, :public_uuid, :string
13
+ add_index table, :public_uuid, unique: true
14
+ add_reference table, :made_public_by, foreign_key: { to_table: :nquery_users }
15
+ add_column table, :public_shared_at, :datetime
16
+ add_column table, :enable_embedding, :boolean, null: false, default: false
17
+ add_column table, :embedding_params, :json, null: false, default: {}
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RemoveEmbeddingParamsFromNqueryChartsAndDashboards < ActiveRecord::Migration[8.1]
4
+ def change
5
+ remove_column :nquery_charts, :embedding_params, :json, null: false, default: {}
6
+ remove_column :nquery_dashboards, :embedding_params, :json, null: false, default: {}
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Boolean
5
+ def self.cast(value)
6
+ ActiveModel::Type::Boolean.new.cast(value)
7
+ end
8
+ end
9
+ end
@@ -10,6 +10,9 @@ module Nquery
10
10
  attr_accessor :query_timeout
11
11
  attr_accessor :query_row_limit
12
12
  attr_accessor :embed_secret
13
+ attr_accessor :public_sharing_enabled
14
+ attr_accessor :static_embedding_enabled
15
+ attr_accessor :embed_frame_ancestors
13
16
 
14
17
  def initialize
15
18
  @data_sources = { main: { adapter: :rails, name: "Application database" } }
@@ -19,6 +22,9 @@ module Nquery
19
22
  @query_timeout = 15
20
23
  @query_row_limit = 10_000
21
24
  @embed_secret = nil
25
+ @public_sharing_enabled = false
26
+ @static_embedding_enabled = false
27
+ @embed_frame_ancestors = nil
22
28
  end
23
29
 
24
30
  def embed_signing_key
@@ -13,9 +13,9 @@ module Nquery
13
13
  token: token,
14
14
  resource_type: resource_type,
15
15
  resource_id: resource_id,
16
- params: params,
17
- exp: expires_at.to_i
16
+ params: params
18
17
  }
18
+ payload[:exp] = expires_at.to_i if expires_at
19
19
  data = Base64.urlsafe_encode64(payload.to_json)
20
20
 
21
21
  signature = OpenSSL::HMAC.hexdigest("SHA256", signing_key, data)
@@ -44,7 +44,8 @@ module Nquery
44
44
  end
45
45
 
46
46
  payload = JSON.parse(Base64.urlsafe_decode64(data))
47
- raise Error, "Token expired" if payload["exp"].to_i <= Time.current.to_i
47
+ exp = payload["exp"]
48
+ raise Error, "Token expired" if exp.present? && exp.to_i <= Time.current.to_i
48
49
 
49
50
  record = EmbedToken.active.find_by(token: payload["token"])
50
51
  raise Error, "Token not found" unless record
@@ -64,14 +65,18 @@ module Nquery
64
65
  token: record.token,
65
66
  resource_type: record.resource_type,
66
67
  resource_id: record.resource_id,
67
- params: record.params || {},
68
- exp: record.expires_at.to_i
68
+ params: record.params || {}
69
69
  }
70
+ payload[:exp] = record.expires_at.to_i if record.expires_at
70
71
  data = Base64.urlsafe_encode64(payload.to_json)
71
72
  signature = OpenSSL::HMAC.hexdigest("SHA256", signing_key, data)
72
73
  "#{data}.#{signature}"
73
74
  end
74
75
 
76
+ def self.revoke!(record)
77
+ record.update!(active: false)
78
+ end
79
+
75
80
  def self.generate_token
76
81
  SecureRandom.urlsafe_base64(24)
77
82
  end
@@ -28,7 +28,7 @@ module Nquery
28
28
 
29
29
  def support_unencrypted_data
30
30
  if ENV.key?("NQUERY_SUPPORT_UNENCRYPTED_DATA")
31
- return ActiveModel::Type::Boolean.new.cast(ENV.fetch("NQUERY_SUPPORT_UNENCRYPTED_DATA"))
31
+ return Nquery::Boolean.cast(ENV.fetch("NQUERY_SUPPORT_UNENCRYPTED_DATA"))
32
32
  end
33
33
 
34
34
  true
data/lib/nquery/seeder.rb CHANGED
@@ -69,12 +69,11 @@ module Nquery
69
69
  card.height = 4
70
70
  end
71
71
 
72
- EmbedTokenService.sign(
73
- resource_type: "Nquery::Chart",
74
- resource_id: chart.id,
75
- creator: admin,
76
- expires_at: 1.year.from_now
77
- ) unless EmbedToken.exists?(resource_type: "Nquery::Chart", resource_id: chart.id)
72
+ chart.share_publicly!(user: admin)
73
+ ensure_embedding_enabled!(chart)
74
+ ensure_embedding_enabled!(dashboard)
75
+ dashboard.unshare_publicly!
76
+ ensure_active_embed_token!(chart, creator: admin)
78
77
 
79
78
  organization.update!(onboarding_completed_at: Time.current) if organization.onboarding_completed_at.blank?
80
79
  end
@@ -118,5 +117,20 @@ module Nquery
118
117
  p.access_level = "no"
119
118
  end
120
119
  end
120
+
121
+ def ensure_embedding_enabled!(resource)
122
+ resource.update!(enable_embedding: true) unless resource.enable_embedding?
123
+ end
124
+
125
+ def ensure_active_embed_token!(resource, creator:)
126
+ return if resource.active_embed_token
127
+
128
+ EmbedTokenService.sign(
129
+ resource_type: resource.class.name,
130
+ resource_id: resource.id,
131
+ creator: creator,
132
+ expires_at: 1.year.from_now
133
+ )
134
+ end
121
135
  end
122
136
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nquery
4
- VERSION = "0.1.2" unless defined?(Nquery::VERSION)
4
+ VERSION = "0.1.3" unless defined?(Nquery::VERSION)
5
5
  end
data/lib/nquery.rb CHANGED
@@ -6,6 +6,7 @@ require "devise/orm/active_record"
6
6
 
7
7
  require "nquery/version"
8
8
  require "nquery/configuration"
9
+ require "nquery/boolean"
9
10
  require "nquery/encryption"
10
11
  require "nquery/permissions/resolver"
11
12
  require "nquery/authorizes_collection"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nquery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nquery Contributors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-07 00:00:00.000000000 Z
11
+ date: 2026-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -315,6 +315,7 @@ files:
315
315
  - app/controllers/concerns/nquery/browsable.rb
316
316
  - app/controllers/concerns/nquery/chart_actions.rb
317
317
  - app/controllers/concerns/nquery/chart_results.rb
318
+ - app/controllers/concerns/nquery/sharing_page.rb
318
319
  - app/controllers/nquery/admin/base_controller.rb
319
320
  - app/controllers/nquery/admin/data_sources_controller.rb
320
321
  - app/controllers/nquery/admin/groups_controller.rb
@@ -337,8 +338,15 @@ files:
337
338
  - app/controllers/nquery/onboarding/companies_controller.rb
338
339
  - app/controllers/nquery/onboarding/confirmations_controller.rb
339
340
  - app/controllers/nquery/onboarding/congrats_controller.rb
341
+ - app/controllers/nquery/public/charts_controller.rb
342
+ - app/controllers/nquery/public/dashboards_controller.rb
343
+ - app/controllers/nquery/public_resources_controller.rb
340
344
  - app/controllers/nquery/queries_controller.rb
341
345
  - app/controllers/nquery/sessions_controller.rb
346
+ - app/controllers/nquery/sharing/base_controller.rb
347
+ - app/controllers/nquery/sharing/embed_tokens_controller.rb
348
+ - app/controllers/nquery/sharing/embeddings_controller.rb
349
+ - app/controllers/nquery/sharing/public_links_controller.rb
342
350
  - app/helpers/nquery/charts_helper.rb
343
351
  - app/helpers/nquery/collections_helper.rb
344
352
  - app/helpers/nquery/components_helper.rb
@@ -347,13 +355,15 @@ files:
347
355
  - app/helpers/nquery/home_helper.rb
348
356
  - app/helpers/nquery/icon_helper.rb
349
357
  - app/helpers/nquery/navigation_helper.rb
358
+ - app/helpers/nquery/sharing_helper.rb
350
359
  - app/javascript/nquery/controllers/chart_preview_controller.js
360
+ - app/javascript/nquery/controllers/clipboard_controller.js
351
361
  - app/javascript/nquery/controllers/csv_mapper_controller.js
352
362
  - app/javascript/nquery/controllers/dashboard_grid_controller.js
353
363
  - app/javascript/nquery/controllers/data_source_form_controller.js
354
- - app/javascript/nquery/controllers/query_editor_controller.js
355
364
  - app/mailers/nquery/application_mailer.rb
356
365
  - app/mailers/nquery/devise_mailer.rb
366
+ - app/models/concerns/nquery/shareable.rb
357
367
  - app/models/nquery/application_permission.rb
358
368
  - app/models/nquery/application_record.rb
359
369
  - app/models/nquery/audit.rb
@@ -411,6 +421,7 @@ files:
411
421
  - app/views/nquery/dashboard/charts/new.html.erb
412
422
  - app/views/nquery/dashboards/_chart_card.html.erb
413
423
  - app/views/nquery/dashboards/edit.html.erb
424
+ - app/views/nquery/dashboards/embed.html.erb
414
425
  - app/views/nquery/dashboards/index.html.erb
415
426
  - app/views/nquery/dashboards/new.html.erb
416
427
  - app/views/nquery/dashboards/show.html.erb
@@ -418,6 +429,7 @@ files:
418
429
  - app/views/nquery/devise_mailer/confirmation_instructions.text.erb
419
430
  - app/views/nquery/embed/charts/show.html.erb
420
431
  - app/views/nquery/embed/dashboards/show.html.erb
432
+ - app/views/nquery/embed/unavailable.html.erb
421
433
  - app/views/nquery/errors/internal_server_error.html.erb
422
434
  - app/views/nquery/errors/not_found.html.erb
423
435
  - app/views/nquery/home/index.html.erb
@@ -426,8 +438,6 @@ files:
426
438
  - app/views/nquery/onboarding/companies/new.html.erb
427
439
  - app/views/nquery/onboarding/confirmations/show.html.erb
428
440
  - app/views/nquery/onboarding/congrats/show.html.erb
429
- - app/views/nquery/queries/edit.html.erb
430
- - app/views/nquery/queries/new.html.erb
431
441
  - app/views/nquery/sessions/new.html.erb
432
442
  - app/views/nquery/shared/_auth_shell.html.erb
433
443
  - app/views/nquery/shared/_breadcrumbs.html.erb
@@ -435,6 +445,8 @@ files:
435
445
  - app/views/nquery/shared/_chart_row_actions.html.erb
436
446
  - app/views/nquery/shared/_flash.html.erb
437
447
  - app/views/nquery/shared/_icon.html.erb
448
+ - app/views/nquery/shared/_shared_chart_card.html.erb
449
+ - app/views/nquery/shared/_sharing_panel.html.erb
438
450
  - app/views/nquery/shared/_sidebar.html.erb
439
451
  - app/views/nquery/shared/_topbar.html.erb
440
452
  - config/importmap.rb
@@ -447,9 +459,12 @@ files:
447
459
  - db/migrate/20260722000002_create_nquery_organizations.rb
448
460
  - db/migrate/20260722000003_add_devise_to_nquery_users.rb
449
461
  - db/migrate/20260722000004_add_onboarding_completed_at_to_nquery_organizations.rb
462
+ - db/migrate/20260907120000_add_sharing_to_nquery_charts_and_dashboards.rb
463
+ - db/migrate/20260907130000_remove_embedding_params_from_nquery_charts_and_dashboards.rb
450
464
  - lib/generators/nquery/install_generator.rb
451
465
  - lib/nquery.rb
452
466
  - lib/nquery/authorizes_collection.rb
467
+ - lib/nquery/boolean.rb
453
468
  - lib/nquery/configuration.rb
454
469
  - lib/nquery/csv_importer.rb
455
470
  - lib/nquery/data_sources/adapter.rb
@@ -1,41 +0,0 @@
1
- import { Controller } from "@hotwired/stimulus"
2
-
3
- export default class extends Controller {
4
- static targets = ["statement", "results", "dataSource", "schema"]
5
-
6
- insertTable(event) {
7
- const table = event.currentTarget.dataset.table
8
- const textarea = this.statementTarget
9
- const insert = `SELECT * FROM ${table} LIMIT 100`
10
- textarea.value = insert
11
- }
12
-
13
- async run() {
14
- const statement = this.statementTarget.value
15
- const dataSourceId = this.dataSourceTarget?.value
16
- const csrf = document.querySelector('meta[name="csrf-token"]')?.content
17
-
18
- try {
19
- const response = await fetch("/queries/run", {
20
- method: "POST",
21
- headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf },
22
- body: JSON.stringify({ statement, data_source_id: dataSourceId })
23
- })
24
- const data = await response.json()
25
- this.resultsTarget.textContent = JSON.stringify(data, null, 2)
26
- } catch (e) {
27
- this.resultsTarget.textContent = e.message
28
- }
29
- }
30
-
31
- async loadSchema() {
32
- if (!this.hasSchemaTarget) return
33
- const dataSourceId = this.dataSourceTarget.value
34
- const response = await fetch(`/queries/schema?data_source_id=${dataSourceId}`)
35
- const data = await response.json()
36
- this.schemaTarget.innerHTML = data.tables.map(t => {
37
- const name = typeof t === "string" ? t : t.name
38
- return `<li class="nq-tree-item" data-action="click->query-editor#insertTable" data-table="${name}">${name}</li>`
39
- }).join("")
40
- }
41
- }
@@ -1,40 +0,0 @@
1
- <div class="nq-page-header">
2
- <h1>Edit query: <%= @query.name || "Untitled" %></h1>
3
- </div>
4
-
5
- <%= form_with model: @query, url: query_path(@query), method: :patch, class: "nq-form" do |f| %>
6
- <div class="nq-grid nq-grid-editor" data-controller="query-editor">
7
- <aside class="nq-card nq-schema-panel">
8
- <h3>Schema</h3>
9
- <ul class="nq-tree" data-query-editor-target="schema">
10
- <% @schema_tables.each do |table| %>
11
- <li class="nq-tree-item" data-action="click->query-editor#insertTable" data-table="<%= table %>"><%= table %></li>
12
- <% end %>
13
- </ul>
14
- </aside>
15
-
16
- <div class="nq-editor-panel">
17
- <div class="nq-form-row">
18
- <%= f.label :name %>
19
- <%= f.text_field :name, class: "nq-input" %>
20
- </div>
21
- <div class="nq-form-row">
22
- <%= f.label :data_source_id, "Data source" %>
23
- <%= f.collection_select :data_source_id, @data_sources, :id, :name, {}, class: "nq-input",
24
- data: { query_editor_target: "dataSource" } %>
25
- </div>
26
- <div class="nq-form-row">
27
- <%= f.label :statement, "SQL" %>
28
- <%= f.text_area :statement, rows: 12, class: "nq-textarea", data: { query_editor_target: "statement" } %>
29
- </div>
30
- <div class="nq-actions">
31
- <button type="button" class="nq-btn" data-action="click->query-editor#run">Run query</button>
32
- <%= f.submit "Save", class: "nq-btn nq-btn-primary" %>
33
- <% if @query.chart %>
34
- <%= link_to "View chart", chart_path(@query.chart), class: "nq-btn" %>
35
- <% end %>
36
- </div>
37
- <pre class="nq-results" data-query-editor-target="results"></pre>
38
- </div>
39
- </div>
40
- <% end %>
@@ -1,37 +0,0 @@
1
- <div class="nq-page-header">
2
- <h1>New SQL query</h1>
3
- </div>
4
-
5
- <%= form_with model: @query, url: queries_path, class: "nq-form" do |f| %>
6
- <div class="nq-grid nq-grid-editor" data-controller="query-editor">
7
- <aside class="nq-card nq-schema-panel">
8
- <h3>Schema</h3>
9
- <ul class="nq-tree">
10
- <% @schema_tables.each do |table| %>
11
- <li class="nq-tree-item" data-action="click->query-editor#insertTable" data-table="<%= table %>"><%= table %></li>
12
- <% end %>
13
- </ul>
14
- </aside>
15
-
16
- <div class="nq-editor-panel">
17
- <div class="nq-form-row">
18
- <%= f.label :name, "Query name" %>
19
- <%= f.text_field :name, class: "nq-input" %>
20
- </div>
21
- <div class="nq-form-row">
22
- <%= f.label :data_source_id, "Data source" %>
23
- <%= f.collection_select :data_source_id, @data_sources, :id, :name, {}, class: "nq-input",
24
- data: { query_editor_target: "dataSource", action: "change->query-editor#loadSchema" } %>
25
- </div>
26
- <div class="nq-form-row">
27
- <%= f.label :statement, "SQL" %>
28
- <%= f.text_area :statement, rows: 12, class: "nq-textarea", data: { query_editor_target: "statement" } %>
29
- </div>
30
- <div class="nq-actions">
31
- <button type="button" class="nq-btn" data-action="click->query-editor#run">Run query</button>
32
- <%= f.submit "Save", class: "nq-btn nq-btn-primary" %>
33
- </div>
34
- <pre class="nq-results" data-query-editor-target="results"></pre>
35
- </div>
36
- </div>
37
- <% end %>