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,100 @@
1
+ <div class="nq-page-header">
2
+ <h1><%= @collection.name %></h1>
3
+ <p class="nq-muted nq-resource-meta">
4
+ <%= collection_kind_label(@collection) %>
5
+ · <%= pluralize(@child_collections.size, "folder", "folders") %>
6
+ · <%= pluralize(@charts.size, "chart") %>
7
+ · <%= pluralize(@dashboards.size, "dashboard") %>
8
+ </p>
9
+ <div class="nq-actions">
10
+ <%= link_to edit_collection_path(@collection), class: "nq-btn" do %>
11
+ <%= nq_icon(:edit) %>
12
+ <span>Edit</span>
13
+ <% end %>
14
+ <% unless @collection.kind == "root" %>
15
+ <% if @collection.archived? %>
16
+ <%= button_to unarchive_collection_path(@collection), method: :patch, class: "nq-btn" do %>
17
+ <%= nq_icon(:unarchive) %>
18
+ <span>Unarchive</span>
19
+ <% end %>
20
+ <% else %>
21
+ <%= button_to archive_collection_path(@collection), method: :patch, class: "nq-btn", form: { data: { turbo_confirm: "Archive this collection?" } } do %>
22
+ <%= nq_icon(:archive) %>
23
+ <span>Archive</span>
24
+ <% end %>
25
+ <% end %>
26
+ <%= button_to collection_path(@collection), method: :delete, class: "nq-btn nq-btn-danger", form: { data: { turbo_confirm: "Delete this collection?" } } do %>
27
+ <%= nq_icon(:trash) %>
28
+ <span>Delete</span>
29
+ <% end %>
30
+ <% end %>
31
+ <%= link_to new_collection_collection_path(@collection), class: "nq-btn" do %>
32
+ <%= nq_icon(:folder) %>
33
+ <span>New folder</span>
34
+ <% end %>
35
+ <%= link_to new_collection_dashboard_path(@collection), class: "nq-btn nq-btn-primary" do %>
36
+ <%= nq_icon(:dashboard) %>
37
+ <span>New dashboard</span>
38
+ <% end %>
39
+ </div>
40
+ </div>
41
+
42
+ <% if @collection.archived? %>
43
+ <%= nq_flash_card "This collection is archived.", type: :warning, toast: false %>
44
+ <% end %>
45
+
46
+ <% if @child_collections.empty? && @dashboards.empty? && @standalone_charts.empty? %>
47
+ <div class="nq-card nq-empty-state">
48
+ <div class="nq-empty-state-icon"><%= nq_icon(:folder, class: "nq-icon-lg") %></div>
49
+ <h2>This collection is empty</h2>
50
+ <p class="nq-muted">Create a dashboard to organize charts in this collection.</p>
51
+ <%= link_to new_collection_dashboard_path(@collection), class: "nq-btn nq-btn-primary" do %>
52
+ <%= nq_icon(:dashboard) %>
53
+ <span>Create dashboard</span>
54
+ <% end %>
55
+ </div>
56
+ <% else %>
57
+ <% if @child_collections.any? %>
58
+ <section class="nq-section nq-mb">
59
+ <h2 class="nq-section-title">Folders</h2>
60
+ <div class="nq-grid nq-grid-3">
61
+ <% @child_collections.each do |collection| %>
62
+ <%= link_to collection_path(collection), class: "nq-folder-card" do %>
63
+ <%= nq_icon(:folder) %>
64
+ <span><%= collection.name %></span>
65
+ <% end %>
66
+ <% end %>
67
+ </div>
68
+ </section>
69
+ <% end %>
70
+
71
+ <% if @dashboards.any? %>
72
+ <section class="nq-section">
73
+ <h2 class="nq-section-title">Dashboards</h2>
74
+ <div class="nq-grid nq-grid-2 nq-dashboard-summary-grid">
75
+ <% @dashboards.each do |dashboard| %>
76
+ <%= render "nquery/collections/dashboard_card", dashboard: dashboard %>
77
+ <% end %>
78
+ </div>
79
+ </section>
80
+ <% end %>
81
+
82
+ <% if @standalone_charts.any? %>
83
+ <section class="nq-section nq-mt">
84
+ <h2 class="nq-section-title">Charts</h2>
85
+ <div class="nq-card">
86
+ <ul class="nq-dashboard-summary-charts">
87
+ <% @standalone_charts.each do |chart| %>
88
+ <li>
89
+ <%= link_to chart_path(chart), class: "nq-dashboard-summary-chart-link" do %>
90
+ <%= nq_icon(:chart) %>
91
+ <span class="nq-dashboard-summary-chart-name"><%= chart.name %></span>
92
+ <span class="nq-muted"><%= chart.chart_type.titleize %></span>
93
+ <% end %>
94
+ </li>
95
+ <% end %>
96
+ </ul>
97
+ </div>
98
+ </section>
99
+ <% end %>
100
+ <% end %>
@@ -0,0 +1,6 @@
1
+ <%= render "nquery/charts/builder_form",
2
+ chart: @chart,
3
+ dashboard: @dashboard,
4
+ form_url: dashboard_charts_path(@dashboard),
5
+ data_sources: @data_sources,
6
+ schema: @schema %>
@@ -0,0 +1,16 @@
1
+ <div class="nq-dashboard-card"
2
+ style="grid-column: span <%= card.width %>; grid-row: span <%= card.height %>;"
3
+ data-dashboard-grid-target="card"
4
+ data-card-id="<%= card.id %>">
5
+ <header class="nq-dashboard-card-header">
6
+ <h3 class="nq-dashboard-card-title"><%= card.chart.name %></h3>
7
+ <%= render "nquery/shared/chart_actions_menu", chart: card.chart, dashboard: dashboard %>
8
+ </header>
9
+ <div class="nq-chart-preview" data-controller="chart-preview"
10
+ data-chart-preview-type-value="<%= card.chart.chart_type %>"
11
+ data-chart-preview-x-value="<%= card.chart.visualization.to_h["x"] %>"
12
+ data-chart-preview-y-value="<%= card.chart.visualization.to_h["y"] %>"
13
+ data-chart-preview-data-value="<%= card_result.to_json %>">
14
+ <canvas data-chart-preview-target="canvas" height="80"></canvas>
15
+ </div>
16
+ </div>
@@ -0,0 +1,34 @@
1
+ <div class="nq-page-header">
2
+ <h1>Edit dashboard: <%= @dashboard.name %></h1>
3
+ </div>
4
+
5
+ <%= form_with model: @dashboard, url: dashboard_path(@dashboard), method: :patch do |f| %>
6
+ <div class="nq-card nq-mb">
7
+ <div class="nq-form-row">
8
+ <%= f.label :name %>
9
+ <%= f.text_field :name, class: "nq-input" %>
10
+ </div>
11
+ <div class="nq-form-row">
12
+ <%= f.label :description %>
13
+ <%= f.text_area :description, class: "nq-textarea", rows: 2 %>
14
+ </div>
15
+ <div class="nq-form-row">
16
+ <%= f.label :collection_id, "Collection" %>
17
+ <%= f.collection_select :collection_id, @collections, :id, :name, { prompt: "Select a collection" }, class: "nq-input" %>
18
+ </div>
19
+ <%= f.submit "Save", class: "nq-btn nq-btn-primary" %>
20
+ </div>
21
+ <% end %>
22
+
23
+ <div class="nq-dashboard-grid nq-dashboard-edit" data-controller="dashboard-grid">
24
+ <% @dashboard.dashboard_cards.each do |card| %>
25
+ <div class="nq-dashboard-card nq-draggable"
26
+ style="grid-column: span <%= card.width %>; grid-row: span <%= card.height %>;"
27
+ data-dashboard-grid-target="card"
28
+ data-card-id="<%= card.id %>"
29
+ draggable="true">
30
+ <h3><%= card.chart.name %></h3>
31
+ <p class="nq-muted">Drag to reposition (preview)</p>
32
+ </div>
33
+ <% end %>
34
+ </div>
@@ -0,0 +1,35 @@
1
+ <div class="nq-page-header">
2
+ <h1>Dashboards</h1>
3
+ </div>
4
+
5
+ <div class="nq-card nq-card-table">
6
+ <table class="nq-table">
7
+ <thead>
8
+ <tr>
9
+ <th>Name</th>
10
+ <th>Collection</th>
11
+ <th></th>
12
+ </tr>
13
+ </thead>
14
+ <tbody>
15
+ <% @dashboards.each do |dashboard| %>
16
+ <tr>
17
+ <td><%= link_to dashboard.name, dashboard_path(dashboard) %></td>
18
+ <td><%= dashboard.collection&.name || "—" %></td>
19
+ <td>
20
+ <div class="nq-actions">
21
+ <%= link_to edit_dashboard_path(dashboard), class: "nq-btn" do %>
22
+ <%= nq_icon(:edit) %>
23
+ <span>Edit</span>
24
+ <% end %>
25
+ <%= button_to dashboard_path(dashboard), method: :delete, class: "nq-btn nq-btn-danger", form: { data: { turbo_confirm: "Delete this dashboard?" } } do %>
26
+ <%= nq_icon(:trash) %>
27
+ <span>Delete</span>
28
+ <% end %>
29
+ </div>
30
+ </td>
31
+ </tr>
32
+ <% end %>
33
+ </tbody>
34
+ </table>
35
+ </div>
@@ -0,0 +1,46 @@
1
+ <div class="nq-page-header">
2
+ <h1><%= @dashboard.name %></h1>
3
+ <p class="nq-muted nq-resource-meta">
4
+ Created by <%= @dashboard.creator&.name || "—" %>
5
+ · Created <%= @dashboard.created_at.to_fs(:long) %>
6
+ · Updated <%= @dashboard.updated_at.to_fs(:long) %>
7
+ </p>
8
+ <div class="nq-actions">
9
+ <%= link_to new_dashboard_chart_path(@dashboard), class: "nq-btn nq-btn-primary" do %>
10
+ <%= nq_icon(:chart) %>
11
+ <span>New chart</span>
12
+ <% end %>
13
+ <%= link_to edit_dashboard_path(@dashboard), class: "nq-btn" do %>
14
+ <%= nq_icon(:edit) %>
15
+ <span>Edit</span>
16
+ <% end %>
17
+ <% if @dashboard.archived? %>
18
+ <%= button_to unarchive_dashboard_path(@dashboard), method: :patch, class: "nq-btn" do %>
19
+ <%= nq_icon(:unarchive) %>
20
+ <span>Unarchive</span>
21
+ <% end %>
22
+ <% else %>
23
+ <%= button_to archive_dashboard_path(@dashboard), method: :patch, class: "nq-btn", form: { data: { turbo_confirm: "Archive this dashboard?" } } do %>
24
+ <%= nq_icon(:archive) %>
25
+ <span>Archive</span>
26
+ <% end %>
27
+ <% end %>
28
+ <%= button_to dashboard_path(@dashboard), method: :delete, class: "nq-btn nq-btn-danger", form: { data: { turbo_confirm: "Remove this dashboard?" } } do %>
29
+ <%= nq_icon(:trash) %>
30
+ <span>Remove</span>
31
+ <% end %>
32
+ </div>
33
+ </div>
34
+
35
+ <% if @dashboard.archived? %>
36
+ <%= nq_flash_card "This dashboard is archived.", type: :warning, toast: false %>
37
+ <% end %>
38
+
39
+ <div class="nq-dashboard-grid" data-controller="dashboard-grid">
40
+ <% @dashboard_cards.each do |card| %>
41
+ <%= render "nquery/dashboards/chart_card",
42
+ card: card,
43
+ dashboard: @dashboard,
44
+ card_result: @card_results[card] %>
45
+ <% end %>
46
+ </div>
@@ -0,0 +1,9 @@
1
+ <h1><%= @chart.name %></h1>
2
+
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>
@@ -0,0 +1,7 @@
1
+ <h1><%= @dashboard.name %></h1>
2
+
3
+ <% @dashboard.dashboard_cards.each do |card| %>
4
+ <section>
5
+ <h2><%= card.chart.name %></h2>
6
+ </section>
7
+ <% end %>
@@ -0,0 +1,143 @@
1
+ <div class="nq-home">
2
+ <header class="nq-home-hero nq-card">
3
+ <div class="nq-home-hero-content">
4
+ <p class="nq-home-eyebrow">Welcome back</p>
5
+ <h1 class="nq-home-title"><%= home_greeting %></h1>
6
+ <p class="nq-muted nq-home-subtitle">Pick up where you left off, or start something new.</p>
7
+ </div>
8
+ <% if @root_collection %>
9
+ <div class="nq-home-hero-actions">
10
+ <%= link_to new_chart_path, class: "nq-btn nq-home-hero-cta" do %>
11
+ <%= nq_icon(:chart) %>
12
+ <span>New question</span>
13
+ <% end %>
14
+ <%= link_to new_collection_dashboard_path(@root_collection), class: "nq-btn nq-btn-primary nq-home-hero-cta" do %>
15
+ <%= nq_icon(:plus) %>
16
+ <span>New dashboard</span>
17
+ <% end %>
18
+ </div>
19
+ <% end %>
20
+ </header>
21
+
22
+ <section class="nq-home-actions" aria-label="Quick actions">
23
+ <%= link_to collections_path, class: "nq-home-action-card" do %>
24
+ <span class="nq-home-action-icon nq-home-action-icon-folder">
25
+ <%= nq_icon(:folder) %>
26
+ </span>
27
+ <span class="nq-home-action-label">Collections</span>
28
+ <span class="nq-muted nq-home-action-desc">Browse folders and content</span>
29
+ <% end %>
30
+
31
+ <%= link_to dashboards_path, class: "nq-home-action-card" do %>
32
+ <span class="nq-home-action-icon nq-home-action-icon-dashboard">
33
+ <%= nq_icon(:dashboard) %>
34
+ </span>
35
+ <span class="nq-home-action-label">Dashboards</span>
36
+ <span class="nq-muted nq-home-action-desc">View all your dashboards</span>
37
+ <% end %>
38
+
39
+ <% if @root_collection %>
40
+ <%= link_to collection_path(@root_collection), class: "nq-home-action-card" do %>
41
+ <span class="nq-home-action-icon nq-home-action-icon-chart">
42
+ <%= nq_icon(:chart) %>
43
+ </span>
44
+ <span class="nq-home-action-label">Analytics</span>
45
+ <span class="nq-muted nq-home-action-desc">Open <%= @root_collection.name %></span>
46
+ <% end %>
47
+ <% end %>
48
+ </section>
49
+
50
+ <div class="nq-home-recents">
51
+ <section class="nq-card nq-home-panel">
52
+ <header class="nq-home-panel-header">
53
+ <div>
54
+ <h2 class="nq-home-panel-title">Recent dashboards</h2>
55
+ <p class="nq-muted nq-home-panel-desc">Dashboards you've viewed or updated recently</p>
56
+ </div>
57
+ <%= link_to dashboards_path, class: "nq-home-panel-link" do %>
58
+ <span>View all</span>
59
+ <%= nq_icon("arrow-right") %>
60
+ <% end %>
61
+ </header>
62
+
63
+ <% if @recent_dashboards.any? %>
64
+ <ul class="nq-home-list">
65
+ <% @recent_dashboards.each do |dashboard| %>
66
+ <li>
67
+ <%= link_to dashboard_path(dashboard), class: "nq-home-list-item" do %>
68
+ <span class="nq-home-list-icon nq-home-list-icon-dashboard">
69
+ <%= nq_icon(:dashboard) %>
70
+ </span>
71
+ <span class="nq-home-list-body">
72
+ <span class="nq-home-list-title"><%= dashboard.name %></span>
73
+ <span class="nq-muted nq-home-list-meta">
74
+ <%= dashboard.collection&.name || "No collection" %>
75
+ <% if dashboard.dashboard_cards.any? %>
76
+ · <%= pluralize(dashboard.dashboard_cards.size, "chart") %>
77
+ <% end %>
78
+ </span>
79
+ </span>
80
+ <span class="nq-muted nq-home-list-time"><%= time_ago_in_words(dashboard.updated_at) %> ago</span>
81
+ <% end %>
82
+ </li>
83
+ <% end %>
84
+ </ul>
85
+ <% else %>
86
+ <div class="nq-home-empty">
87
+ <%= nq_icon(:dashboard, class: "nq-icon-lg") %>
88
+ <p class="nq-muted">No dashboards yet</p>
89
+ <% if @root_collection %>
90
+ <%= link_to new_collection_dashboard_path(@root_collection), class: "nq-btn nq-btn-primary" do %>
91
+ <%= nq_icon(:plus) %>
92
+ <span>Create dashboard</span>
93
+ <% end %>
94
+ <% end %>
95
+ </div>
96
+ <% end %>
97
+ </section>
98
+
99
+ <section class="nq-card nq-home-panel">
100
+ <header class="nq-home-panel-header">
101
+ <div>
102
+ <h2 class="nq-home-panel-title">Recent charts</h2>
103
+ <p class="nq-muted nq-home-panel-desc">Charts from across your collections</p>
104
+ </div>
105
+ <%= link_to collections_path, class: "nq-home-panel-link" do %>
106
+ <span>Browse</span>
107
+ <%= nq_icon("arrow-right") %>
108
+ <% end %>
109
+ </header>
110
+
111
+ <% if @recent_charts.any? %>
112
+ <ul class="nq-home-list">
113
+ <% @recent_charts.each do |chart| %>
114
+ <li>
115
+ <%= link_to chart_path(chart), class: "nq-home-list-item" do %>
116
+ <span class="nq-home-list-icon nq-home-list-icon-chart">
117
+ <%= nq_icon(:chart) %>
118
+ </span>
119
+ <span class="nq-home-list-body">
120
+ <span class="nq-home-list-title"><%= chart.name %></span>
121
+ <span class="nq-muted nq-home-list-meta">
122
+ <%= chart.collection&.name || "No collection" %>
123
+ · <%= chart_type_label(chart) %>
124
+ </span>
125
+ </span>
126
+ <span class="nq-muted nq-home-list-time"><%= time_ago_in_words(chart.updated_at) %> ago</span>
127
+ <% end %>
128
+ </li>
129
+ <% end %>
130
+ </ul>
131
+ <% else %>
132
+ <div class="nq-home-empty">
133
+ <%= nq_icon(:chart, class: "nq-icon-lg") %>
134
+ <p class="nq-muted">No charts yet</p>
135
+ <%= link_to collections_path, class: "nq-btn" do %>
136
+ <%= nq_icon(:folder) %>
137
+ <span>Browse collections</span>
138
+ <% end %>
139
+ </div>
140
+ <% end %>
141
+ </section>
142
+ </div>
143
+ </div>
@@ -0,0 +1,20 @@
1
+ <div class="nq-page-header">
2
+ <h1>Import CSV</h1>
3
+ </div>
4
+
5
+ <%= form_with url: imports_path, multipart: true, class: "nq-form", data: { controller: "csv-mapper" } do %>
6
+ <div class="nq-card">
7
+ <div class="nq-form-row">
8
+ <label>Name</label>
9
+ <input type="text" name="name" class="nq-input" placeholder="My dataset" />
10
+ </div>
11
+ <div class="nq-form-row">
12
+ <label>CSV file</label>
13
+ <input type="file" name="file" accept=".csv" class="nq-input"
14
+ data-action="change->csv-mapper#preview" data-csv-mapper-target="file" />
15
+ </div>
16
+ <div data-csv-mapper-target="preview" class="nq-csv-preview"></div>
17
+ <input type="hidden" name="column_mapping" data-csv-mapper-target="mapping" value="{}" />
18
+ <%= submit_tag "Import", class: "nq-btn nq-btn-primary" %>
19
+ </div>
20
+ <% end %>
@@ -0,0 +1,40 @@
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 %>
@@ -0,0 +1,37 @@
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 %>
@@ -0,0 +1,34 @@
1
+ <%= render layout: "nquery/shared/auth_shell",
2
+ locals: {
3
+ title: "Create your account",
4
+ subtitle: "Get started with SQL charts and dashboards in minutes.",
5
+ footer_text: "Already have an account?",
6
+ footer_link: login_path,
7
+ footer_label: "Sign in"
8
+ } do %>
9
+ <%= form_with model: @user, url: signup_path, class: "nq-form nq-auth-form" do |f| %>
10
+ <div class="nq-form-row">
11
+ <%= f.label :email, "Email address" %>
12
+ <%= f.email_field :email, class: "nq-input", required: true, autofocus: true, autocomplete: "email", placeholder: "you@company.com" %>
13
+ </div>
14
+ <div class="nq-form-row nq-auth-form-row-split">
15
+ <div>
16
+ <%= f.label :first_name, "First name" %>
17
+ <%= f.text_field :first_name, class: "nq-input", required: true, autocomplete: "given-name", placeholder: "Jane" %>
18
+ </div>
19
+ <div>
20
+ <%= f.label :last_name, "Last name" %>
21
+ <%= f.text_field :last_name, class: "nq-input", required: true, autocomplete: "family-name", placeholder: "Doe" %>
22
+ </div>
23
+ </div>
24
+ <div class="nq-form-row">
25
+ <%= f.label :password, "Password" %>
26
+ <%= f.password_field :password, class: "nq-input", required: true, autocomplete: "new-password", placeholder: "At least 8 characters" %>
27
+ </div>
28
+ <div class="nq-form-row">
29
+ <%= f.label :password_confirmation, "Confirm password" %>
30
+ <%= f.password_field :password_confirmation, class: "nq-input", required: true, autocomplete: "new-password", placeholder: "Re-enter your password" %>
31
+ </div>
32
+ <%= f.submit "Create account", class: "nq-btn nq-btn-primary nq-btn-block nq-auth-submit" %>
33
+ <% end %>
34
+ <% end %>
@@ -0,0 +1,29 @@
1
+ <%= render layout: "nquery/shared/auth_shell",
2
+ locals: {
3
+ title: "Welcome back",
4
+ subtitle: "Sign in to explore your charts and dashboards.",
5
+ footer_text: "Don't have an account?",
6
+ footer_link: signup_path,
7
+ footer_label: "Create an account"
8
+ } do %>
9
+ <%= form_with url: login_path, class: "nq-form nq-auth-form" do %>
10
+ <div class="nq-form-row">
11
+ <%= label_tag :email, "Email address" %>
12
+ <%= email_field_tag :email, nil,
13
+ class: "nq-input",
14
+ required: true,
15
+ autofocus: true,
16
+ autocomplete: "email",
17
+ placeholder: "you@company.com" %>
18
+ </div>
19
+ <div class="nq-form-row">
20
+ <%= label_tag :password, "Password" %>
21
+ <%= password_field_tag :password, nil,
22
+ class: "nq-input",
23
+ required: true,
24
+ autocomplete: "current-password",
25
+ placeholder: "Enter your password" %>
26
+ </div>
27
+ <button type="submit" class="nq-btn nq-btn-primary nq-btn-block nq-auth-submit">Sign in</button>
28
+ <% end %>
29
+ <% end %>
@@ -0,0 +1,22 @@
1
+ <div class="nq-auth">
2
+ <div class="nq-auth-backdrop" aria-hidden="true"></div>
3
+
4
+ <div class="nq-auth-panel">
5
+ <header class="nq-auth-header">
6
+ <span class="nq-auth-mark">nquery</span>
7
+ <h1><%= title %></h1>
8
+ <% if subtitle.present? %>
9
+ <p class="nq-auth-subtitle"><%= subtitle %></p>
10
+ <% end %>
11
+ </header>
12
+
13
+ <%= yield %>
14
+
15
+ <% if footer_link.present? %>
16
+ <footer class="nq-auth-footer">
17
+ <span class="nq-muted"><%= footer_text %></span>
18
+ <%= link_to footer_label, footer_link, class: "nq-auth-link" %>
19
+ </footer>
20
+ <% end %>
21
+ </div>
22
+ </div>
@@ -0,0 +1,15 @@
1
+ <% if breadcrumbs.any? %>
2
+ <nav class="nq-breadcrumbs" aria-label="Breadcrumb">
3
+ <ol class="nq-breadcrumb-list">
4
+ <% breadcrumbs.each_with_index do |crumb, index| %>
5
+ <li class="nq-breadcrumb-item">
6
+ <% if crumb[:path].present? %>
7
+ <%= link_to crumb[:label], crumb[:path], **(index == breadcrumbs.length - 1 ? { aria: { current: "page" } } : {}) %>
8
+ <% else %>
9
+ <span<% if index == breadcrumbs.length - 1 %> aria-current="page"<% end %>><%= crumb[:label] %></span>
10
+ <% end %>
11
+ </li>
12
+ <% end %>
13
+ </ol>
14
+ </nav>
15
+ <% end %>
@@ -0,0 +1,27 @@
1
+ <details class="nq-menu">
2
+ <summary class="nq-btn nq-btn-icon" aria-label="Chart actions">
3
+ <%= nq_icon(:more) %>
4
+ </summary>
5
+ <div class="nq-menu-panel" role="menu">
6
+ <%= link_to chart_edit_path(chart, dashboard: dashboard), class: "nq-menu-item", role: "menuitem" do %>
7
+ <%= nq_icon(:edit) %>
8
+ <span>Edit</span>
9
+ <% end %>
10
+ <%= button_to chart_archive_path(chart, dashboard: dashboard),
11
+ method: :patch,
12
+ class: "nq-menu-item nq-menu-item-button",
13
+ form: { data: { turbo_confirm: "Archive this chart?" }, role: "none" },
14
+ role: "menuitem" do %>
15
+ <%= nq_icon(:archive) %>
16
+ <span>Archive</span>
17
+ <% end %>
18
+ <%= button_to chart_show_path(chart, dashboard: dashboard),
19
+ method: :delete,
20
+ class: "nq-menu-item nq-menu-item-button nq-menu-item-danger",
21
+ form: { data: { turbo_confirm: "Remove this chart?" }, role: "none" },
22
+ role: "menuitem" do %>
23
+ <%= nq_icon(:trash) %>
24
+ <span>Remove</span>
25
+ <% end %>
26
+ </div>
27
+ </details>
@@ -0,0 +1,16 @@
1
+ <div class="nq-chart-row-actions">
2
+ <%= button_to chart_archive_path(chart, dashboard: dashboard),
3
+ method: :patch,
4
+ class: "nq-btn nq-btn-icon",
5
+ form: { data: { turbo_confirm: "Archive this chart?" } },
6
+ aria: { label: "Archive chart" } do %>
7
+ <%= nq_icon(:archive) %>
8
+ <% end %>
9
+ <%= button_to chart_show_path(chart, dashboard: dashboard),
10
+ method: :delete,
11
+ class: "nq-btn nq-btn-icon nq-btn-icon-danger",
12
+ form: { data: { turbo_confirm: "Remove this chart?" } },
13
+ aria: { label: "Remove chart" } do %>
14
+ <%= nq_icon(:trash) %>
15
+ <% end %>
16
+ </div>