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
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ class PublicResourcesController < ActionController::Base
5
+ include Nquery::Engine.routes.url_helpers
6
+ include ChartResults
7
+
8
+ protect_from_forgery with: :exception
9
+ layout "nquery/embed"
10
+
11
+ helper Nquery::Engine.helpers
12
+ helper_method :current_nquery_user, :embed_titled?, :embed_bordered?, :embed_theme, :embed_appearance_classes
13
+
14
+ after_action :set_embed_headers
15
+
16
+ def self.apply_embed_rate_limit(to: self)
17
+ if to.respond_to?(:rate_limit)
18
+ to.rate_limit to: 60, within: 1.minute, by: -> { request.remote_ip }
19
+ else
20
+ Rails.logger.warn("[nquery] public/embed endpoints are not rate limited; Rails 7.2+ rate_limit is unavailable")
21
+ end
22
+ end
23
+ apply_embed_rate_limit
24
+
25
+ private
26
+
27
+ def current_nquery_user
28
+ nil
29
+ end
30
+
31
+ def embed_titled?
32
+ params[:titled] != "false"
33
+ end
34
+
35
+ def embed_bordered?
36
+ params[:bordered] != "false"
37
+ end
38
+
39
+ def embed_theme
40
+ theme = params[:theme].to_s
41
+ theme.presence_in(%w[night transparent])
42
+ end
43
+
44
+ def embed_appearance_classes
45
+ classes = ["nq-embed-body"]
46
+ classes << "nq-embed-untitled" unless embed_titled?
47
+ classes << "nq-embed-borderless" unless embed_bordered?
48
+ classes << "nq-embed-theme-#{embed_theme}" if embed_theme
49
+ classes
50
+ end
51
+
52
+ def set_embed_headers
53
+ response.headers.delete("X-Frame-Options")
54
+ response.headers["Content-Security-Policy"] = "frame-ancestors #{frame_ancestors_value}"
55
+ response.headers["X-Robots-Tag"] = "noindex"
56
+ end
57
+
58
+ def frame_ancestors_value
59
+ ancestors = Nquery.configuration.embed_frame_ancestors
60
+ ancestors.present? ? Array(ancestors).join(" ") : "*"
61
+ end
62
+
63
+ def render_unavailable(status:, message: "This visualization is not available.")
64
+ @unavailable_message = message
65
+ render template: "nquery/embed/unavailable", status: status
66
+ end
67
+
68
+ def render_forbidden(message = "Invalid or expired embed token")
69
+ render_unavailable(status: :forbidden, message: message)
70
+ end
71
+
72
+ def load_dashboard_cards(dashboard)
73
+ dashboard.dashboard_cards
74
+ .joins(:chart)
75
+ .merge(Chart.active)
76
+ .includes(chart: { query: :data_source })
77
+ end
78
+ end
79
+ end
@@ -2,55 +2,20 @@
2
2
 
3
3
  module Nquery
4
4
  class QueriesController < ApplicationController
5
- before_action :set_query, only: %i[show edit update]
6
- before_action :authorize_query_collection!, only: %i[show edit update]
5
+ before_action :set_query, only: :update
6
+ before_action :authorize_query_collection!, only: :update
7
7
  before_action :set_data_source_for_run, only: %i[run schema]
8
8
  before_action :authorize_run_data_source!, only: %i[run schema]
9
9
 
10
- def new
11
- @query = Query.new(statement: "SELECT 1 AS example")
12
- @data_sources = DataSource.active.order(:name)
13
- @schema_tables = schema_tables
14
- end
15
-
16
- def create
17
- @query = Query.new(query_params.merge(creator: current_nquery_user))
18
- if @query.save
19
- redirect_to edit_query_path(@query), notice: "Query created."
20
- else
21
- @data_sources = DataSource.active.order(:name)
22
- @schema_tables = schema_tables
23
- render :new, status: :unprocessable_content
24
- end
25
- end
26
-
27
- def edit
28
- @data_sources = DataSource.active.order(:name)
29
- @schema_tables = schema_tables
30
- end
31
-
32
10
  def update
33
11
  if @query.update(query_params)
34
- respond_to do |format|
35
- format.html { redirect_to edit_query_path(@query), notice: "Query saved." }
36
- format.json { render json: { ok: true, notice: "Query saved." } }
37
- end
12
+ render json: { ok: true, notice: "Query saved." }
38
13
  else
39
- @data_sources = DataSource.active.order(:name)
40
- @schema_tables = schema_tables
41
- respond_to do |format|
42
- format.html { render :edit, status: :unprocessable_content }
43
- format.json {
44
- render json: { error: @query.errors.full_messages.to_sentence.presence || "Query could not be saved." },
45
- status: :unprocessable_content
46
- }
47
- end
14
+ render json: { error: @query.errors.full_messages.to_sentence.presence || "Query could not be saved." },
15
+ status: :unprocessable_content
48
16
  end
49
17
  end
50
18
 
51
- def show
52
- end
53
-
54
19
  def run
55
20
  result = QueryRunner.new(
56
21
  data_source: @data_source,
@@ -91,14 +56,5 @@ module Nquery
91
56
  def query_params
92
57
  params.require(:query).permit(:name, :statement, :data_source_id, :collection_id)
93
58
  end
94
-
95
- def schema_tables
96
- data_source = DataSource.first
97
- return [] unless data_source
98
-
99
- DataSources::Adapter.for(data_source).tables
100
- rescue StandardError
101
- []
102
- end
103
59
  end
104
60
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Sharing
5
+ class BaseController < ApplicationController
6
+ before_action :set_shareable
7
+ before_action :authorize_shareable_publish!, only: %i[create update]
8
+ before_action :authorize_shareable_revoke!, only: %i[destroy]
9
+
10
+ private
11
+
12
+ def set_shareable
13
+ @shareable = if params[:chart_id]
14
+ Chart.find(params[:chart_id])
15
+ else
16
+ Dashboard.find(params[:dashboard_id])
17
+ end
18
+ end
19
+
20
+ def authorize_shareable_publish!
21
+ authorize_shareable_collection!
22
+ return if performed?
23
+
24
+ if shareable_missing_data_source?
25
+ deny_collection_access!(missing_data_source_message)
26
+ return
27
+ end
28
+
29
+ shareable_data_sources.each do |data_source|
30
+ authorize_data_source_access!(data_source)
31
+ return if performed?
32
+ end
33
+ end
34
+
35
+ def authorize_shareable_revoke!
36
+ authorize_shareable_collection!
37
+ end
38
+
39
+ def authorize_shareable_collection!
40
+ return if permission_resolver.admin?
41
+
42
+ if @shareable.collection.nil?
43
+ deny_collection_access!("You do not have permission to access this collection.")
44
+ return
45
+ end
46
+
47
+ authorize_collection_access!(@shareable.collection, required: :curate)
48
+ end
49
+
50
+ def shareable_charts
51
+ if @shareable.is_a?(Chart)
52
+ [@shareable]
53
+ else
54
+ @shareable.dashboard_cards
55
+ .joins(:chart)
56
+ .merge(Chart.active)
57
+ .includes(chart: { query: :data_source })
58
+ .map(&:chart)
59
+ end
60
+ end
61
+
62
+ def shareable_data_sources
63
+ shareable_charts.filter_map(&:data_source).uniq
64
+ end
65
+
66
+ def shareable_missing_data_source?
67
+ shareable_charts.any? { |chart| chart.query && chart.data_source.nil? }
68
+ end
69
+
70
+ def missing_data_source_message
71
+ if @shareable.is_a?(Chart)
72
+ "This chart has no data source."
73
+ else
74
+ "A chart on this dashboard has no data source."
75
+ end
76
+ end
77
+
78
+ def sharing_page_path
79
+ if params[:chart_id] && params[:dashboard_id]
80
+ embed_dashboard_chart_path(params[:dashboard_id], params[:chart_id])
81
+ elsif params[:chart_id]
82
+ embed_chart_path(params[:chart_id])
83
+ else
84
+ embed_dashboard_path(params[:dashboard_id])
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Sharing
5
+ class EmbedTokensController < BaseController
6
+ def create
7
+ unless Nquery.configuration.static_embedding_enabled
8
+ return redirect_to sharing_page_path, alert: "Static embedding is disabled."
9
+ end
10
+
11
+ @shareable.embed_tokens.active.find_each { |token| EmbedTokenService.revoke!(token) }
12
+ EmbedTokenService.sign(
13
+ resource_type: @shareable.class.name,
14
+ resource_id: @shareable.id,
15
+ creator: current_nquery_user,
16
+ expires_at: expires_at_from_param
17
+ )
18
+ @shareable.update!(enable_embedding: true) unless @shareable.enable_embedding?
19
+
20
+ redirect_to sharing_page_path, notice: "Embed token generated."
21
+ end
22
+
23
+ def destroy
24
+ token = @shareable.embed_tokens.find(params[:id])
25
+ EmbedTokenService.revoke!(token)
26
+ redirect_to sharing_page_path, notice: "Embed token revoked."
27
+ end
28
+
29
+ private
30
+
31
+ def expires_at_from_param
32
+ case params[:expires_in]
33
+ when "1.day" then 1.day.from_now
34
+ when "30.days" then 30.days.from_now
35
+ when "never" then nil
36
+ else 1.hour.from_now
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Sharing
5
+ class EmbeddingsController < BaseController
6
+ def update
7
+ unless Nquery.configuration.static_embedding_enabled
8
+ return redirect_to sharing_page_path, alert: "Static embedding is disabled."
9
+ end
10
+
11
+ enabled = Nquery::Boolean.cast(params[:enable_embedding])
12
+ @shareable.update!(enable_embedding: enabled)
13
+ redirect_to sharing_page_path, notice: enabled ? "Embedding enabled." : "Embedding disabled."
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Sharing
5
+ class PublicLinksController < BaseController
6
+ def create
7
+ unless Nquery.configuration.public_sharing_enabled
8
+ return redirect_to sharing_page_path, alert: "Public sharing is disabled."
9
+ end
10
+
11
+ @shareable.share_publicly!(user: current_nquery_user)
12
+ redirect_to sharing_page_path, notice: "Public link created."
13
+ end
14
+
15
+ def destroy
16
+ @shareable.unshare_publicly!
17
+ redirect_to sharing_page_path, notice: "Public link removed."
18
+ end
19
+ end
20
+ end
21
+ end
@@ -18,6 +18,10 @@ module Nquery
18
18
  dashboard ? embed_dashboard_chart_path(dashboard, chart) : embed_chart_path(chart)
19
19
  end
20
20
 
21
+ def dashboard_embed_path(dashboard)
22
+ embed_dashboard_path(dashboard)
23
+ end
24
+
21
25
  def chart_archive_path(chart, dashboard: chart_dashboard)
22
26
  dashboard ? archive_dashboard_chart_path(dashboard, chart) : archive_chart_path(chart)
23
27
  end
@@ -29,5 +33,14 @@ module Nquery
29
33
  def chart_builder_chart_types
30
34
  Chart::CHART_TYPES - %w[table]
31
35
  end
36
+
37
+ def chart_builder_data(chart, result: nil)
38
+ {
39
+ controller: "chart-builder",
40
+ query_save_url: chart.query&.persisted? ? query_path(chart.query) : nil,
41
+ query_run_url: run_queries_path,
42
+ query_schema_url: schema_queries_path
43
+ }.tap { |data| data[:initial_result] = result if result.present? }
44
+ end
32
45
  end
33
46
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module SharingHelper
5
+ EMBED_EXPIRY_OPTIONS = [
6
+ ["1 hour", "1.hour"],
7
+ ["1 day", "1.day"],
8
+ ["30 days", "30.days"],
9
+ ["Never", "never"]
10
+ ].freeze
11
+
12
+ def shareable_public_link_path(resource)
13
+ case resource
14
+ when Chart
15
+ chart_dashboard ? dashboard_chart_public_link_path(chart_dashboard, resource) : chart_public_link_path(resource)
16
+ when Dashboard
17
+ dashboard_public_link_path(resource)
18
+ end
19
+ end
20
+
21
+ def shareable_embedding_path(resource)
22
+ case resource
23
+ when Chart
24
+ chart_dashboard ? dashboard_chart_embedding_path(chart_dashboard, resource) : chart_embedding_path(resource)
25
+ when Dashboard
26
+ dashboard_embedding_path(resource)
27
+ end
28
+ end
29
+
30
+ def shareable_embed_tokens_path(resource)
31
+ case resource
32
+ when Chart
33
+ chart_dashboard ? dashboard_chart_embed_tokens_path(chart_dashboard, resource) : chart_embed_tokens_path(resource)
34
+ when Dashboard
35
+ dashboard_embed_tokens_path(resource)
36
+ end
37
+ end
38
+
39
+ def shareable_embed_token_path(resource, token)
40
+ case resource
41
+ when Chart
42
+ if chart_dashboard
43
+ dashboard_chart_embed_token_path(chart_dashboard, resource, token)
44
+ else
45
+ chart_embed_token_path(resource, token)
46
+ end
47
+ when Dashboard
48
+ dashboard_embed_token_path(resource, token)
49
+ end
50
+ end
51
+
52
+ def can_curate_shareable?(resource)
53
+ return true if permission_resolver.admin?
54
+ return false if resource.collection.nil?
55
+
56
+ permission_resolver.collection_access(resource.collection) == :curate
57
+ end
58
+
59
+ def share_iframe_snippet(url, height: 600)
60
+ %(<iframe src="#{url}" width="800" height="#{height}" frameborder="0" loading="lazy"></iframe>)
61
+ end
62
+
63
+ def share_iframe_height(resource)
64
+ resource.is_a?(Dashboard) ? 800 : 600
65
+ end
66
+ end
67
+ end
@@ -1,5 +1,13 @@
1
1
  import { Controller } from "@hotwired/stimulus"
2
2
 
3
+ function escapeHtml(value) {
4
+ return String(value)
5
+ .replace(/&/g, "&amp;")
6
+ .replace(/</g, "&lt;")
7
+ .replace(/>/g, "&gt;")
8
+ .replace(/"/g, "&quot;")
9
+ }
10
+
3
11
  function columnIndex(columns, name) {
4
12
  const index = columns.indexOf(name)
5
13
  return index >= 0 ? index : 0
@@ -72,10 +80,20 @@ export default class extends Controller {
72
80
  connect() {
73
81
  if (typeof Chart === "undefined") return
74
82
 
83
+ Chart.defaults.font.family = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
84
+ Chart.defaults.color = document.body.classList.contains("nq-embed-theme-night") ? "#e2e8f0" : "#1e293b"
85
+
75
86
  const demo = this.hasDataValue ? this.dataValue : {
76
87
  columns: ["month", "revenue"],
77
88
  rows: [["Jan", 1200], ["Feb", 1800], ["Mar", 2400]]
78
89
  }
90
+
91
+ if (demo.error) {
92
+ this.element.classList.add("is-error")
93
+ this.element.innerHTML = `<div class="nq-chart-error">${escapeHtml(demo.error)}</div>`
94
+ return
95
+ }
96
+
79
97
  const type = this.typeValue || "bar"
80
98
  const xCol = this.hasXValue ? this.xValue : ""
81
99
  const yCol = this.hasYValue ? this.yValue : ""
@@ -84,7 +102,7 @@ export default class extends Controller {
84
102
  const yIndex = columnIndex(demo.columns || [], yCol || demo.columns?.[1] || demo.columns?.[0])
85
103
  const value = demo.rows?.[0]?.[yIndex] ?? "—"
86
104
  this.element.classList.add("is-number")
87
- this.element.innerHTML = `<div class="nq-number-display">${value}</div>`
105
+ this.element.innerHTML = `<div class="nq-number-display">${escapeHtml(value)}</div>`
88
106
  return
89
107
  }
90
108
 
@@ -0,0 +1,23 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static values = { text: String }
5
+
6
+ async copy(event) {
7
+ event.preventDefault()
8
+ const button = event.currentTarget
9
+ const targetSelector = button.dataset.copyTarget
10
+ const target = targetSelector ? document.querySelector(targetSelector) : null
11
+ const text = target ? target.textContent : this.textValue
12
+ const original = button.textContent
13
+
14
+ try {
15
+ await navigator.clipboard.writeText(text.trim())
16
+ button.textContent = "Copied"
17
+ } catch {
18
+ button.textContent = "Copy failed"
19
+ }
20
+
21
+ setTimeout(() => { button.textContent = original }, 1500)
22
+ }
23
+ }
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nquery
4
+ module Shareable
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ resource_class_name = name
9
+
10
+ belongs_to :made_public_by, class_name: "Nquery::User", optional: true
11
+ has_many :embed_tokens, -> { where(resource_type: resource_class_name) },
12
+ class_name: "Nquery::EmbedToken", foreign_key: :resource_id, dependent: :destroy
13
+
14
+ scope :publicly_shared, -> { where.not(public_uuid: nil) }
15
+ scope :embedding_enabled, -> { where(enable_embedding: true) }
16
+
17
+ validates :public_uuid, uniqueness: true, allow_nil: true
18
+ end
19
+
20
+ def publicly_shared?
21
+ public_uuid.present?
22
+ end
23
+
24
+ def share_publicly!(user:)
25
+ return self if publicly_shared?
26
+
27
+ update!(
28
+ public_uuid: SecureRandom.uuid,
29
+ made_public_by: user,
30
+ public_shared_at: Time.current
31
+ )
32
+ self
33
+ end
34
+
35
+ def unshare_publicly!
36
+ update!(public_uuid: nil, made_public_by: nil, public_shared_at: nil)
37
+ end
38
+
39
+ def active_embed_token
40
+ embed_tokens.active.order(created_at: :desc).first
41
+ end
42
+ end
43
+ end
@@ -4,12 +4,12 @@ module Nquery
4
4
  class Chart < ApplicationRecord
5
5
  CHART_TYPES = %w[table number bar line area pie scatter].freeze
6
6
 
7
+ include Shareable
8
+
7
9
  belongs_to :query, class_name: "Nquery::Query", optional: true
8
10
  belongs_to :collection, class_name: "Nquery::Collection", optional: true
9
11
  belongs_to :creator, class_name: "Nquery::User", optional: true
10
12
  has_many :dashboard_cards, class_name: "Nquery::DashboardCard", dependent: :destroy
11
- has_many :embed_tokens, -> { where(resource_type: "Nquery::Chart") },
12
- class_name: "Nquery::EmbedToken", foreign_key: :resource_id, dependent: :destroy
13
13
 
14
14
  accepts_nested_attributes_for :query
15
15
 
@@ -33,5 +33,17 @@ module Nquery
33
33
  def chart_type
34
34
  visualization["type"] || "bar"
35
35
  end
36
+
37
+ def statement
38
+ query&.statement
39
+ end
40
+
41
+ def statement?
42
+ query&.statement? || false
43
+ end
44
+
45
+ def data_source
46
+ query&.data_source
47
+ end
36
48
  end
37
49
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  module Nquery
4
4
  class Dashboard < ApplicationRecord
5
+ include Shareable
6
+
5
7
  belongs_to :collection, class_name: "Nquery::Collection"
6
8
  belongs_to :creator, class_name: "Nquery::User", optional: true
7
9
  has_many :dashboard_cards, class_name: "Nquery::DashboardCard", dependent: :destroy
@@ -42,7 +42,7 @@ module Nquery
42
42
  scope :active, -> { all }
43
43
 
44
44
  def connection_fields_submitted?
45
- ActiveModel::Type::Boolean.new.cast(connection_fields_submitted)
45
+ Nquery::Boolean.cast(connection_fields_submitted)
46
46
  end
47
47
 
48
48
  def connection_config_hash
@@ -17,6 +17,10 @@ module Nquery
17
17
  self.name ||= "Untitled query"
18
18
  end
19
19
 
20
+ def statement?
21
+ statement.present?
22
+ end
23
+
20
24
  private
21
25
 
22
26
  def statement_must_be_readonly
@@ -3,10 +3,12 @@
3
3
  <head>
4
4
  <title>nquery embed</title>
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="robots" content="noindex">
6
7
  <%= stylesheet_link_tag "nquery/application" %>
7
8
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
9
+ <%= javascript_include_tag "nquery/application", defer: true %>
8
10
  </head>
9
- <body class="nq-embed-body">
11
+ <body class="<%= embed_appearance_classes.join(" ") %>">
10
12
  <%= yield %>
11
13
  </body>
12
14
  </html>
@@ -1,13 +1,8 @@
1
1
  <% viz = chart.visualization.is_a?(Hash) ? chart.visualization : {} %>
2
2
  <% chart_type = viz["type"].presence || "table" %>
3
3
  <% active_chart_type = chart_builder_chart_types.include?(chart_type) ? chart_type : "bar" %>
4
- <% builder_data = {
5
- controller: "chart-builder",
6
- query_save_url: chart.query&.persisted? ? query_path(chart.query) : nil
7
- } %>
8
- <% builder_data[:initial_result] = result if local_assigns[:result].present? %>
9
4
  <%= form_with model: chart, url: form_url, class: "nq-form nq-chart-builder-form", html: {
10
- data: builder_data
5
+ data: chart_builder_data(chart, result: result)
11
6
  } do |f| %>
12
7
  <%= render "nquery/charts/new_header",
13
8
  f: f,
@@ -2,14 +2,8 @@
2
2
  <h1>Embed: <%= @chart.name %></h1>
3
3
  </div>
4
4
 
5
- <div class="nq-card">
6
- <h2>Embed settings</h2>
7
- <% if @embed_token %>
8
- <p class="nq-muted">Signed embed token (expires <%= @embed_token.expires_at&.to_fs(:long) || "never" %>)</p>
9
- <pre class="nq-code"><%= @embed_url %></pre>
10
- <h3>Iframe snippet</h3>
11
- <pre class="nq-code">&lt;iframe src="<%= @embed_url %>" width="800" height="600" frameborder="0"&gt;&lt;/iframe&gt;</pre>
12
- <% else %>
13
- <p class="nq-muted">No embed token yet. Generate one from the admin panel (phase 1 UI placeholder).</p>
14
- <% end %>
15
- </div>
5
+ <%= render "nquery/shared/sharing_panel",
6
+ shareable: @shareable,
7
+ public_url: @public_url,
8
+ embed_url: @embed_url,
9
+ embed_token: @embed_token %>
@@ -2,4 +2,5 @@
2
2
  chart: @chart,
3
3
  form_url: charts_path,
4
4
  data_sources: @data_sources,
5
- schema: @schema %>
5
+ schema: @schema,
6
+ result: nil %>
@@ -3,4 +3,5 @@
3
3
  dashboard: @dashboard,
4
4
  form_url: dashboard_charts_path(@dashboard),
5
5
  data_sources: @data_sources,
6
- schema: @schema %>
6
+ schema: @schema,
7
+ result: nil %>
@@ -0,0 +1,9 @@
1
+ <div class="nq-page-header">
2
+ <h1>Embed: <%= @dashboard.name %></h1>
3
+ </div>
4
+
5
+ <%= render "nquery/shared/sharing_panel",
6
+ shareable: @shareable,
7
+ public_url: @public_url,
8
+ embed_url: @embed_url,
9
+ embed_token: @embed_token %>