nquery 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +52 -0
  4. data/Rakefile +9 -0
  5. data/app/assets/builds/nquery/application.css +339 -0
  6. data/app/assets/builds/nquery/application.js +989 -0
  7. data/app/assets/config/nquery_manifest.js +2 -0
  8. data/app/components/nquery/_card_component.html.erb +6 -0
  9. data/app/components/nquery/_card_table_component.html.erb +14 -0
  10. data/app/components/nquery/_flash_card_component.html.erb +32 -0
  11. data/app/components/nquery/_flash_cards_component.html.erb +3 -0
  12. data/app/components/nquery/_infinite_scroll_component.html.erb +11 -0
  13. data/app/components/nquery/card_component.rb +18 -0
  14. data/app/components/nquery/card_table_component.rb +19 -0
  15. data/app/components/nquery/component.rb +26 -0
  16. data/app/components/nquery/flash_card_component.rb +51 -0
  17. data/app/components/nquery/flash_cards_component.rb +20 -0
  18. data/app/components/nquery/infinite_scroll_component.rb +19 -0
  19. data/app/controllers/concerns/nquery/breadcrumbs.rb +238 -0
  20. data/app/controllers/concerns/nquery/browsable.rb +56 -0
  21. data/app/controllers/concerns/nquery/chart_actions.rb +104 -0
  22. data/app/controllers/concerns/nquery/chart_results.rb +30 -0
  23. data/app/controllers/nquery/admin/base_controller.rb +9 -0
  24. data/app/controllers/nquery/admin/data_sources_controller.rb +47 -0
  25. data/app/controllers/nquery/admin/groups_controller.rb +63 -0
  26. data/app/controllers/nquery/admin/logs_controller.rb +28 -0
  27. data/app/controllers/nquery/admin/permissions_controller.rb +53 -0
  28. data/app/controllers/nquery/admin/users_controller.rb +80 -0
  29. data/app/controllers/nquery/application_controller.rb +70 -0
  30. data/app/controllers/nquery/charts_controller.rb +51 -0
  31. data/app/controllers/nquery/collection/dashboards_controller.rb +38 -0
  32. data/app/controllers/nquery/collections_controller.rb +117 -0
  33. data/app/controllers/nquery/dashboard/charts_controller.rb +62 -0
  34. data/app/controllers/nquery/dashboards_controller.rb +90 -0
  35. data/app/controllers/nquery/embed/charts_controller.rb +24 -0
  36. data/app/controllers/nquery/embed/dashboards_controller.rb +17 -0
  37. data/app/controllers/nquery/home_controller.rb +16 -0
  38. data/app/controllers/nquery/imports_controller.rb +41 -0
  39. data/app/controllers/nquery/queries_controller.rb +105 -0
  40. data/app/controllers/nquery/registrations_controller.rb +31 -0
  41. data/app/controllers/nquery/sessions_controller.rb +28 -0
  42. data/app/helpers/nquery/charts_helper.rb +33 -0
  43. data/app/helpers/nquery/collections_helper.rb +13 -0
  44. data/app/helpers/nquery/components_helper.rb +29 -0
  45. data/app/helpers/nquery/home_helper.rb +23 -0
  46. data/app/helpers/nquery/icon_helper.rb +9 -0
  47. data/app/helpers/nquery/navigation_helper.rb +10 -0
  48. data/app/javascript/nquery/controllers/chart_preview_controller.js +125 -0
  49. data/app/javascript/nquery/controllers/csv_mapper_controller.js +23 -0
  50. data/app/javascript/nquery/controllers/dashboard_grid_controller.js +25 -0
  51. data/app/javascript/nquery/controllers/query_editor_controller.js +41 -0
  52. data/app/models/nquery/application_permission.rb +12 -0
  53. data/app/models/nquery/application_record.rb +7 -0
  54. data/app/models/nquery/audit.rb +38 -0
  55. data/app/models/nquery/chart.rb +37 -0
  56. data/app/models/nquery/collection.rb +35 -0
  57. data/app/models/nquery/collection_permission.rb +12 -0
  58. data/app/models/nquery/csv_upload.rb +11 -0
  59. data/app/models/nquery/dashboard.rb +27 -0
  60. data/app/models/nquery/dashboard_card.rb +8 -0
  61. data/app/models/nquery/data_permission.rb +13 -0
  62. data/app/models/nquery/data_source.rb +27 -0
  63. data/app/models/nquery/embed_token.rb +21 -0
  64. data/app/models/nquery/group.rb +27 -0
  65. data/app/models/nquery/group_membership.rb +10 -0
  66. data/app/models/nquery/query.rb +19 -0
  67. data/app/models/nquery/user.rb +58 -0
  68. data/app/views/layouts/nquery/application.html.erb +32 -0
  69. data/app/views/layouts/nquery/auth.html.erb +16 -0
  70. data/app/views/layouts/nquery/embed.html.erb +12 -0
  71. data/app/views/nquery/admin/data_sources/edit.html.erb +8 -0
  72. data/app/views/nquery/admin/data_sources/index.html.erb +21 -0
  73. data/app/views/nquery/admin/data_sources/new.html.erb +7 -0
  74. data/app/views/nquery/admin/groups/edit.html.erb +6 -0
  75. data/app/views/nquery/admin/groups/index.html.erb +21 -0
  76. data/app/views/nquery/admin/groups/new.html.erb +6 -0
  77. data/app/views/nquery/admin/groups/show.html.erb +24 -0
  78. data/app/views/nquery/admin/logs/index.html.erb +74 -0
  79. data/app/views/nquery/admin/permissions/_collection.html.erb +25 -0
  80. data/app/views/nquery/admin/permissions/_data_source.html.erb +27 -0
  81. data/app/views/nquery/admin/permissions/_group.html.erb +42 -0
  82. data/app/views/nquery/admin/permissions/by_collection.html.erb +10 -0
  83. data/app/views/nquery/admin/permissions/by_data_source.html.erb +7 -0
  84. data/app/views/nquery/admin/permissions/by_group.html.erb +12 -0
  85. data/app/views/nquery/admin/users/edit.html.erb +17 -0
  86. data/app/views/nquery/admin/users/index.html.erb +34 -0
  87. data/app/views/nquery/admin/users/new.html.erb +14 -0
  88. data/app/views/nquery/admin/users/show.html.erb +6 -0
  89. data/app/views/nquery/charts/_builder_form.html.erb +185 -0
  90. data/app/views/nquery/charts/_new_header.html.erb +31 -0
  91. data/app/views/nquery/charts/_schema_tree.html.erb +25 -0
  92. data/app/views/nquery/charts/edit.html.erb +6 -0
  93. data/app/views/nquery/charts/embed.html.erb +15 -0
  94. data/app/views/nquery/charts/new.html.erb +5 -0
  95. data/app/views/nquery/charts/show.html.erb +15 -0
  96. data/app/views/nquery/charts/update.turbo_stream.erb +1 -0
  97. data/app/views/nquery/collection/dashboards/new.html.erb +13 -0
  98. data/app/views/nquery/collections/_dashboard_card.html.erb +57 -0
  99. data/app/views/nquery/collections/edit.html.erb +15 -0
  100. data/app/views/nquery/collections/index.html.erb +41 -0
  101. data/app/views/nquery/collections/new.html.erb +15 -0
  102. data/app/views/nquery/collections/show.html.erb +100 -0
  103. data/app/views/nquery/dashboard/charts/new.html.erb +6 -0
  104. data/app/views/nquery/dashboards/_chart_card.html.erb +16 -0
  105. data/app/views/nquery/dashboards/edit.html.erb +34 -0
  106. data/app/views/nquery/dashboards/index.html.erb +35 -0
  107. data/app/views/nquery/dashboards/show.html.erb +46 -0
  108. data/app/views/nquery/embed/charts/show.html.erb +9 -0
  109. data/app/views/nquery/embed/dashboards/show.html.erb +7 -0
  110. data/app/views/nquery/home/index.html.erb +143 -0
  111. data/app/views/nquery/imports/new.html.erb +20 -0
  112. data/app/views/nquery/queries/edit.html.erb +40 -0
  113. data/app/views/nquery/queries/new.html.erb +37 -0
  114. data/app/views/nquery/registrations/new.html.erb +34 -0
  115. data/app/views/nquery/sessions/new.html.erb +29 -0
  116. data/app/views/nquery/shared/_auth_shell.html.erb +22 -0
  117. data/app/views/nquery/shared/_breadcrumbs.html.erb +15 -0
  118. data/app/views/nquery/shared/_chart_actions_menu.html.erb +27 -0
  119. data/app/views/nquery/shared/_chart_row_actions.html.erb +16 -0
  120. data/app/views/nquery/shared/_flash.html.erb +1 -0
  121. data/app/views/nquery/shared/_icon.html.erb +50 -0
  122. data/app/views/nquery/shared/_sidebar.html.erb +16 -0
  123. data/app/views/nquery/shared/_topbar.html.erb +12 -0
  124. data/config/importmap.rb +9 -0
  125. data/config/routes.rb +78 -0
  126. data/db/migrate/20260721000001_create_nquery_tables.rb +146 -0
  127. data/db/migrate/20260721180000_add_archived_at_to_nquery_dashboards.rb +8 -0
  128. data/db/migrate/20260721190000_add_archived_at_to_nquery_charts.rb +8 -0
  129. data/db/migrate/20260721200000_add_archived_at_to_nquery_collections.rb +8 -0
  130. data/lib/generators/nquery/install_generator.rb +38 -0
  131. data/lib/nquery/authorizes_collection.rb +50 -0
  132. data/lib/nquery/configuration.rb +44 -0
  133. data/lib/nquery/csv_importer.rb +68 -0
  134. data/lib/nquery/data_sources/adapter.rb +34 -0
  135. data/lib/nquery/data_sources/mysql_adapter.rb +24 -0
  136. data/lib/nquery/data_sources/postgresql_adapter.rb +46 -0
  137. data/lib/nquery/data_sources/rails_adapter.rb +54 -0
  138. data/lib/nquery/data_sources/sqlite_adapter.rb +8 -0
  139. data/lib/nquery/embed_token_service.rb +83 -0
  140. data/lib/nquery/engine.rb +55 -0
  141. data/lib/nquery/permissions/resolver.rb +83 -0
  142. data/lib/nquery/query_runner.rb +69 -0
  143. data/lib/nquery/sample_data/ecommerce.rb +157 -0
  144. data/lib/nquery/schema_explorer.rb +28 -0
  145. data/lib/nquery/seeder.rb +131 -0
  146. data/lib/nquery/version.rb +5 -0
  147. data/lib/nquery.rb +36 -0
  148. data/lib/tasks/nquery.rake +9 -0
  149. data/nquery.gemspec +40 -0
  150. metadata +395 -0
@@ -0,0 +1,42 @@
1
+ <div class="nq-card nq-mt">
2
+ <form method="get" class="nq-form-row">
3
+ <label>Group</label>
4
+ <select name="group_id" class="nq-input" onchange="this.form.submit()">
5
+ <% @groups.each do |group| %>
6
+ <option value="<%= group.id %>" <%= "selected" if group == @selected_group %>><%= group.name %></option>
7
+ <% end %>
8
+ </select>
9
+ </form>
10
+
11
+ <% if @selected_group %>
12
+ <h2>Collection permissions — <%= @selected_group.name %></h2>
13
+ <table class="nq-table">
14
+ <thead><tr><th>Collection</th><th>Access</th></tr></thead>
15
+ <tbody>
16
+ <% @collections.each do |collection| %>
17
+ <% perm = Nquery::CollectionPermission.find_by(group: @selected_group, collection: collection) %>
18
+ <tr>
19
+ <td><%= collection.name %></td>
20
+ <td><span class="nq-badge"><%= perm&.access_level || "no_access" %></span></td>
21
+ </tr>
22
+ <% end %>
23
+ </tbody>
24
+ </table>
25
+
26
+ <h2 class="nq-mt">Data permissions</h2>
27
+ <table class="nq-table">
28
+ <thead><tr><th>Data source</th><th>View data</th><th>Create queries</th></tr></thead>
29
+ <tbody>
30
+ <% @data_sources.each do |ds| %>
31
+ <% view = Nquery::DataPermission.find_by(group: @selected_group, data_source: ds, permission_type: "view_data") %>
32
+ <% create = Nquery::DataPermission.find_by(group: @selected_group, data_source: ds, permission_type: "create_queries") %>
33
+ <tr>
34
+ <td><%= ds.name %></td>
35
+ <td><span class="nq-badge"><%= view&.access_level || "blocked" %></span></td>
36
+ <td><span class="nq-badge"><%= create&.access_level || "no" %></span></td>
37
+ </tr>
38
+ <% end %>
39
+ </tbody>
40
+ </table>
41
+ <% end %>
42
+ </div>
@@ -0,0 +1,10 @@
1
+ <div class="nq-page-header"><h1>Permissions</h1></div>
2
+ <div class="nq-tabs">
3
+ <%= link_to "By group", by_group_admin_permissions_path, class: "nq-tab" %>
4
+ <%= link_to "By data source", by_data_source_admin_permissions_path, class: "nq-tab" %>
5
+ <%= link_to "By collection", by_collection_admin_permissions_path, class: "nq-tab active" %>
6
+ </div>
7
+ <% if @warnings&.any? %>
8
+ <%= nq_flash_card nil, messages: @warnings, type: :warning, toast: false, auto_dismiss: false %>
9
+ <% end %>
10
+ <%= render "nquery/admin/permissions/collection" %>
@@ -0,0 +1,7 @@
1
+ <div class="nq-page-header"><h1>Permissions</h1></div>
2
+ <div class="nq-tabs">
3
+ <%= link_to "By group", by_group_admin_permissions_path, class: "nq-tab" %>
4
+ <%= link_to "By data source", by_data_source_admin_permissions_path, class: "nq-tab active" %>
5
+ <%= link_to "By collection", by_collection_admin_permissions_path, class: "nq-tab" %>
6
+ </div>
7
+ <%= render "nquery/admin/permissions/data_source" %>
@@ -0,0 +1,12 @@
1
+ <div class="nq-page-header"><h1>Permissions</h1></div>
2
+ <div class="nq-tabs">
3
+ <%= link_to "By group", by_group_admin_permissions_path, class: "nq-tab #{'active' if @view == 'group'}" %>
4
+ <%= link_to "By data source", by_data_source_admin_permissions_path, class: "nq-tab #{'active' if @view == 'data_source'}" %>
5
+ <%= link_to "By collection", by_collection_admin_permissions_path, class: "nq-tab #{'active' if @view == 'collection'}" %>
6
+ </div>
7
+
8
+ <% if @warnings&.any? %>
9
+ <%= nq_flash_card nil, messages: @warnings, type: :warning, toast: false, auto_dismiss: false %>
10
+ <% end %>
11
+
12
+ <%= render "nquery/admin/permissions/#{@view}" %>
@@ -0,0 +1,17 @@
1
+ <div class="nq-page-header"><h1>Edit user</h1></div>
2
+ <%= form_with model: @user, url: admin_user_path(@user), method: :patch, class: "nq-form nq-card" do |f| %>
3
+ <div class="nq-form-row"><%= f.label :email %><%= f.email_field :email, class: "nq-input" %></div>
4
+ <div class="nq-form-row"><%= f.label :first_name %><%= f.text_field :first_name, class: "nq-input" %></div>
5
+ <div class="nq-form-row"><%= f.label :last_name %><%= f.text_field :last_name, class: "nq-input" %></div>
6
+ <div class="nq-form-row"><%= f.label :password, "New password (optional)" %><%= f.password_field :password, class: "nq-input" %></div>
7
+ <div class="nq-form-row">
8
+ <label>Groups</label>
9
+ <% @groups.each do |group| %>
10
+ <label class="nq-checkbox">
11
+ <input type="checkbox" name="group_ids[]" value="<%= group.id %>" <%= "checked" if @user.groups.include?(group) %>>
12
+ <%= group.name %>
13
+ </label>
14
+ <% end %>
15
+ </div>
16
+ <%= f.submit "Update", class: "nq-btn nq-btn-primary" %>
17
+ <% end %>
@@ -0,0 +1,34 @@
1
+ <div class="nq-page-header"><h1>Users</h1><%= link_to "Invite user", new_admin_user_path, class: "nq-btn nq-btn-primary" %></div>
2
+
3
+ <%= nq_card_table columns: ["Email", "Name", "Groups", ""] do %>
4
+ <% @users.each do |user| %>
5
+ <tr>
6
+ <td><%= user.email %></td>
7
+ <td><%= user.name %></td>
8
+ <td>
9
+ <div class="nq-tags">
10
+ <% user.groups.each do |group| %>
11
+ <span class="nq-badge"><%= group.name %></span>
12
+ <% end %>
13
+ </div>
14
+ </td>
15
+ <td>
16
+ <div class="nq-actions">
17
+ <%= link_to "Edit", edit_admin_user_path(user), class: "nq-btn" %>
18
+ <% unless user == current_nquery_user %>
19
+ <%= button_to "Disable",
20
+ deactivate_admin_user_path(user),
21
+ method: :patch,
22
+ class: "nq-btn",
23
+ form: { data: { turbo_confirm: "Disable #{user.name}?" } } %>
24
+ <%= button_to "Remove",
25
+ admin_user_path(user),
26
+ method: :delete,
27
+ class: "nq-btn nq-btn-danger",
28
+ form: { data: { turbo_confirm: "Remove #{user.name}? This cannot be undone." } } %>
29
+ <% end %>
30
+ </div>
31
+ </td>
32
+ </tr>
33
+ <% end %>
34
+ <% end %>
@@ -0,0 +1,14 @@
1
+ <div class="nq-page-header"><h1>Invite user</h1></div>
2
+ <%= form_with model: @user, url: admin_users_path, class: "nq-form nq-card" do |f| %>
3
+ <div class="nq-form-row"><%= f.label :email %><%= f.email_field :email, class: "nq-input" %></div>
4
+ <div class="nq-form-row"><%= f.label :first_name %><%= f.text_field :first_name, class: "nq-input" %></div>
5
+ <div class="nq-form-row"><%= f.label :last_name %><%= f.text_field :last_name, class: "nq-input" %></div>
6
+ <div class="nq-form-row"><%= f.label :password %><%= f.password_field :password, class: "nq-input" %></div>
7
+ <div class="nq-form-row">
8
+ <label>Groups</label>
9
+ <% @groups.each do |group| %>
10
+ <label class="nq-checkbox"><input type="checkbox" name="group_ids[]" value="<%= group.id %>"> <%= group.name %></label>
11
+ <% end %>
12
+ </div>
13
+ <%= f.submit "Create user", class: "nq-btn nq-btn-primary" %>
14
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <div class="nq-page-header">
2
+ <h1><%= @user.name %></h1>
3
+ <p class="nq-muted"><%= @user.email %></p>
4
+ </div>
5
+
6
+ <%= link_to "Edit", edit_admin_user_path(@user), class: "nq-btn" %>
@@ -0,0 +1,185 @@
1
+ <% viz = chart.visualization.is_a?(Hash) ? chart.visualization : {} %>
2
+ <% chart_type = viz["type"].presence || "table" %>
3
+ <% active_chart_type = chart_builder_chart_types.include?(chart_type) ? chart_type : "bar" %>
4
+ <%= form_with model: chart, url: form_url, class: "nq-form nq-chart-builder-form", html: {
5
+ data: {
6
+ controller: "chart-builder",
7
+ query_save_url: chart.query&.persisted? ? query_path(chart.query) : nil
8
+ }
9
+ } do |f| %>
10
+ <%= render "nquery/charts/new_header",
11
+ f: f,
12
+ chart: chart,
13
+ dashboard: local_assigns[:dashboard] %>
14
+
15
+ <div class="nq-chart-builder">
16
+ <div class="nq-chart-builder-layout">
17
+ <aside class="nq-card nq-chart-builder-panel nq-chart-builder-schema">
18
+ <h3 class="nq-panel-title">Schema</h3>
19
+ <%= render "nquery/charts/schema_tree", schema: schema %>
20
+ </aside>
21
+
22
+ <section class="nq-card nq-chart-builder-panel nq-chart-builder-workspace">
23
+ <input type="hidden" name="chart[visualization][type]" value="<%= chart_type %>" data-chart-builder-target="typeField">
24
+ <input type="hidden" name="chart[visualization][x]" value="<%= viz["x"] %>" data-chart-builder-target="xField">
25
+ <input type="hidden" name="chart[visualization][y]" value="<%= viz["y"] %>" data-chart-builder-target="yField">
26
+
27
+ <header class="nq-workspace-header">
28
+ <div class="nq-workspace-tabs" role="tablist" aria-label="Chart builder views">
29
+ <button type="button"
30
+ role="tab"
31
+ aria-selected="true"
32
+ aria-controls="chart-builder-query-panel"
33
+ class="nq-workspace-tab is-active"
34
+ data-chart-builder-target="workspaceTab"
35
+ data-tab="query">
36
+ Query
37
+ </button>
38
+ <button type="button"
39
+ role="tab"
40
+ aria-selected="false"
41
+ aria-controls="chart-builder-output-panel"
42
+ class="nq-workspace-tab"
43
+ data-chart-builder-target="workspaceTab"
44
+ data-tab="output">
45
+ Output
46
+ </button>
47
+ </div>
48
+
49
+ <div class="nq-workspace-header-actions">
50
+ <span class="nq-muted nq-query-meta" data-chart-builder-target="meta" hidden></span>
51
+
52
+ <div class="nq-output-tabs" role="tablist" aria-label="Output views" data-chart-builder-target="tabList" hidden>
53
+ <button type="button"
54
+ role="tab"
55
+ aria-selected="true"
56
+ aria-controls="chart-builder-table-panel"
57
+ class="nq-output-tab is-active"
58
+ data-chart-builder-target="outputTab"
59
+ data-tab="table">
60
+ Table
61
+ </button>
62
+ <button type="button"
63
+ role="tab"
64
+ aria-selected="false"
65
+ aria-controls="chart-builder-chart-panel"
66
+ class="nq-output-tab"
67
+ data-chart-builder-target="outputTab"
68
+ data-tab="chart">
69
+ Chart
70
+ </button>
71
+ </div>
72
+ </div>
73
+ </header>
74
+
75
+ <div id="chart-builder-query-panel"
76
+ class="nq-workspace-panel is-active"
77
+ data-chart-builder-target="queryPanel"
78
+ role="tabpanel">
79
+ <div class="nq-panel-header">
80
+ <%= f.fields_for :query do |query_fields| %>
81
+ <%= query_fields.collection_select :data_source_id,
82
+ data_sources,
83
+ :id,
84
+ :name,
85
+ {},
86
+ class: "nq-input nq-input-sm",
87
+ data: {
88
+ chart_builder_target: "dataSource",
89
+ action: "change->chart-builder#loadSchema"
90
+ } %>
91
+ <% end %>
92
+ </div>
93
+
94
+ <%= f.fields_for :query do |query_fields| %>
95
+ <div class="nq-sql-editor-shell">
96
+ <%= query_fields.text_area :statement,
97
+ rows: 16,
98
+ class: "nq-textarea nq-sql-editor",
99
+ placeholder: "SELECT ...",
100
+ spellcheck: false,
101
+ data: { chart_builder_target: "statement" } %>
102
+ </div>
103
+ <%= query_fields.hidden_field :name, data: { chart_builder_target: "queryName" } %>
104
+ <% end %>
105
+
106
+ <div class="nq-chart-builder-query-actions">
107
+ <button type="button"
108
+ class="nq-btn"
109
+ data-manages-loading="true"
110
+ data-action="click->chart-builder#format"
111
+ data-chart-builder-target="formatButton">
112
+ <%= nq_icon(:format) %>
113
+ <span>Format SQL</span>
114
+ </button>
115
+ <button type="button" class="nq-btn nq-btn-primary" data-manages-loading="true" data-action="click->chart-builder#run">
116
+ Run query
117
+ </button>
118
+ <span class="nq-sql-save-status nq-muted" data-chart-builder-target="saveStatus" hidden></span>
119
+ <span class="nq-sql-editor-hint nq-muted">⌘↵ run · ⌘⇧F format</span>
120
+ </div>
121
+ </div>
122
+
123
+ <div id="chart-builder-output-panel"
124
+ class="nq-workspace-panel"
125
+ data-chart-builder-target="workspaceOutputPanel"
126
+ role="tabpanel"
127
+ hidden>
128
+ <div class="nq-output-body" data-chart-builder-target="outputBody">
129
+ <div class="nq-output-empty" data-chart-builder-target="emptyState">
130
+ <p>Run your query to see results.</p>
131
+ <p class="nq-muted">Preview data as a table or chart.</p>
132
+ </div>
133
+
134
+ <div class="nq-output-error" data-chart-builder-target="error" hidden></div>
135
+
136
+ <div id="chart-builder-table-panel"
137
+ class="nq-output-panel"
138
+ data-chart-builder-target="tablePanel"
139
+ role="tabpanel"
140
+ hidden>
141
+ <div class="nq-viz-table-wrap" data-chart-builder-target="table"></div>
142
+ </div>
143
+
144
+ <div id="chart-builder-chart-panel"
145
+ class="nq-output-panel"
146
+ data-chart-builder-target="chartPanel"
147
+ role="tabpanel"
148
+ hidden>
149
+ <div class="nq-chart-settings">
150
+ <div class="nq-viz-types" role="radiogroup" aria-label="Chart type">
151
+ <% chart_builder_chart_types.each do |type| %>
152
+ <button type="button"
153
+ class="nq-viz-type<%= " is-active" if type == active_chart_type %>"
154
+ data-chart-builder-target="typeButton"
155
+ data-type="<%= type %>">
156
+ <%= type.titleize %>
157
+ </button>
158
+ <% end %>
159
+ </div>
160
+
161
+ <div class="nq-viz-mapping" data-chart-builder-target="mapping">
162
+ <label class="nq-viz-mapping-field" data-chart-builder-target="xMapping">
163
+ <span>X axis</span>
164
+ <select class="nq-input" data-chart-builder-target="xAxis"></select>
165
+ </label>
166
+ <label class="nq-viz-mapping-field">
167
+ <span>Y axis</span>
168
+ <select class="nq-input" data-chart-builder-target="yAxis"></select>
169
+ </label>
170
+ </div>
171
+ </div>
172
+
173
+ <div class="nq-viz-chart-preview" data-chart-builder-target="chartPreview">
174
+ <div class="nq-viz-chart-wrap" data-chart-builder-target="chartWrap" hidden>
175
+ <canvas data-chart-builder-target="canvas" height="180"></canvas>
176
+ </div>
177
+ <div class="nq-viz-number" data-chart-builder-target="number" hidden></div>
178
+ </div>
179
+ </div>
180
+ </div>
181
+ </div>
182
+ </section>
183
+ </div>
184
+ </div>
185
+ <% end %>
@@ -0,0 +1,31 @@
1
+ <% chart = local_assigns[:chart] %>
2
+ <% f = local_assigns.fetch(:f) %>
3
+
4
+ <header class="nq-page-header nq-chart-builder-header">
5
+ <div class="nq-chart-builder-header-main">
6
+ <% unless chart.persisted? %>
7
+ <p class="nq-chart-builder-eyebrow">New chart</p>
8
+ <% end %>
9
+ <div class="nq-chart-builder-name">
10
+ <%= f.text_field :name,
11
+ placeholder: "Chart name",
12
+ class: "nq-input",
13
+ aria: { label: "Chart name" },
14
+ data: { chart_builder_target: "name", action: "input->chart-builder#syncName" } %>
15
+ </div>
16
+ <% if chart.persisted? %>
17
+ <% if local_assigns[:dashboard] %>
18
+ <p class="nq-muted nq-resource-meta">On <%= dashboard.name %>. Update the query and visualization below.</p>
19
+ <% else %>
20
+ <p class="nq-muted nq-resource-meta">Update the query, preview results, then adjust the visualization.</p>
21
+ <% end %>
22
+ <% elsif local_assigns[:dashboard] %>
23
+ <p class="nq-muted nq-resource-meta">Adding to <%= dashboard.name %>. Run your query, preview results, then choose a visualization.</p>
24
+ <% else %>
25
+ <p class="nq-muted nq-resource-meta">Write a query, preview the results, then pick how to visualize them.</p>
26
+ <% end %>
27
+ </div>
28
+ <div class="nq-actions">
29
+ <%= f.submit "Save chart", class: "nq-btn nq-btn-primary" %>
30
+ </div>
31
+ </header>
@@ -0,0 +1,25 @@
1
+ <ul class="nq-tree nq-schema-tree" data-chart-builder-target="schema">
2
+ <% schema.each do |table| %>
3
+ <li class="nq-schema-table">
4
+ <div class="nq-schema-table-header">
5
+ <button type="button"
6
+ class="nq-schema-toggle"
7
+ aria-expanded="false"
8
+ aria-label="Toggle <%= table[:name] %> columns">
9
+ <svg class="nq-schema-toggle-icon" width="12" height="12" viewBox="0 0 12 12" aria-hidden="true">
10
+ <path d="M4 2l4 4-4 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
11
+ </svg>
12
+ </button>
13
+ <span class="nq-schema-table-name"><%= table[:name] %></span>
14
+ </div>
15
+ <ul class="nq-schema-columns" hidden>
16
+ <% table[:columns].each do |column| %>
17
+ <li class="nq-schema-column">
18
+ <span class="nq-schema-column-name"><%= column[:name] %></span>
19
+ <span class="nq-schema-column-type"><%= column[:type] %></span>
20
+ </li>
21
+ <% end %>
22
+ </ul>
23
+ </li>
24
+ <% end %>
25
+ </ul>
@@ -0,0 +1,6 @@
1
+ <%= render "nquery/charts/builder_form",
2
+ chart: @chart,
3
+ dashboard: @dashboard,
4
+ form_url: chart_form_path(@chart),
5
+ data_sources: @data_sources,
6
+ schema: @schema %>
@@ -0,0 +1,15 @@
1
+ <div class="nq-page-header">
2
+ <h1>Embed: <%= @chart.name %></h1>
3
+ </div>
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>
@@ -0,0 +1,5 @@
1
+ <%= render "nquery/charts/builder_form",
2
+ chart: @chart,
3
+ form_url: charts_path,
4
+ data_sources: @data_sources,
5
+ schema: @schema %>
@@ -0,0 +1,15 @@
1
+ <div class="nq-page-header">
2
+ <h1><%= @chart.name %></h1>
3
+ <div class="nq-actions">
4
+ <%= link_to "Edit", chart_edit_path(@chart), class: "nq-btn" %>
5
+ <%= link_to "Embed", chart_embed_path(@chart), class: "nq-btn" %>
6
+ </div>
7
+ </div>
8
+
9
+ <div class="nq-card nq-chart-preview" data-controller="chart-preview"
10
+ data-chart-preview-type-value="<%= @chart.chart_type %>"
11
+ data-chart-preview-x-value="<%= @chart.visualization.to_h["x"] %>"
12
+ data-chart-preview-y-value="<%= @chart.visualization.to_h["y"] %>"
13
+ data-chart-preview-data-value="<%= @result.to_json %>">
14
+ <canvas data-chart-preview-target="canvas" height="120"></canvas>
15
+ </div>
@@ -0,0 +1 @@
1
+ <%= turbo_stream.update "flash", partial: "nquery/shared/flash" %>
@@ -0,0 +1,13 @@
1
+ <div class="nq-page-header"><h1>New dashboard</h1></div>
2
+
3
+ <%= form_with model: @dashboard, url: collection_dashboards_path(@collection), class: "nq-form nq-card" do |f| %>
4
+ <div class="nq-form-row">
5
+ <%= f.label :name %>
6
+ <%= f.text_field :name, class: "nq-input" %>
7
+ </div>
8
+ <div class="nq-form-row">
9
+ <%= f.label :description %>
10
+ <%= f.text_area :description, class: "nq-textarea", rows: 2 %>
11
+ </div>
12
+ <%= f.submit "Create", class: "nq-btn nq-btn-primary" %>
13
+ <% end %>
@@ -0,0 +1,57 @@
1
+ <article class="nq-dashboard-summary-card">
2
+ <header class="nq-dashboard-summary-header">
3
+ <div class="nq-dashboard-summary-heading">
4
+ <%= link_to dashboard.name, dashboard_path(dashboard), class: "nq-dashboard-summary-title" %>
5
+ <p class="nq-muted nq-resource-meta">
6
+ Created by <%= dashboard.creator&.name || "—" %>
7
+ · Created <%= dashboard.created_at.to_fs(:long) %>
8
+ · Updated <%= dashboard.updated_at.to_fs(:long) %>
9
+ </p>
10
+ </div>
11
+ <details class="nq-menu">
12
+ <summary class="nq-btn nq-btn-icon" aria-label="Dashboard actions">
13
+ <%= nq_icon(:more) %>
14
+ </summary>
15
+ <div class="nq-menu-panel" role="menu">
16
+ <%= link_to edit_dashboard_path(dashboard), class: "nq-menu-item", role: "menuitem" do %>
17
+ <%= nq_icon(:edit) %>
18
+ <span>Edit</span>
19
+ <% end %>
20
+ <%= button_to archive_dashboard_path(dashboard),
21
+ method: :patch,
22
+ class: "nq-menu-item nq-menu-item-button",
23
+ form: { data: { turbo_confirm: "Archive this dashboard?" }, role: "none" },
24
+ role: "menuitem" do %>
25
+ <%= nq_icon(:archive) %>
26
+ <span>Archive</span>
27
+ <% end %>
28
+ <%= button_to dashboard_path(dashboard),
29
+ method: :delete,
30
+ class: "nq-menu-item nq-menu-item-button nq-menu-item-danger",
31
+ form: { data: { turbo_confirm: "Remove this dashboard?" }, role: "none" },
32
+ role: "menuitem" do %>
33
+ <%= nq_icon(:trash) %>
34
+ <span>Remove</span>
35
+ <% end %>
36
+ </div>
37
+ </details>
38
+ </header>
39
+
40
+ <ul class="nq-dashboard-summary-charts">
41
+ <% dashboard.dashboard_cards.each do |card| %>
42
+ <li class="nq-dashboard-summary-chart-row">
43
+ <%= link_to chart_show_path(card.chart, dashboard: dashboard), class: "nq-dashboard-summary-chart-link" do %>
44
+ <%= nq_icon(:chart) %>
45
+ <span class="nq-dashboard-summary-chart-copy">
46
+ <span class="nq-dashboard-summary-chart-name"><%= card.chart.name %></span>
47
+ <span class="nq-dashboard-summary-chart-type"><%= card.chart.chart_type.titleize %></span>
48
+ </span>
49
+ <% end %>
50
+ <%= render "nquery/shared/chart_row_actions", chart: card.chart, dashboard: dashboard %>
51
+ </li>
52
+ <% end %>
53
+ <% if dashboard.dashboard_cards.empty? %>
54
+ <li class="nq-dashboard-summary-empty nq-muted">No charts on this dashboard yet.</li>
55
+ <% end %>
56
+ </ul>
57
+ </article>
@@ -0,0 +1,15 @@
1
+ <div class="nq-page-header"><h1>Edit collection: <%= @collection.name %></h1></div>
2
+
3
+ <%= form_with model: @collection, url: collection_path(@collection), method: :patch, class: "nq-form nq-card" do |f| %>
4
+ <div class="nq-form-row">
5
+ <%= f.label :name %>
6
+ <%= f.text_field :name, class: "nq-input" %>
7
+ </div>
8
+ <% unless @collection.kind == "root" %>
9
+ <div class="nq-form-row">
10
+ <%= f.label :parent_id, "Parent collection" %>
11
+ <%= f.collection_select :parent_id, @parent_collections, :id, :name, { include_blank: "None" }, class: "nq-input" %>
12
+ </div>
13
+ <% end %>
14
+ <%= f.submit "Save", class: "nq-btn nq-btn-primary" %>
15
+ <% end %>
@@ -0,0 +1,41 @@
1
+ <div class="nq-page-header">
2
+ <h1>Collections</h1>
3
+ <%= link_to "New collection", new_collection_path, class: "nq-btn nq-btn-primary" %>
4
+ </div>
5
+
6
+ <div class="nq-card nq-card-table">
7
+ <table class="nq-table">
8
+ <thead>
9
+ <tr>
10
+ <th>Name</th>
11
+ <th>Kind</th>
12
+ <th>Parent</th>
13
+ <th></th>
14
+ </tr>
15
+ </thead>
16
+ <tbody>
17
+ <% @collections.each do |collection| %>
18
+ <tr>
19
+ <td><%= link_to collection.name, collection_path(collection) %></td>
20
+ <td><%= collection.kind %></td>
21
+ <td><%= collection.parent&.name || "—" %></td>
22
+ <td>
23
+ <div class="nq-actions">
24
+ <%= link_to "Edit", edit_collection_path(collection), class: "nq-btn" %>
25
+ <% unless collection.kind == "root" %>
26
+ <% unless collection.archived? %>
27
+ <%= button_to "Archive",
28
+ archive_collection_path(collection),
29
+ method: :patch,
30
+ class: "nq-btn",
31
+ form: { data: { turbo_confirm: "Archive this collection?" } } %>
32
+ <% end %>
33
+ <%= button_to "Delete", collection_path(collection), method: :delete, class: "nq-btn nq-btn-danger", form: { data: { turbo_confirm: "Delete this collection?" } } %>
34
+ <% end %>
35
+ </div>
36
+ </td>
37
+ </tr>
38
+ <% end %>
39
+ </tbody>
40
+ </table>
41
+ </div>
@@ -0,0 +1,15 @@
1
+ <div class="nq-page-header"><h1>New collection</h1></div>
2
+
3
+ <%= form_with model: @collection, url: (@parent_collection ? collection_collections_path(@parent_collection) : collections_path), class: "nq-form nq-card" do |f| %>
4
+ <div class="nq-form-row">
5
+ <%= f.label :name %>
6
+ <%= f.text_field :name, class: "nq-input" %>
7
+ </div>
8
+ <% unless @parent_collection %>
9
+ <div class="nq-form-row">
10
+ <%= f.label :parent_id, "Parent collection" %>
11
+ <%= f.collection_select :parent_id, @parent_collections, :id, :name, { include_blank: "None" }, class: "nq-input" %>
12
+ </div>
13
+ <% end %>
14
+ <%= f.submit "Create", class: "nq-btn nq-btn-primary" %>
15
+ <% end %>