@agent-native/core 0.78.9 → 0.79.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 (176) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +11 -0
  3. package/corpus/core/docs/content/actions.md +4 -2
  4. package/corpus/core/docs/content/agent-surfaces.md +9 -0
  5. package/corpus/core/docs/content/extensions.md +10 -0
  6. package/corpus/core/docs/content/generative-ui.md +217 -0
  7. package/corpus/core/docs/content/key-concepts.md +2 -2
  8. package/corpus/core/docs/content/mcp-apps.md +3 -5
  9. package/corpus/core/docs/content/native-chat-ui.md +9 -5
  10. package/corpus/core/docs/content/tracking.md +9 -3
  11. package/corpus/core/package.json +1 -1
  12. package/corpus/core/src/action-ui.ts +1 -0
  13. package/corpus/core/src/client/analytics.ts +195 -12
  14. package/corpus/core/src/client/chat/index.ts +1 -0
  15. package/corpus/core/src/client/chat/tool-call-display.tsx +5 -2
  16. package/corpus/core/src/client/chat/widgets/InlineExtensionWidget.tsx +139 -0
  17. package/corpus/core/src/client/chat/widgets/builtin-tool-renderers.tsx +27 -1
  18. package/corpus/core/src/client/conversation/AgentConversation.tsx +38 -0
  19. package/corpus/core/src/client/conversation/code-agent-transcript.ts +9 -0
  20. package/corpus/core/src/client/conversation/types.ts +4 -0
  21. package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +27 -5
  22. package/corpus/core/src/client/extensions/ExtensionViewer.tsx +26 -5
  23. package/corpus/core/src/client/extensions/InlineExtensionFrame.tsx +654 -0
  24. package/corpus/core/src/client/extensions/iframe-bridge.ts +30 -0
  25. package/corpus/core/src/client/extensions/index.ts +5 -0
  26. package/corpus/core/src/client/index.ts +1 -0
  27. package/corpus/core/src/client/session-replay.ts +89 -14
  28. package/corpus/core/src/code-agents/transcript-normalizer.ts +14 -0
  29. package/corpus/core/src/extensions/actions.ts +218 -1
  30. package/corpus/core/src/extensions/html-shell.ts +70 -3
  31. package/corpus/core/src/extensions/routes.ts +12 -4
  32. package/corpus/core/src/index.browser.ts +1 -0
  33. package/corpus/core/src/index.ts +1 -0
  34. package/corpus/core/src/server/agent-chat-plugin.ts +21 -9
  35. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  36. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  37. package/corpus/core/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
  38. package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +198 -59
  39. package/corpus/templates/analytics/AGENTS.md +12 -0
  40. package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +1105 -0
  41. package/corpus/templates/analytics/actions/dashboard-panel-order.ts +169 -0
  42. package/corpus/templates/analytics/actions/get-sql-dashboard.ts +89 -6
  43. package/corpus/templates/analytics/actions/mutate-dashboard.ts +333 -0
  44. package/corpus/templates/analytics/actions/reorder-dashboard-panels.ts +214 -0
  45. package/corpus/templates/analytics/actions/send-dashboard-report-now.ts +48 -21
  46. package/corpus/templates/analytics/actions/update-dashboard.ts +193 -100
  47. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +11 -0
  48. package/corpus/templates/analytics/app/components/layout/Sidebar.tsx +1 -1
  49. package/corpus/templates/analytics/app/global.css +81 -10
  50. package/corpus/templates/analytics/app/i18n-data.ts +249 -4
  51. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/ChartCard.tsx +3 -3
  52. package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +41 -1
  53. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/EmailReportDialog.tsx +1 -1
  54. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +4 -4
  55. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +48 -18
  56. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-layout.ts +32 -0
  57. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +204 -70
  58. package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +1101 -227
  59. package/corpus/templates/analytics/app/pages/sessions/SessionsPage.tsx +160 -133
  60. package/corpus/templates/analytics/changelog/2026-06-26-agents-can-now-edit-dashboards-through-a-typed-mutation-scri.md +6 -0
  61. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-bar-chart-hovers-stay-subtle-in-dark-mode.md +6 -0
  62. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-chart-moves-are-now-handled-by-panel-id-making-sim.md +6 -0
  63. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-drag-smoothly-with-stable-cards-inactive-ch.md +6 -0
  64. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-charts-show-a-refresh-spinner-while-keeping-existi.md +6 -0
  65. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-edits-stay-stable-while-chart-order-and-deletions-.md +6 -0
  66. package/corpus/templates/analytics/changelog/2026-06-26-dashboard-email-reports-queue-immediate-sends.md +6 -0
  67. package/corpus/templates/analytics/changelog/2026-06-26-explorer-dashboards-now-drag-charts-with-a-stable-preview-an.md +6 -0
  68. package/corpus/templates/analytics/changelog/2026-06-26-session-replay-playback-can-now-be-toggled-by-clicking-the-r.md +6 -0
  69. package/corpus/templates/analytics/changelog/2026-06-26-session-replays-now-open-from-the-full-row-and-play-with-tim.md +6 -0
  70. package/corpus/templates/analytics/server/lib/dashboard-report-subscriptions.ts +21 -0
  71. package/corpus/templates/analytics/server/lib/dashboard-report.ts +41 -17
  72. package/corpus/templates/analytics/server/plugins/agent-chat.ts +5 -0
  73. package/dist/action-ui.d.ts +1 -0
  74. package/dist/action-ui.d.ts.map +1 -1
  75. package/dist/action-ui.js +1 -0
  76. package/dist/action-ui.js.map +1 -1
  77. package/dist/client/analytics.d.ts.map +1 -1
  78. package/dist/client/analytics.js +173 -12
  79. package/dist/client/analytics.js.map +1 -1
  80. package/dist/client/chat/index.d.ts +1 -1
  81. package/dist/client/chat/index.d.ts.map +1 -1
  82. package/dist/client/chat/index.js +1 -1
  83. package/dist/client/chat/index.js.map +1 -1
  84. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  85. package/dist/client/chat/tool-call-display.js +3 -1
  86. package/dist/client/chat/tool-call-display.js.map +1 -1
  87. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts +17 -0
  88. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts.map +1 -0
  89. package/dist/client/chat/widgets/InlineExtensionWidget.js +85 -0
  90. package/dist/client/chat/widgets/InlineExtensionWidget.js.map +1 -0
  91. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts.map +1 -1
  92. package/dist/client/chat/widgets/builtin-tool-renderers.js +15 -2
  93. package/dist/client/chat/widgets/builtin-tool-renderers.js.map +1 -1
  94. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  95. package/dist/client/conversation/AgentConversation.js +33 -0
  96. package/dist/client/conversation/AgentConversation.js.map +1 -1
  97. package/dist/client/conversation/code-agent-transcript.js +8 -0
  98. package/dist/client/conversation/code-agent-transcript.js.map +1 -1
  99. package/dist/client/conversation/types.d.ts +4 -0
  100. package/dist/client/conversation/types.d.ts.map +1 -1
  101. package/dist/client/conversation/types.js.map +1 -1
  102. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  103. package/dist/client/extensions/EmbeddedExtension.js +29 -1
  104. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  105. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  106. package/dist/client/extensions/ExtensionViewer.js +28 -1
  107. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  108. package/dist/client/extensions/InlineExtensionFrame.d.ts +25 -0
  109. package/dist/client/extensions/InlineExtensionFrame.d.ts.map +1 -0
  110. package/dist/client/extensions/InlineExtensionFrame.js +488 -0
  111. package/dist/client/extensions/InlineExtensionFrame.js.map +1 -0
  112. package/dist/client/extensions/iframe-bridge.d.ts +2 -0
  113. package/dist/client/extensions/iframe-bridge.d.ts.map +1 -1
  114. package/dist/client/extensions/iframe-bridge.js +23 -0
  115. package/dist/client/extensions/iframe-bridge.js.map +1 -1
  116. package/dist/client/extensions/index.d.ts +1 -0
  117. package/dist/client/extensions/index.d.ts.map +1 -1
  118. package/dist/client/extensions/index.js +1 -0
  119. package/dist/client/extensions/index.js.map +1 -1
  120. package/dist/client/index.d.ts +1 -1
  121. package/dist/client/index.d.ts.map +1 -1
  122. package/dist/client/index.js +1 -1
  123. package/dist/client/index.js.map +1 -1
  124. package/dist/client/session-replay.d.ts +11 -1
  125. package/dist/client/session-replay.d.ts.map +1 -1
  126. package/dist/client/session-replay.js +74 -14
  127. package/dist/client/session-replay.js.map +1 -1
  128. package/dist/code-agents/transcript-normalizer.d.ts +2 -0
  129. package/dist/code-agents/transcript-normalizer.d.ts.map +1 -1
  130. package/dist/code-agents/transcript-normalizer.js +8 -0
  131. package/dist/code-agents/transcript-normalizer.js.map +1 -1
  132. package/dist/collab/awareness.d.ts +2 -2
  133. package/dist/collab/awareness.d.ts.map +1 -1
  134. package/dist/collab/routes.d.ts +1 -1
  135. package/dist/extensions/actions.d.ts.map +1 -1
  136. package/dist/extensions/actions.js +200 -1
  137. package/dist/extensions/actions.js.map +1 -1
  138. package/dist/extensions/html-shell.d.ts.map +1 -1
  139. package/dist/extensions/html-shell.js +70 -3
  140. package/dist/extensions/html-shell.js.map +1 -1
  141. package/dist/extensions/routes.d.ts.map +1 -1
  142. package/dist/extensions/routes.js +11 -4
  143. package/dist/extensions/routes.js.map +1 -1
  144. package/dist/index.browser.d.ts +1 -1
  145. package/dist/index.browser.d.ts.map +1 -1
  146. package/dist/index.browser.js +1 -1
  147. package/dist/index.browser.js.map +1 -1
  148. package/dist/index.d.ts +1 -1
  149. package/dist/index.d.ts.map +1 -1
  150. package/dist/index.js +1 -1
  151. package/dist/index.js.map +1 -1
  152. package/dist/notifications/routes.d.ts +2 -2
  153. package/dist/observability/routes.d.ts +3 -3
  154. package/dist/progress/routes.d.ts +1 -1
  155. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  156. package/dist/resources/handlers.d.ts +3 -3
  157. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  158. package/dist/server/agent-chat-plugin.js +21 -9
  159. package/dist/server/agent-chat-plugin.js.map +1 -1
  160. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  161. package/dist/server/transcribe-voice.d.ts +1 -1
  162. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  163. package/dist/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  164. package/dist/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
  165. package/docs/content/actions.md +4 -2
  166. package/docs/content/agent-surfaces.md +9 -0
  167. package/docs/content/extensions.md +10 -0
  168. package/docs/content/generative-ui.md +217 -0
  169. package/docs/content/key-concepts.md +2 -2
  170. package/docs/content/mcp-apps.md +3 -5
  171. package/docs/content/native-chat-ui.md +9 -5
  172. package/docs/content/tracking.md +9 -3
  173. package/package.json +1 -1
  174. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +13 -0
  175. package/src/templates/workspace-core/.agents/skills/extensions/references/api.md +2 -0
  176. package/src/templates/workspace-core/.agents/skills/generative-ui/SKILL.md +115 -0
@@ -1,6 +1,5 @@
1
1
  import { useT } from "@agent-native/core/client";
2
2
  import { useDraggable } from "@dnd-kit/core";
3
- import { CSS } from "@dnd-kit/utilities";
4
3
  import {
5
4
  IconGripVertical,
6
5
  IconDotsVertical,
@@ -11,8 +10,8 @@ import {
11
10
  IconCode,
12
11
  IconDownload,
13
12
  } from "@tabler/icons-react";
14
- import { useQueryClient } from "@tanstack/react-query";
15
- import { useCallback, useEffect, useRef, useState } from "react";
13
+ import { useIsFetching, useQueryClient } from "@tanstack/react-query";
14
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
16
15
 
17
16
  import { ChartFillHeight, SqlChart } from "@/components/dashboard/SqlChart";
18
17
  import {
@@ -39,6 +38,7 @@ import {
39
38
  DropdownMenuSeparator,
40
39
  DropdownMenuTrigger,
41
40
  } from "@/components/ui/dropdown-menu";
41
+ import { Spinner } from "@/components/ui/spinner";
42
42
  import {
43
43
  Tooltip,
44
44
  TooltipContent,
@@ -72,8 +72,10 @@ export function SqlChartCard({
72
72
  }: SqlChartCardProps) {
73
73
  const t = useT();
74
74
  const queryClient = useQueryClient();
75
- const { attributes, listeners, setNodeRef, transform, isDragging } =
76
- useDraggable({ id: panel.id, disabled: !editable });
75
+ const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
76
+ id: panel.id,
77
+ disabled: !editable,
78
+ });
77
79
 
78
80
  const [confirmOpen, setConfirmOpen] = useState(false);
79
81
  const [expanded, setExpanded] = useState(false);
@@ -82,6 +84,20 @@ export function SqlChartCard({
82
84
  eagerLoad || panel.chartType === "section",
83
85
  );
84
86
  const cardRef = useRef<HTMLDivElement | null>(null);
87
+ const chartQueryKey = useMemo(
88
+ () =>
89
+ [
90
+ "sql-chart",
91
+ panel.id,
92
+ serializePanelSql(resolvedSql ?? panel.sql),
93
+ panel.source,
94
+ ] as const,
95
+ [panel.id, panel.source, panel.sql, resolvedSql],
96
+ );
97
+ const chartFetchCount = useIsFetching({ queryKey: chartQueryKey });
98
+ const chartHasCachedData =
99
+ queryClient.getQueryData(chartQueryKey) !== undefined;
100
+ const isChartRefreshing = chartHasCachedData && chartFetchCount > 0;
85
101
 
86
102
  const setCardNodeRef = useCallback(
87
103
  (node: HTMLDivElement | null) => {
@@ -98,14 +114,9 @@ export function SqlChartCard({
98
114
  const handleRefresh = useCallback(() => {
99
115
  setShouldLoadData(true);
100
116
  void queryClient.invalidateQueries({
101
- queryKey: [
102
- "sql-chart",
103
- panel.id,
104
- serializePanelSql(resolvedSql ?? panel.sql),
105
- panel.source,
106
- ],
117
+ queryKey: chartQueryKey,
107
118
  });
108
- }, [panel.id, panel.source, panel.sql, queryClient, resolvedSql]);
119
+ }, [chartQueryKey, queryClient]);
109
120
 
110
121
  useEffect(() => {
111
122
  if (eagerLoad) {
@@ -145,9 +156,7 @@ export function SqlChartCard({
145
156
  }, [panel.id]);
146
157
 
147
158
  const style = {
148
- transform: CSS.Transform.toString(transform),
149
159
  zIndex: isDragging ? 50 : undefined,
150
- opacity: isDragging ? 0.7 : 1,
151
160
  };
152
161
 
153
162
  // Section panels render as a flush header row (no card chrome, full width)
@@ -158,7 +167,8 @@ export function SqlChartCard({
158
167
  <div
159
168
  ref={setCardNodeRef}
160
169
  style={style}
161
- className="group relative mt-2 first:mt-0"
170
+ data-dragging={isDragging ? "true" : undefined}
171
+ className="dashboard-section-card group relative mt-2 first:mt-0"
162
172
  >
163
173
  <div className="flex items-center gap-2 border-b border-border pb-2">
164
174
  <h2 className="text-base font-semibold flex-1">{panel.title}</h2>
@@ -261,14 +271,34 @@ export function SqlChartCard({
261
271
  <div
262
272
  ref={setCardNodeRef}
263
273
  style={style}
264
- className="group relative h-full hover:z-20 focus-within:z-20"
274
+ data-dragging={isDragging ? "true" : undefined}
275
+ className="dashboard-chart-card group relative h-full hover:z-20 focus-within:z-20"
265
276
  >
266
277
  <Card className="flex h-full flex-col overflow-visible">
267
278
  <CardHeader className="pb-2 flex flex-row items-center gap-2 shrink-0">
268
279
  <CardTitle className="text-sm font-medium flex-1 truncate">
269
280
  {panel.title}
270
281
  </CardTitle>
271
- <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100">
282
+ <div
283
+ className={`flex items-center gap-1 transition-opacity ${
284
+ isChartRefreshing
285
+ ? "opacity-100"
286
+ : "opacity-0 group-hover:opacity-100 focus-within:opacity-100"
287
+ }`}
288
+ >
289
+ {isChartRefreshing ? (
290
+ <Tooltip>
291
+ <TooltipTrigger asChild>
292
+ <span className="inline-flex size-6 items-center justify-center rounded text-muted-foreground">
293
+ <Spinner
294
+ className="size-3.5"
295
+ aria-label={t("sqlDashboard.refreshing")}
296
+ />
297
+ </span>
298
+ </TooltipTrigger>
299
+ <TooltipContent>{t("sqlDashboard.refreshing")}</TooltipContent>
300
+ </Tooltip>
301
+ ) : null}
272
302
  {showPanelMenu ? (
273
303
  <DropdownMenu>
274
304
  <Tooltip>
@@ -363,7 +393,7 @@ export function SqlChartCard({
363
393
  ) : null}
364
394
  </div>
365
395
  </CardHeader>
366
- <CardContent className="flex flex-1 flex-col overflow-visible pt-0">
396
+ <CardContent className="dashboard-chart-content flex flex-1 flex-col overflow-visible pt-0">
367
397
  <SqlChart
368
398
  panel={panel}
369
399
  resolvedSql={resolvedSql}
@@ -26,10 +26,42 @@ export type DashboardDropSlot =
26
26
  columnIndex: number;
27
27
  };
28
28
 
29
+ export type DashboardPointerCoordinates = {
30
+ x: number;
31
+ y: number;
32
+ };
33
+
34
+ export type DashboardClientRect = {
35
+ left: number;
36
+ right: number;
37
+ top: number;
38
+ bottom: number;
39
+ };
40
+
29
41
  function rowKey(panels: SqlPanel[], index: number): string {
30
42
  return panels.map((panel) => panel.id).join(":") || `empty-${index}`;
31
43
  }
32
44
 
45
+ export function distanceFromPointerToRect(
46
+ pointer: DashboardPointerCoordinates,
47
+ rect: DashboardClientRect,
48
+ ): number {
49
+ const dx =
50
+ pointer.x < rect.left
51
+ ? rect.left - pointer.x
52
+ : pointer.x > rect.right
53
+ ? pointer.x - rect.right
54
+ : 0;
55
+ const dy =
56
+ pointer.y < rect.top
57
+ ? rect.top - pointer.y
58
+ : pointer.y > rect.bottom
59
+ ? pointer.y - rect.bottom
60
+ : 0;
61
+
62
+ return Math.hypot(dx, dy);
63
+ }
64
+
33
65
  export function rebalanceRowWidths(
34
66
  panels: SqlPanel[],
35
67
  columns: number,
@@ -14,16 +14,18 @@ import {
14
14
  type CollabUser,
15
15
  } from "@agent-native/core/client";
16
16
  import {
17
- closestCenter,
18
17
  useDroppable,
19
18
  DndContext,
19
+ DragOverlay,
20
20
  pointerWithin,
21
21
  PointerSensor,
22
22
  useSensor,
23
23
  useSensors,
24
24
  type DragEndEvent,
25
25
  type DragOverEvent,
26
+ type DragStartEvent,
26
27
  type CollisionDetection,
28
+ type Collision,
27
29
  } from "@dnd-kit/core";
28
30
  import {
29
31
  IconArchive,
@@ -31,6 +33,7 @@ import {
31
33
  IconDots,
32
34
  IconEye,
33
35
  IconEyeOff,
36
+ IconGripVertical,
34
37
  IconInfoCircle,
35
38
  IconMail,
36
39
  IconPencil,
@@ -42,6 +45,7 @@ import {
42
45
  import { useQuery, useQueryClient } from "@tanstack/react-query";
43
46
  import {
44
47
  Fragment,
48
+ memo,
45
49
  useState,
46
50
  useEffect,
47
51
  useCallback,
@@ -102,6 +106,7 @@ import BlankDashboard from "../BlankDashboard";
102
106
  import { DashboardSkeleton } from "../DashboardSkeleton";
103
107
  import {
104
108
  buildDashboardPanelGroups,
109
+ distanceFromPointerToRect,
105
110
  dropSlotId,
106
111
  isDropSlotAvailable,
107
112
  movePanelToDropSlot,
@@ -189,6 +194,100 @@ function DashboardDropLine({
189
194
  );
190
195
  }
191
196
 
197
+ function DashboardDragPreview({ panel }: { panel: SqlPanel | null }) {
198
+ if (!panel) return null;
199
+
200
+ return (
201
+ <div className="dashboard-drag-preview flex max-w-64 items-center gap-2 rounded-md border bg-background/95 px-3 py-2 text-sm font-medium text-foreground shadow-lg ring-1 ring-primary/20">
202
+ <IconGripVertical className="h-4 w-4 shrink-0 text-muted-foreground" />
203
+ <span className="truncate">{panel.title}</span>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ /**
209
+ * A single chart cell, memoized so that drag interactions — which re-render the
210
+ * dashboard page on every drop-slot change — do NOT re-render every chart's
211
+ * Recharts subtree. During a drag the panel, vars, remoteEditor, and the
212
+ * stable callbacks below don't change, so React skips these cells entirely and
213
+ * only the lightweight drop-line indicators update. This keeps dragging smooth
214
+ * on dense dashboards. Outside a drag, prop changes (filter/vars edits, remote
215
+ * collaborator highlights, panel edits) still re-render normally.
216
+ */
217
+ const PanelCell = memo(function PanelCell({
218
+ panel,
219
+ vars,
220
+ remoteEditor,
221
+ editable,
222
+ eagerLoad,
223
+ onRemovePanel,
224
+ onEditPanel,
225
+ onSavePanel,
226
+ }: {
227
+ panel: SqlPanel;
228
+ vars: Record<string, string>;
229
+ remoteEditor: { color: string; name: string } | undefined;
230
+ editable: boolean;
231
+ eagerLoad: boolean;
232
+ onRemovePanel: (panelId: string) => void;
233
+ onEditPanel: (panel: SqlPanel) => void;
234
+ onSavePanel: (panel: SqlPanel) => Promise<void>;
235
+ }) {
236
+ const resolved = useMemo(
237
+ () =>
238
+ panel.config?.description
239
+ ? {
240
+ ...panel,
241
+ config: {
242
+ ...panel.config,
243
+ description: interpolate(panel.config.description, vars),
244
+ },
245
+ }
246
+ : panel,
247
+ [panel, vars],
248
+ );
249
+ const resolvedSql = useMemo(
250
+ () => interpolate(serializePanelSql(panel.sql), vars),
251
+ [panel.sql, vars],
252
+ );
253
+
254
+ return (
255
+ <div
256
+ className="dashboard-grid-cell relative h-full"
257
+ style={
258
+ remoteEditor
259
+ ? {
260
+ outline: `2px solid ${remoteEditor.color}`,
261
+ outlineOffset: 2,
262
+ borderRadius: 8,
263
+ }
264
+ : undefined
265
+ }
266
+ >
267
+ {remoteEditor && (
268
+ <span
269
+ className="absolute -top-2.5 left-3 px-1.5 text-[10px] font-medium rounded z-10"
270
+ style={{
271
+ backgroundColor: remoteEditor.color,
272
+ color: "#fff",
273
+ }}
274
+ >
275
+ {remoteEditor.name}
276
+ </span>
277
+ )}
278
+ <SqlChartCard
279
+ panel={resolved}
280
+ resolvedSql={resolvedSql}
281
+ onRemove={() => onRemovePanel(panel.id)}
282
+ onEdit={() => onEditPanel(panel)}
283
+ onSaveSql={(sql) => onSavePanel({ ...panel, sql })}
284
+ editable={editable}
285
+ eagerLoad={eagerLoad}
286
+ />
287
+ </div>
288
+ );
289
+ });
290
+
192
291
  type FetchedDashboard = {
193
292
  id: string;
194
293
  config: SqlDashboardConfig;
@@ -237,7 +336,7 @@ async function fetchDashboard(id: string): Promise<FetchedDashboard | null> {
237
336
  try {
238
337
  const data: any = await callAction(
239
338
  "get-sql-dashboard",
240
- { id },
339
+ { id, includeConfig: true },
241
340
  { method: "GET" },
242
341
  );
243
342
  if (!data || data.error) return null;
@@ -319,6 +418,9 @@ export default function SqlDashboardPage() {
319
418
  const [emailReportOpen, setEmailReportOpen] = useState(false);
320
419
  const [activeDropSlot, setActiveDropSlot] =
321
420
  useState<DashboardDropSlot | null>(null);
421
+ const [activeDragPanelId, setActiveDragPanelId] = useState<string | null>(
422
+ null,
423
+ );
322
424
  const viewedDashboardIdRef = useRef<string | null>(null);
323
425
  const canEdit = !reportScreenshot && resourceCanEdit(resourceAccess);
324
426
  const canManage = !reportScreenshot && resourceCanManage(resourceAccess);
@@ -636,6 +738,24 @@ export default function SqlDashboardPage() {
636
738
  [collabDocId],
637
739
  );
638
740
 
741
+ const updateCachedDashboardConfig = useCallback(
742
+ (updated: SqlDashboardConfig) => {
743
+ if (!dashboardId) return;
744
+ queryClient.setQueriesData<FetchedDashboard | null>(
745
+ { queryKey: ["data", "sql-dashboard", dashboardId] },
746
+ (prev) => (prev ? { ...prev, config: updated } : prev),
747
+ );
748
+ queryClient.setQueryData<PrefetchSnapshot<FetchedDashboard | null>>(
749
+ sqlDashboardPrefetchKey(dashboardId),
750
+ (prev) =>
751
+ prev?.data
752
+ ? { ...prev, data: { ...prev.data, config: updated } }
753
+ : prev,
754
+ );
755
+ },
756
+ [dashboardId, queryClient],
757
+ );
758
+
639
759
  /**
640
760
  * Persist without throwing — background save used for drag reorder, width
641
761
  * toggle, title/description edits, and panel delete. If the save fails
@@ -650,6 +770,7 @@ export default function SqlDashboardPage() {
650
770
  return;
651
771
  }
652
772
  setDashboard(updated);
773
+ updateCachedDashboardConfig(updated);
653
774
  pushToCollab(updated);
654
775
  saveDashboard(dashboardId, updated)
655
776
  .then(() => {
@@ -676,7 +797,14 @@ export default function SqlDashboardPage() {
676
797
  );
677
798
  });
678
799
  },
679
- [dashboardId, canEdit, queryClient, pushToCollab, t],
800
+ [
801
+ dashboardId,
802
+ canEdit,
803
+ queryClient,
804
+ pushToCollab,
805
+ t,
806
+ updateCachedDashboardConfig,
807
+ ],
680
808
  );
681
809
 
682
810
  /**
@@ -691,6 +819,7 @@ export default function SqlDashboardPage() {
691
819
  }
692
820
  await saveDashboard(dashboardId, updated);
693
821
  setDashboard(updated);
822
+ updateCachedDashboardConfig(updated);
694
823
  pushToCollab(updated);
695
824
  queryClient.removeQueries({
696
825
  queryKey: sqlDashboardPrefetchKey(dashboardId),
@@ -701,7 +830,14 @@ export default function SqlDashboardPage() {
701
830
  queryKey: ["data", "sql-dashboard", dashboardId],
702
831
  });
703
832
  },
704
- [dashboardId, canEdit, queryClient, pushToCollab, t],
833
+ [
834
+ dashboardId,
835
+ canEdit,
836
+ queryClient,
837
+ pushToCollab,
838
+ t,
839
+ updateCachedDashboardConfig,
840
+ ],
705
841
  );
706
842
 
707
843
  const removePanel = useCallback(
@@ -756,13 +892,22 @@ export default function SqlDashboardPage() {
756
892
  useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
757
893
  );
758
894
 
895
+ const handleDragStart = useCallback((event: DragStartEvent) => {
896
+ setActiveDragPanelId(String(event.active.id));
897
+ }, []);
898
+
759
899
  const handleDragOver = useCallback((event: DragOverEvent) => {
760
- setActiveDropSlot(readDropSlot(event.over?.data.current));
900
+ const nextSlot = readDropSlot(event.over?.data.current);
901
+ setActiveDropSlot((currentSlot) => {
902
+ if (!nextSlot) return currentSlot === null ? currentSlot : null;
903
+ return sameDropSlot(currentSlot, nextSlot) ? currentSlot : nextSlot;
904
+ });
761
905
  }, []);
762
906
 
763
907
  const handleDragEnd = useCallback(
764
908
  (event: DragEndEvent) => {
765
909
  setActiveDropSlot(null);
910
+ setActiveDragPanelId(null);
766
911
  if (!dashboard) return;
767
912
  const slot = readDropSlot(event.over?.data.current);
768
913
  if (!slot) return;
@@ -784,6 +929,7 @@ export default function SqlDashboardPage() {
784
929
 
785
930
  const handleDragCancel = useCallback(() => {
786
931
  setActiveDropSlot(null);
932
+ setActiveDragPanelId(null);
787
933
  }, []);
788
934
 
789
935
  const handleSaveName = useCallback(() => {
@@ -889,6 +1035,14 @@ export default function SqlDashboardPage() {
889
1035
  const panelGroups = useMemo(() => {
890
1036
  return buildDashboardPanelGroups(visiblePanels, dashboardColumns);
891
1037
  }, [visiblePanels, dashboardColumns]);
1038
+ const activeDragPanel = useMemo(
1039
+ () =>
1040
+ activeDragPanelId
1041
+ ? (visiblePanels.find((panel) => panel.id === activeDragPanelId) ??
1042
+ null)
1043
+ : null,
1044
+ [activeDragPanelId, visiblePanels],
1045
+ );
892
1046
 
893
1047
  const dashboardCollisionDetection = useCallback<CollisionDetection>(
894
1048
  (args) => {
@@ -910,9 +1064,28 @@ export default function SqlDashboardPage() {
910
1064
  });
911
1065
  if (exactCollisions.length > 0) return exactCollisions;
912
1066
 
913
- return closestCenter({
914
- ...args,
915
- droppableContainers,
1067
+ if (!args.pointerCoordinates) return [];
1068
+
1069
+ const collisions: Collision[] = [];
1070
+ for (const droppableContainer of droppableContainers) {
1071
+ const rect = args.droppableRects.get(droppableContainer.id);
1072
+ if (!rect) continue;
1073
+
1074
+ collisions.push({
1075
+ id: droppableContainer.id,
1076
+ data: {
1077
+ droppableContainer,
1078
+ value: distanceFromPointerToRect(args.pointerCoordinates, rect),
1079
+ },
1080
+ });
1081
+ }
1082
+
1083
+ return collisions.sort((a, b) => {
1084
+ const aValue =
1085
+ typeof a.data?.value === "number" ? a.data.value : Number.MAX_VALUE;
1086
+ const bValue =
1087
+ typeof b.data?.value === "number" ? b.data.value : Number.MAX_VALUE;
1088
+ return aValue - bValue;
916
1089
  });
917
1090
  },
918
1091
  [panelGroups],
@@ -1418,71 +1591,16 @@ export default function SqlDashboardPage() {
1418
1591
  <DndContext
1419
1592
  sensors={sensors}
1420
1593
  collisionDetection={dashboardCollisionDetection}
1594
+ onDragStart={canEdit ? handleDragStart : undefined}
1421
1595
  onDragOver={canEdit ? handleDragOver : undefined}
1422
1596
  onDragEnd={canEdit ? handleDragEnd : undefined}
1423
1597
  onDragCancel={handleDragCancel}
1424
1598
  >
1425
- <div className="dashboard-grid-container flex flex-col gap-1">
1599
+ <div
1600
+ className="dashboard-grid-container flex flex-col gap-1"
1601
+ data-dashboard-dragging={activeDragPanel ? "true" : undefined}
1602
+ >
1426
1603
  {panelGroups.map((group) => {
1427
- const renderPanelCell = (panel: SqlPanel) => {
1428
- const resolved = panel.config?.description
1429
- ? {
1430
- ...panel,
1431
- config: {
1432
- ...panel.config,
1433
- description: interpolate(
1434
- panel.config.description,
1435
- vars,
1436
- ),
1437
- },
1438
- }
1439
- : panel;
1440
- const remoteEditor = reportScreenshot
1441
- ? undefined
1442
- : remoteEditingPanels.get(panel.id);
1443
- return (
1444
- <div
1445
- key={panel.id}
1446
- className="dashboard-grid-cell relative h-full"
1447
- style={
1448
- {
1449
- ...(remoteEditor
1450
- ? {
1451
- outline: `2px solid ${remoteEditor.color}`,
1452
- outlineOffset: 2,
1453
- borderRadius: 8,
1454
- }
1455
- : null),
1456
- } as React.CSSProperties
1457
- }
1458
- >
1459
- {remoteEditor && (
1460
- <span
1461
- className="absolute -top-2.5 left-3 px-1.5 text-[10px] font-medium rounded z-10"
1462
- style={{
1463
- backgroundColor: remoteEditor.color,
1464
- color: "#fff",
1465
- }}
1466
- >
1467
- {remoteEditor.name}
1468
- </span>
1469
- )}
1470
- <SqlChartCard
1471
- panel={resolved}
1472
- resolvedSql={interpolate(
1473
- serializePanelSql(panel.sql),
1474
- vars,
1475
- )}
1476
- onRemove={() => removePanel(panel.id)}
1477
- onEdit={() => openEditPanel(panel)}
1478
- onSaveSql={(sql) => handleSavePanel({ ...panel, sql })}
1479
- editable={canEdit}
1480
- eagerLoad={reportScreenshot}
1481
- />
1482
- </div>
1483
- );
1484
- };
1485
-
1486
1604
  const renderSection = (section: SqlPanel) => {
1487
1605
  const remoteEditor = reportScreenshot
1488
1606
  ? undefined
@@ -1501,7 +1619,7 @@ export default function SqlDashboardPage() {
1501
1619
  : section;
1502
1620
  return (
1503
1621
  <div
1504
- className="relative"
1622
+ className="dashboard-section-cell relative"
1505
1623
  style={
1506
1624
  remoteEditor
1507
1625
  ? {
@@ -1571,7 +1689,20 @@ export default function SqlDashboardPage() {
1571
1689
  />
1572
1690
  {row.panels.map((panel, columnIndex) => (
1573
1691
  <Fragment key={panel.id}>
1574
- {renderPanelCell(panel)}
1692
+ <PanelCell
1693
+ panel={panel}
1694
+ vars={vars}
1695
+ remoteEditor={
1696
+ reportScreenshot
1697
+ ? undefined
1698
+ : remoteEditingPanels.get(panel.id)
1699
+ }
1700
+ editable={canEdit}
1701
+ eagerLoad={reportScreenshot}
1702
+ onRemovePanel={removePanel}
1703
+ onEditPanel={openEditPanel}
1704
+ onSavePanel={handleSavePanel}
1705
+ />
1575
1706
  <DashboardDropLine
1576
1707
  slot={{
1577
1708
  type: "column",
@@ -1593,6 +1724,9 @@ export default function SqlDashboardPage() {
1593
1724
  );
1594
1725
  })}
1595
1726
  </div>
1727
+ <DragOverlay adjustScale={false} dropAnimation={null} zIndex={1000}>
1728
+ <DashboardDragPreview panel={activeDragPanel} />
1729
+ </DragOverlay>
1596
1730
  </DndContext>
1597
1731
  )}
1598
1732