@agent-native/core 0.77.24 → 0.78.1

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 (212) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +20 -0
  3. package/corpus/core/docs/content/template-analytics.md +5 -0
  4. package/corpus/core/docs/content/tracking.md +45 -0
  5. package/corpus/core/package.json +3 -1
  6. package/corpus/core/src/agent/types.ts +22 -0
  7. package/corpus/core/src/client/AgentPanel.tsx +34 -18
  8. package/corpus/core/src/client/AssistantChat.tsx +40 -0
  9. package/corpus/core/src/client/agent-chat.ts +173 -0
  10. package/corpus/core/src/client/analytics.ts +141 -5
  11. package/corpus/core/src/client/composer/TiptapComposer.tsx +316 -60
  12. package/corpus/core/src/client/composer/extensions/MentionReference.tsx +3 -0
  13. package/corpus/core/src/client/composer/types.ts +22 -0
  14. package/corpus/core/src/client/index.ts +14 -0
  15. package/corpus/core/src/client/session-replay.ts +800 -0
  16. package/corpus/core/src/db/schema.ts +12 -0
  17. package/corpus/core/src/deploy/build.ts +75 -0
  18. package/corpus/core/src/private-blob/index.ts +18 -0
  19. package/corpus/core/src/private-blob/registry.ts +241 -0
  20. package/corpus/core/src/private-blob/types.ts +55 -0
  21. package/corpus/core/src/scripts/utils.ts +34 -6
  22. package/corpus/core/src/server/agent-chat-plugin.ts +10 -0
  23. package/corpus/core/src/server/email.ts +5 -0
  24. package/corpus/core/src/server/index.ts +1 -0
  25. package/corpus/core/src/styles/agent-native.css +24 -0
  26. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +1 -1
  27. package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +43 -0
  28. package/corpus/templates/analytics/AGENTS.md +32 -1
  29. package/corpus/templates/analytics/DEVELOPING.md +9 -1
  30. package/corpus/templates/analytics/actions/delete-dashboard-report-subscription.ts +24 -0
  31. package/corpus/templates/analytics/actions/get-session-replay-events.ts +50 -0
  32. package/corpus/templates/analytics/actions/get-session-replay-summary.ts +27 -0
  33. package/corpus/templates/analytics/actions/list-dashboard-report-subscriptions.ts +24 -0
  34. package/corpus/templates/analytics/actions/list-session-recordings.ts +59 -0
  35. package/corpus/templates/analytics/actions/navigate.ts +15 -5
  36. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +2 -2
  37. package/corpus/templates/analytics/actions/save-dashboard-report-subscription.ts +36 -0
  38. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +60 -0
  39. package/corpus/templates/analytics/actions/view-screen.ts +50 -2
  40. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +51 -2
  41. package/corpus/templates/analytics/app/components/layout/Header.tsx +4 -1
  42. package/corpus/templates/analytics/app/components/layout/Layout.tsx +14 -2
  43. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +6 -6
  44. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +35 -6
  45. package/corpus/templates/analytics/app/i18n-data.ts +1119 -0
  46. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +437 -0
  47. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +11 -12
  48. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +43 -0
  49. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +136 -43
  50. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/report-filters.ts +26 -0
  51. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +450 -0
  52. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +419 -0
  53. package/corpus/templates/analytics/app/routes/overview.tsx +13 -6
  54. package/corpus/templates/analytics/app/routes/sessions.$recordingId.tsx +10 -0
  55. package/corpus/templates/analytics/app/routes/sessions._index.tsx +10 -0
  56. package/corpus/templates/analytics/changelog/2026-06-25-analytics-now-opens-directly-to-ask-instead-of-the-old-overv.md +6 -0
  57. package/corpus/templates/analytics/changelog/2026-06-25-dashboard-charts-now-show-reliable-blue-placement-lines-whil.md +6 -0
  58. package/corpus/templates/analytics/changelog/2026-06-26-dashboards-can-now-send-scheduled-daily-email-reports-from-t.md +6 -0
  59. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-browser-sessions-can-now-be-reviewed-from.md +6 -0
  60. package/corpus/templates/analytics/package.json +5 -2
  61. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +131 -0
  62. package/corpus/templates/analytics/scripts/seed-session-replay.ts +198 -0
  63. package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +140 -27
  64. package/corpus/templates/analytics/server/db/index.ts +12 -0
  65. package/corpus/templates/analytics/server/db/schema.ts +131 -0
  66. package/corpus/templates/analytics/server/handlers/session-replay.ts +281 -0
  67. package/corpus/templates/analytics/server/handlers/sql-query.ts +8 -348
  68. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +66 -0
  69. package/corpus/templates/analytics/server/jobs/session-replay-retention.ts +29 -0
  70. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +365 -0
  71. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +473 -0
  72. package/corpus/templates/analytics/server/lib/dashboard-report.ts +405 -0
  73. package/corpus/templates/analytics/server/lib/first-party-analytics.ts +63 -18
  74. package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +98 -26
  75. package/corpus/templates/analytics/server/lib/session-replay.ts +1645 -0
  76. package/corpus/templates/analytics/server/plugins/auth.ts +1 -1
  77. package/corpus/templates/analytics/server/plugins/dashboard-report-jobs.ts +47 -0
  78. package/corpus/templates/analytics/server/plugins/db.ts +200 -0
  79. package/corpus/templates/analytics/server/plugins/session-replay-retention-jobs.ts +41 -0
  80. package/corpus/templates/analytics/server/routes/api/analytics/replay.options.ts +1 -0
  81. package/corpus/templates/analytics/server/routes/api/analytics/replay.post.ts +1 -0
  82. package/corpus/templates/analytics/server/routes/api/dashboard-reports/run.post.ts +61 -0
  83. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/chunks/[seq].get.ts +1 -0
  84. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/events.get.ts +1 -0
  85. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId]/manifest.get.ts +1 -0
  86. package/corpus/templates/analytics/server/routes/api/session-replay/recordings/[recordingId].get.ts +1 -0
  87. package/corpus/templates/analytics/server/routes/api/session-replay/recordings.get.ts +1 -0
  88. package/corpus/templates/assets/.agents/skills/asset-generation/SKILL.md +10 -2
  89. package/corpus/templates/assets/.agents/skills/image-generation/SKILL.md +14 -7
  90. package/corpus/templates/assets/.agents/skills/library-management/SKILL.md +12 -0
  91. package/corpus/templates/assets/AGENTS.md +18 -3
  92. package/corpus/templates/assets/actions/_image-model-default.ts +24 -0
  93. package/corpus/templates/assets/actions/dismiss-variant-slots.ts +15 -22
  94. package/corpus/templates/assets/actions/generate-asset.ts +30 -23
  95. package/corpus/templates/assets/actions/generate-image-batch.ts +76 -29
  96. package/corpus/templates/assets/actions/generate-image.ts +46 -30
  97. package/corpus/templates/assets/actions/generate-video.ts +18 -3
  98. package/corpus/templates/assets/actions/list-assets.ts +39 -8
  99. package/corpus/templates/assets/actions/list-libraries.ts +24 -2
  100. package/corpus/templates/assets/actions/navigate.ts +1 -1
  101. package/corpus/templates/assets/actions/open-asset-picker.ts +38 -1
  102. package/corpus/templates/assets/actions/refresh-generation-run.ts +10 -0
  103. package/corpus/templates/assets/actions/save-generated-image.ts +21 -19
  104. package/corpus/templates/assets/actions/variant-slots.ts +131 -28
  105. package/corpus/templates/assets/actions/view-screen.ts +9 -0
  106. package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +425 -0
  107. package/corpus/templates/assets/app/components/layout/Header.tsx +3 -1
  108. package/corpus/templates/assets/app/components/layout/Layout.tsx +5 -0
  109. package/corpus/templates/assets/app/components/layout/Sidebar.tsx +4 -6
  110. package/corpus/templates/assets/app/hooks/use-navigation-state.ts +47 -21
  111. package/corpus/templates/assets/app/i18n-data.ts +1815 -0
  112. package/corpus/templates/assets/app/lib/libraries.ts +6 -0
  113. package/corpus/templates/assets/app/lib/picker-chat-handoff.ts +71 -0
  114. package/corpus/templates/assets/app/routes/_index.tsx +4 -0
  115. package/corpus/templates/assets/app/routes/asset.$id.tsx +2 -2
  116. package/corpus/templates/assets/app/routes/audit.tsx +1 -1
  117. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +1557 -2164
  118. package/corpus/templates/assets/app/routes/brand-kits._index.tsx +7 -191
  119. package/corpus/templates/assets/app/routes/brand-kits.tsx +1 -5
  120. package/corpus/templates/assets/app/routes/libraries.tsx +4 -4
  121. package/corpus/templates/assets/app/routes/library.$id.tsx +9 -7
  122. package/corpus/templates/assets/app/routes/library.tsx +1454 -175
  123. package/corpus/templates/assets/server/db/index.ts +1 -1
  124. package/corpus/templates/assets/server/lib/generation.ts +86 -15
  125. package/corpus/templates/assets/server/plugins/agent-chat.ts +196 -0
  126. package/corpus/templates/assets/server/plugins/db.ts +5 -0
  127. package/corpus/templates/assets/shared/api.ts +6 -3
  128. package/dist/agent/types.d.ts +21 -0
  129. package/dist/agent/types.d.ts.map +1 -1
  130. package/dist/agent/types.js.map +1 -1
  131. package/dist/client/AgentPanel.d.ts +6 -2
  132. package/dist/client/AgentPanel.d.ts.map +1 -1
  133. package/dist/client/AgentPanel.js +10 -4
  134. package/dist/client/AgentPanel.js.map +1 -1
  135. package/dist/client/AssistantChat.d.ts +6 -0
  136. package/dist/client/AssistantChat.d.ts.map +1 -1
  137. package/dist/client/AssistantChat.js +15 -3
  138. package/dist/client/AssistantChat.js.map +1 -1
  139. package/dist/client/agent-chat.d.ts +32 -0
  140. package/dist/client/agent-chat.d.ts.map +1 -1
  141. package/dist/client/agent-chat.js +113 -0
  142. package/dist/client/agent-chat.js.map +1 -1
  143. package/dist/client/analytics.d.ts +22 -3
  144. package/dist/client/analytics.d.ts.map +1 -1
  145. package/dist/client/analytics.js +96 -4
  146. package/dist/client/analytics.js.map +1 -1
  147. package/dist/client/composer/TiptapComposer.d.ts +2 -1
  148. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  149. package/dist/client/composer/TiptapComposer.js +228 -56
  150. package/dist/client/composer/TiptapComposer.js.map +1 -1
  151. package/dist/client/composer/extensions/MentionReference.d.ts.map +1 -1
  152. package/dist/client/composer/extensions/MentionReference.js +3 -0
  153. package/dist/client/composer/extensions/MentionReference.js.map +1 -1
  154. package/dist/client/composer/types.d.ts +21 -0
  155. package/dist/client/composer/types.d.ts.map +1 -1
  156. package/dist/client/composer/types.js.map +1 -1
  157. package/dist/client/index.d.ts +2 -2
  158. package/dist/client/index.d.ts.map +1 -1
  159. package/dist/client/index.js +2 -2
  160. package/dist/client/index.js.map +1 -1
  161. package/dist/client/session-replay.d.ts +41 -0
  162. package/dist/client/session-replay.d.ts.map +1 -0
  163. package/dist/client/session-replay.js +595 -0
  164. package/dist/client/session-replay.js.map +1 -0
  165. package/dist/collab/awareness.d.ts +2 -2
  166. package/dist/collab/awareness.d.ts.map +1 -1
  167. package/dist/collab/routes.d.ts +2 -2
  168. package/dist/db/schema.d.ts +3 -1
  169. package/dist/db/schema.d.ts.map +1 -1
  170. package/dist/db/schema.js +6 -2
  171. package/dist/db/schema.js.map +1 -1
  172. package/dist/deploy/build.d.ts +8 -0
  173. package/dist/deploy/build.d.ts.map +1 -1
  174. package/dist/deploy/build.js +66 -0
  175. package/dist/deploy/build.js.map +1 -1
  176. package/dist/file-upload/actions/upload-image.d.ts +2 -2
  177. package/dist/notifications/routes.d.ts +3 -3
  178. package/dist/observability/routes.d.ts +8 -8
  179. package/dist/private-blob/index.d.ts +3 -0
  180. package/dist/private-blob/index.d.ts.map +1 -0
  181. package/dist/private-blob/index.js +2 -0
  182. package/dist/private-blob/index.js.map +1 -0
  183. package/dist/private-blob/registry.d.ts +10 -0
  184. package/dist/private-blob/registry.d.ts.map +1 -0
  185. package/dist/private-blob/registry.js +152 -0
  186. package/dist/private-blob/registry.js.map +1 -0
  187. package/dist/private-blob/types.d.ts +51 -0
  188. package/dist/private-blob/types.d.ts.map +1 -0
  189. package/dist/private-blob/types.js +2 -0
  190. package/dist/private-blob/types.js.map +1 -0
  191. package/dist/resources/handlers.d.ts +3 -3
  192. package/dist/scripts/utils.d.ts +4 -4
  193. package/dist/scripts/utils.d.ts.map +1 -1
  194. package/dist/scripts/utils.js +30 -6
  195. package/dist/scripts/utils.js.map +1 -1
  196. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  197. package/dist/server/agent-chat-plugin.js +5 -0
  198. package/dist/server/agent-chat-plugin.js.map +1 -1
  199. package/dist/server/agent-engine-api-key-route.d.ts +2 -2
  200. package/dist/server/email.d.ts +2 -0
  201. package/dist/server/email.d.ts.map +1 -1
  202. package/dist/server/email.js +3 -0
  203. package/dist/server/email.js.map +1 -1
  204. package/dist/server/index.d.ts +1 -1
  205. package/dist/server/index.d.ts.map +1 -1
  206. package/dist/server/index.js.map +1 -1
  207. package/dist/server/transcribe-voice.d.ts +1 -1
  208. package/dist/styles/agent-native.css +24 -0
  209. package/docs/content/template-analytics.md +5 -0
  210. package/docs/content/tracking.md +45 -0
  211. package/package.json +3 -1
  212. package/corpus/templates/analytics/app/pages/overview/OverviewPage.tsx +0 -209
@@ -14,6 +14,7 @@ import {
14
14
  type CollabUser,
15
15
  } from "@agent-native/core/client";
16
16
  import {
17
+ closestCenter,
17
18
  useDroppable,
18
19
  DndContext,
19
20
  pointerWithin,
@@ -22,7 +23,7 @@ import {
22
23
  useSensors,
23
24
  type DragEndEvent,
24
25
  type DragOverEvent,
25
- type DragStartEvent,
26
+ type CollisionDetection,
26
27
  } from "@dnd-kit/core";
27
28
  import {
28
29
  IconArchive,
@@ -31,6 +32,7 @@ import {
31
32
  IconEye,
32
33
  IconEyeOff,
33
34
  IconInfoCircle,
35
+ IconMail,
34
36
  IconPencil,
35
37
  IconPlus,
36
38
  IconTrash,
@@ -101,6 +103,7 @@ import { DashboardSkeleton } from "../DashboardSkeleton";
101
103
  import {
102
104
  buildDashboardPanelGroups,
103
105
  dropSlotId,
106
+ isDropSlotAvailable,
104
107
  movePanelToDropSlot,
105
108
  readDropSlot,
106
109
  removePanelFromLayout,
@@ -110,8 +113,10 @@ import {
110
113
  import {
111
114
  DashboardFilterBar,
112
115
  FILTER_PARAM_PREFIX,
116
+ extractFilterParams,
113
117
  resolveFilterVars,
114
118
  } from "./DashboardFilterBar";
119
+ import { EmailReportDialog } from "./EmailReportDialog";
115
120
  import { interpolate } from "./interpolate";
116
121
  import { serializePanelSql } from "./panel-sql";
117
122
  import { AddPanelPopover, PanelEditorDialog } from "./PanelEditorDialog";
@@ -289,6 +294,7 @@ export default function SqlDashboardPage() {
289
294
  const queryClient = useQueryClient();
290
295
  const navigate = useNavigate();
291
296
  const dashboardId = searchParams.get("id") || routeId;
297
+ const reportScreenshot = searchParams.get("reportScreenshot") === "1";
292
298
 
293
299
  const [dashboard, setDashboard] = useState<SqlDashboardConfig | null>(null);
294
300
  const [archivedAt, setArchivedAt] = useState<string | null>(null);
@@ -310,12 +316,12 @@ export default function SqlDashboardPage() {
310
316
  const [descriptionInput, setDescriptionInput] = useState("");
311
317
  const [loaded, setLoaded] = useState(false);
312
318
  const [confirmDeleteOpen, setConfirmDeleteOpen] = useState(false);
313
- const [draggingPanelId, setDraggingPanelId] = useState<string | null>(null);
319
+ const [emailReportOpen, setEmailReportOpen] = useState(false);
314
320
  const [activeDropSlot, setActiveDropSlot] =
315
321
  useState<DashboardDropSlot | null>(null);
316
322
  const viewedDashboardIdRef = useRef<string | null>(null);
317
- const canEdit = resourceCanEdit(resourceAccess);
318
- const canManage = resourceCanManage(resourceAccess);
323
+ const canEdit = !reportScreenshot && resourceCanEdit(resourceAccess);
324
+ const canManage = !reportScreenshot && resourceCanManage(resourceAccess);
319
325
  const dashboardColumns = clampDashboardColumns(
320
326
  dashboard?.columns ?? DEFAULT_DASHBOARD_COLUMNS,
321
327
  );
@@ -378,15 +384,17 @@ export default function SqlDashboardPage() {
378
384
 
379
385
  // ── Collaborative editing ──────────────────────────────────────────
380
386
  const { session } = useSession();
381
- const currentUser: CollabUser | undefined = session?.email
382
- ? {
383
- name: emailToName(session.email),
384
- email: session.email,
385
- color: emailToColor(session.email),
386
- }
387
- : undefined;
387
+ const currentUser: CollabUser | undefined =
388
+ !reportScreenshot && session?.email
389
+ ? {
390
+ name: emailToName(session.email),
391
+ email: session.email,
392
+ color: emailToColor(session.email),
393
+ }
394
+ : undefined;
388
395
 
389
- const collabDocId = dashboardId ? `dash-${dashboardId}` : null;
396
+ const collabDocId =
397
+ !reportScreenshot && dashboardId ? `dash-${dashboardId}` : null;
390
398
  const {
391
399
  ydoc,
392
400
  awareness,
@@ -506,15 +514,31 @@ export default function SqlDashboardPage() {
506
514
  : null,
507
515
  );
508
516
  setLoaded(true);
509
- if (fetched && viewedDashboardIdRef.current !== dashboardId) {
517
+ if (
518
+ fetched &&
519
+ !reportScreenshot &&
520
+ viewedDashboardIdRef.current !== dashboardId
521
+ ) {
510
522
  viewedDashboardIdRef.current = dashboardId;
511
523
  incrementItemView("dashboard", dashboardId);
512
524
  }
513
- }, [dashboardId, dashboardQuery.data, dashboardQuery.isSuccess]);
525
+ }, [
526
+ dashboardId,
527
+ dashboardQuery.data,
528
+ dashboardQuery.isSuccess,
529
+ reportScreenshot,
530
+ ]);
514
531
 
515
532
  // Apply saved filters on initial load if no filter URL params are present
516
533
  useEffect(() => {
517
- if (appliedSaved.current || filtersLoading || !loaded || !dashboard) return;
534
+ if (
535
+ reportScreenshot ||
536
+ appliedSaved.current ||
537
+ filtersLoading ||
538
+ !loaded ||
539
+ !dashboard
540
+ )
541
+ return;
518
542
  appliedSaved.current = true;
519
543
 
520
544
  // Check if there's a view param — if so, load view filters
@@ -558,6 +582,7 @@ export default function SqlDashboardPage() {
558
582
  loaded,
559
583
  dashboard,
560
584
  savedFilters,
585
+ reportScreenshot,
561
586
  searchParams,
562
587
  setSearchParams,
563
588
  ]);
@@ -565,7 +590,13 @@ export default function SqlDashboardPage() {
565
590
  // Auto-save filter state when URL params change (debounced)
566
591
  const saveTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
567
592
  useEffect(() => {
568
- if (!loaded || !dashboard?.filters?.length || !dashboardId) return;
593
+ if (
594
+ reportScreenshot ||
595
+ !loaded ||
596
+ !dashboard?.filters?.length ||
597
+ !dashboardId
598
+ )
599
+ return;
569
600
  clearTimeout(saveTimer.current);
570
601
  saveTimer.current = setTimeout(() => {
571
602
  const currentFilters: Record<string, string> = {};
@@ -577,7 +608,14 @@ export default function SqlDashboardPage() {
577
608
  saveFilterPref({ filters: currentFilters });
578
609
  }, 1500);
579
610
  return () => clearTimeout(saveTimer.current);
580
- }, [searchParams, loaded, dashboard?.filters, dashboardId, saveFilterPref]);
611
+ }, [
612
+ searchParams,
613
+ loaded,
614
+ dashboard?.filters,
615
+ dashboardId,
616
+ saveFilterPref,
617
+ reportScreenshot,
618
+ ]);
581
619
 
582
620
  /**
583
621
  * Push a config update through the collab layer so other tabs/users
@@ -718,24 +756,20 @@ export default function SqlDashboardPage() {
718
756
  useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
719
757
  );
720
758
 
721
- const handleDragStart = useCallback((event: DragStartEvent) => {
722
- setDraggingPanelId(String(event.active.id));
723
- }, []);
724
-
725
759
  const handleDragOver = useCallback((event: DragOverEvent) => {
726
760
  setActiveDropSlot(readDropSlot(event.over?.data.current));
727
761
  }, []);
728
762
 
729
763
  const handleDragEnd = useCallback(
730
764
  (event: DragEndEvent) => {
731
- setDraggingPanelId(null);
732
765
  setActiveDropSlot(null);
733
766
  if (!dashboard) return;
734
767
  const slot = readDropSlot(event.over?.data.current);
735
768
  if (!slot) return;
769
+ const panelId = String(event.active.id);
736
770
  const nextPanels = movePanelToDropSlot(
737
771
  dashboard.panels,
738
- String(event.active.id),
772
+ panelId,
739
773
  slot,
740
774
  dashboardColumns,
741
775
  );
@@ -749,7 +783,6 @@ export default function SqlDashboardPage() {
749
783
  );
750
784
 
751
785
  const handleDragCancel = useCallback(() => {
752
- setDraggingPanelId(null);
753
786
  setActiveDropSlot(null);
754
787
  }, []);
755
788
 
@@ -780,6 +813,15 @@ export default function SqlDashboardPage() {
780
813
  return { ...(dashboard?.variables ?? {}), ...filterValues };
781
814
  }, [dashboard?.variables, dashboard?.filters, searchParams]);
782
815
 
816
+ const currentReportFilters = useMemo<Record<string, string>>(() => {
817
+ const out = dashboard?.filters
818
+ ? extractFilterParams(dashboard.filters, searchParams)
819
+ : {};
820
+ const tab = searchParams.get("tab");
821
+ if (tab) out.tab = tab;
822
+ return out;
823
+ }, [dashboard?.filters, searchParams]);
824
+
783
825
  // Distinct tab values across panels in declaration order. When this is
784
826
  // non-empty the dashboard renders a tab strip and filters panels by tab.
785
827
  const tabs = useMemo<string[]>(() => {
@@ -848,6 +890,34 @@ export default function SqlDashboardPage() {
848
890
  return buildDashboardPanelGroups(visiblePanels, dashboardColumns);
849
891
  }, [visiblePanels, dashboardColumns]);
850
892
 
893
+ const dashboardCollisionDetection = useCallback<CollisionDetection>(
894
+ (args) => {
895
+ const activePanelId = String(args.active.id);
896
+ const droppableContainers = args.droppableContainers.filter(
897
+ (container) => {
898
+ const slot = readDropSlot(container.data.current);
899
+ return (
900
+ !!slot && isDropSlotAvailable(panelGroups, activePanelId, slot)
901
+ );
902
+ },
903
+ );
904
+
905
+ if (droppableContainers.length === 0) return [];
906
+
907
+ const exactCollisions = pointerWithin({
908
+ ...args,
909
+ droppableContainers,
910
+ });
911
+ if (exactCollisions.length > 0) return exactCollisions;
912
+
913
+ return closestCenter({
914
+ ...args,
915
+ droppableContainers,
916
+ });
917
+ },
918
+ [panelGroups],
919
+ );
920
+
851
921
  const handleDelete = useCallback(async () => {
852
922
  if (!dashboardId) return;
853
923
  if (!canManage) return;
@@ -945,7 +1015,7 @@ export default function SqlDashboardPage() {
945
1015
  );
946
1016
 
947
1017
  useSetPageTitle(
948
- dashboard ? (
1018
+ reportScreenshot ? null : dashboard ? (
949
1019
  <div className="flex items-center gap-2 min-w-0">
950
1020
  {editingName && canEdit ? (
951
1021
  <Input
@@ -979,7 +1049,7 @@ export default function SqlDashboardPage() {
979
1049
  );
980
1050
 
981
1051
  useSetHeaderActions(
982
- dashboard ? (
1052
+ reportScreenshot ? null : dashboard ? (
983
1053
  <>
984
1054
  <PresenceBar
985
1055
  activeUsers={activeUsers}
@@ -1088,6 +1158,20 @@ export default function SqlDashboardPage() {
1088
1158
  {(canEdit && !archivedAt) || canManage ? (
1089
1159
  <DropdownMenuSeparator />
1090
1160
  ) : null}
1161
+ {dashboardId ? (
1162
+ <>
1163
+ <DropdownMenuSeparator />
1164
+ <DropdownMenuItem
1165
+ onSelect={(event) => {
1166
+ event.preventDefault();
1167
+ setEmailReportOpen(true);
1168
+ }}
1169
+ >
1170
+ <IconMail className="mr-2 h-3.5 w-3.5" />
1171
+ {t("sqlDashboard.emailReports")}
1172
+ </DropdownMenuItem>
1173
+ </>
1174
+ ) : null}
1091
1175
  {canEdit && !archivedAt ? (
1092
1176
  <DropdownMenuItem
1093
1177
  onSelect={(event) => {
@@ -1116,6 +1200,15 @@ export default function SqlDashboardPage() {
1116
1200
  ) : null}
1117
1201
  </DropdownMenuContent>
1118
1202
  </DropdownMenu>
1203
+ {dashboardId ? (
1204
+ <EmailReportDialog
1205
+ open={emailReportOpen}
1206
+ onOpenChange={setEmailReportOpen}
1207
+ dashboardId={dashboardId}
1208
+ dashboardName={dashboard.name}
1209
+ filters={currentReportFilters}
1210
+ />
1211
+ ) : null}
1119
1212
  {canManage ? (
1120
1213
  <AlertDialog
1121
1214
  open={confirmDeleteOpen}
@@ -1163,7 +1256,11 @@ export default function SqlDashboardPage() {
1163
1256
  if (!dashboard) return <BlankDashboard />;
1164
1257
 
1165
1258
  return (
1166
- <div className="space-y-4">
1259
+ <div
1260
+ className="space-y-4"
1261
+ data-dashboard-report-capture
1262
+ data-dashboard-report-ready={loaded && dashboard ? "true" : "false"}
1263
+ >
1167
1264
  {hiddenAt ? (
1168
1265
  <div className="flex flex-wrap items-center gap-3 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-900 dark:border-amber-900/70 dark:bg-amber-950/40 dark:text-amber-200">
1169
1266
  <IconEyeOff className="h-4 w-4 shrink-0 text-amber-600 dark:text-amber-400" />
@@ -1320,8 +1417,7 @@ export default function SqlDashboardPage() {
1320
1417
  ) : (
1321
1418
  <DndContext
1322
1419
  sensors={sensors}
1323
- collisionDetection={pointerWithin}
1324
- onDragStart={canEdit ? handleDragStart : undefined}
1420
+ collisionDetection={dashboardCollisionDetection}
1325
1421
  onDragOver={canEdit ? handleDragOver : undefined}
1326
1422
  onDragEnd={canEdit ? handleDragEnd : undefined}
1327
1423
  onDragCancel={handleDragCancel}
@@ -1341,7 +1437,9 @@ export default function SqlDashboardPage() {
1341
1437
  },
1342
1438
  }
1343
1439
  : panel;
1344
- const remoteEditor = remoteEditingPanels.get(panel.id);
1440
+ const remoteEditor = reportScreenshot
1441
+ ? undefined
1442
+ : remoteEditingPanels.get(panel.id);
1345
1443
  return (
1346
1444
  <div
1347
1445
  key={panel.id}
@@ -1379,13 +1477,16 @@ export default function SqlDashboardPage() {
1379
1477
  onEdit={() => openEditPanel(panel)}
1380
1478
  onSaveSql={(sql) => handleSavePanel({ ...panel, sql })}
1381
1479
  editable={canEdit}
1480
+ eagerLoad={reportScreenshot}
1382
1481
  />
1383
1482
  </div>
1384
1483
  );
1385
1484
  };
1386
1485
 
1387
1486
  const renderSection = (section: SqlPanel) => {
1388
- const remoteEditor = remoteEditingPanels.get(section.id);
1487
+ const remoteEditor = reportScreenshot
1488
+ ? undefined
1489
+ : remoteEditingPanels.get(section.id);
1389
1490
  const resolved = section.config?.description
1390
1491
  ? {
1391
1492
  ...section,
@@ -1428,6 +1529,7 @@ export default function SqlDashboardPage() {
1428
1529
  onRemove={() => removePanel(section.id)}
1429
1530
  onEdit={() => openEditPanel(section)}
1430
1531
  editable={canEdit}
1532
+ eagerLoad={reportScreenshot}
1431
1533
  />
1432
1534
  </div>
1433
1535
  );
@@ -1438,7 +1540,7 @@ export default function SqlDashboardPage() {
1438
1540
  key={`row-slot-${group.key}-${rowIndex}`}
1439
1541
  slot={{ type: "row", groupKey: group.key, rowIndex }}
1440
1542
  activeSlot={activeDropSlot}
1441
- disabled={!canEdit || !draggingPanelId}
1543
+ disabled={!canEdit}
1442
1544
  />
1443
1545
  );
1444
1546
 
@@ -1447,15 +1549,6 @@ export default function SqlDashboardPage() {
1447
1549
  {group.section && renderSection(group.section)}
1448
1550
  {renderRowDropLine(0)}
1449
1551
  {group.rows.map((row, rowIndex) => {
1450
- const rowContainsDragging =
1451
- !!draggingPanelId &&
1452
- row.panels.some((panel) => panel.id === draggingPanelId);
1453
- const columnDropDisabled =
1454
- !canEdit ||
1455
- !draggingPanelId ||
1456
- (row.panels.length >= group.columns &&
1457
- !rowContainsDragging);
1458
-
1459
1552
  return (
1460
1553
  <Fragment key={row.key}>
1461
1554
  <div
@@ -1474,7 +1567,7 @@ export default function SqlDashboardPage() {
1474
1567
  columnIndex: 0,
1475
1568
  }}
1476
1569
  activeSlot={activeDropSlot}
1477
- disabled={columnDropDisabled}
1570
+ disabled={!canEdit}
1478
1571
  />
1479
1572
  {row.panels.map((panel, columnIndex) => (
1480
1573
  <Fragment key={panel.id}>
@@ -1487,7 +1580,7 @@ export default function SqlDashboardPage() {
1487
1580
  columnIndex: columnIndex + 1,
1488
1581
  }}
1489
1582
  activeSlot={activeDropSlot}
1490
- disabled={columnDropDisabled}
1583
+ disabled={!canEdit}
1491
1584
  />
1492
1585
  </Fragment>
1493
1586
  ))}
@@ -0,0 +1,26 @@
1
+ export function normalizeReportFilterSnapshot(
2
+ filters: Record<string, string>,
3
+ ): Record<string, string> {
4
+ const out: Record<string, string> = {};
5
+ for (const [key, value] of Object.entries(filters)) {
6
+ const k = key.trim();
7
+ const v = String(value ?? "").trim();
8
+ if (k && v) out[k] = v;
9
+ }
10
+ return out;
11
+ }
12
+
13
+ export function reportFilterSnapshotKey(
14
+ filters: Record<string, string>,
15
+ ): string {
16
+ return Object.keys(filters)
17
+ .sort()
18
+ .map((key) => `${key}:${filters[key]}`)
19
+ .join("\n");
20
+ }
21
+
22
+ export function savedReportFiltersForEdit(
23
+ savedFilters: Record<string, string>,
24
+ ): Record<string, string> {
25
+ return normalizeReportFilterSnapshot(savedFilters);
26
+ }