@agent-native/core-corpus 0.201.0 → 0.201.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 (40) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/CHANGELOG.md +6 -0
  3. package/corpus/templates/analytics/actions/save-dashboard-view.ts +10 -1
  4. package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +136 -4
  5. package/corpus/templates/analytics/app/hooks/use-dashboard-views.ts +4 -0
  6. package/corpus/templates/analytics/app/i18n/en-US.ts +7 -0
  7. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +86 -23
  8. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +4 -3
  9. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +55 -1
  10. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewsMenu.tsx +44 -7
  11. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-filter-restore.ts +196 -0
  12. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +2 -0
  13. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +158 -47
  14. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
  15. package/corpus/templates/analytics/changelog/2026-10-05-custom-date-controls-stay-beside-the-range-selector-and-dash.md +6 -0
  16. package/corpus/templates/analytics/server/db/schema.ts +20 -8
  17. package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +50 -3
  18. package/corpus/templates/analytics/server/lib/dashboard-view-remap.ts +52 -0
  19. package/corpus/templates/analytics/server/lib/dashboards-store.ts +77 -42
  20. package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +23 -8
  21. package/corpus/templates/analytics/server/plugins/db.ts +7 -0
  22. package/corpus/templates/clips/CHANGELOG.md +4 -0
  23. package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
  24. package/corpus/templates/clips/changelog/2026-10-05-the-agent-composer-is-easier-to-distinguish-from-the-shared-.md +6 -0
  25. package/corpus/templates/design/CHANGELOG.md +1 -0
  26. package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +1 -1
  27. package/corpus/templates/design/changelog/2026-10-05-design-frame-labels-apply-truncation-as-soon-as-resizing-end.md +6 -0
  28. package/corpus/templates/slides/CHANGELOG.md +7 -0
  29. package/corpus/templates/slides/agent-native.app-skill.json +2 -2
  30. package/corpus/templates/slides/app/components/home/SlidesHomeLibrary.tsx +11 -11
  31. package/corpus/templates/slides/app/context/DeckContext.tsx +16 -12
  32. package/corpus/templates/slides/app/hooks/use-new-deck-generation.ts +129 -6
  33. package/corpus/templates/slides/app/lib/create-deck-generation.ts +4 -1
  34. package/corpus/templates/slides/app/pages/DeckEditor.tsx +25 -8
  35. package/corpus/templates/slides/app/pages/Index.tsx +85 -42
  36. package/corpus/templates/slides/changelog/2026-10-05-file-uploads-explain-the-free-builder-io-option-and-how-to-c.md +6 -0
  37. package/corpus/templates/slides/changelog/2026-10-05-slides-restores-pending-generation-questions-when-reopened.md +6 -0
  38. package/corpus/templates/slides/changelog/2026-10-05-the-home-page-shows-both-library-tabs-and-starter-prompts-im.md +6 -0
  39. package/corpus/templates/slides/changelog/2026-10-05-undo-stops-at-generated-deck.md +6 -0
  40. package/package.json +1 -1
@@ -5,7 +5,7 @@ import {
5
5
  IconTrash,
6
6
  IconLayoutGrid,
7
7
  } from "@tabler/icons-react";
8
- import { useMemo, useState } from "react";
8
+ import { useId, useMemo, useState } from "react";
9
9
  import { useSearchParams } from "react-router";
10
10
  import { toast } from "sonner";
11
11
 
@@ -20,7 +20,9 @@ import {
20
20
  AlertDialogHeader,
21
21
  AlertDialogTitle,
22
22
  } from "@/components/ui/alert-dialog";
23
+ import { Badge } from "@/components/ui/badge";
23
24
  import { Button } from "@/components/ui/button";
25
+ import { Checkbox } from "@/components/ui/checkbox";
24
26
  import {
25
27
  Dialog,
26
28
  DialogContent,
@@ -47,7 +49,7 @@ import {
47
49
  type DashboardView,
48
50
  } from "@/hooks/use-dashboard-views";
49
51
 
50
- import { FILTER_PARAM_PREFIX } from "./DashboardFilterBar";
52
+ import { FILTER_PARAM_PREFIX } from "./filter-vars";
51
53
 
52
54
  interface ViewsMenuProps {
53
55
  dashboardId: string;
@@ -98,8 +100,10 @@ export function ViewsMenu({ dashboardId, canEdit = true }: ViewsMenuProps) {
98
100
  const [menuOpen, setMenuOpen] = useState(false);
99
101
  const [saveDialogOpen, setSaveDialogOpen] = useState(false);
100
102
  const [viewName, setViewName] = useState("");
103
+ const [setAsDefault, setSetAsDefault] = useState(false);
101
104
  const [savingView, setSavingView] = useState(false);
102
105
  const [deleteTarget, setDeleteTarget] = useState<DashboardView | null>(null);
106
+ const defaultCheckboxId = useId();
103
107
 
104
108
  const currentFilters = useMemo(
105
109
  () => extractCurrentFilters(searchParams),
@@ -141,16 +145,18 @@ export function ViewsMenu({ dashboardId, canEdit = true }: ViewsMenuProps) {
141
145
  id: slugify(name),
142
146
  name,
143
147
  filters: currentFilters,
148
+ isDefault: setAsDefault,
144
149
  });
145
150
  setViewName("");
151
+ setSetAsDefault(false);
146
152
  setSaveDialogOpen(false);
147
153
  } catch (error) {
148
154
  toast.error(
149
- t("sqlDashboard.saveFailedWithMessage", {
155
+ t("sqlDashboard.saveViewFailedWithMessage", {
150
156
  message:
151
157
  error instanceof Error
152
158
  ? error.message
153
- : t("sqlDashboard.saveFailed"),
159
+ : t("sqlDashboard.saveViewFailed"),
154
160
  }),
155
161
  );
156
162
  } finally {
@@ -206,7 +212,14 @@ export function ViewsMenu({ dashboardId, canEdit = true }: ViewsMenuProps) {
206
212
  applyView(v);
207
213
  }}
208
214
  >
209
- <span className="truncate flex-1">{v.name}</span>
215
+ <span className="flex min-w-0 flex-1 items-center gap-2">
216
+ <span className="truncate">{v.name}</span>
217
+ {v.isDefault && (
218
+ <Badge variant="secondary" className="shrink-0">
219
+ {t("sqlDashboard.defaultView")}
220
+ </Badge>
221
+ )}
222
+ </span>
210
223
  {canEdit ? (
211
224
  <Tooltip>
212
225
  <TooltipTrigger asChild>
@@ -250,7 +263,16 @@ export function ViewsMenu({ dashboardId, canEdit = true }: ViewsMenuProps) {
250
263
  </DropdownMenuContent>
251
264
  </DropdownMenu>
252
265
 
253
- <Dialog open={canEdit && saveDialogOpen} onOpenChange={setSaveDialogOpen}>
266
+ <Dialog
267
+ open={canEdit && saveDialogOpen}
268
+ onOpenChange={(open) => {
269
+ setSaveDialogOpen(open);
270
+ if (!open) {
271
+ setViewName("");
272
+ setSetAsDefault(false);
273
+ }
274
+ }}
275
+ >
254
276
  <DialogContent className="sm:max-w-[400px]">
255
277
  <DialogHeader>
256
278
  <DialogTitle>{t("sqlDashboard.saveView")}</DialogTitle>
@@ -265,6 +287,17 @@ export function ViewsMenu({ dashboardId, canEdit = true }: ViewsMenuProps) {
265
287
  }}
266
288
  autoFocus
267
289
  />
290
+ <label
291
+ htmlFor={defaultCheckboxId}
292
+ className="mt-3 flex cursor-pointer items-center gap-2 text-sm"
293
+ >
294
+ <Checkbox
295
+ id={defaultCheckboxId}
296
+ checked={setAsDefault}
297
+ onCheckedChange={(checked) => setSetAsDefault(checked === true)}
298
+ />
299
+ <span>{t("sqlDashboard.setAsDefault")}</span>
300
+ </label>
268
301
  {Object.keys(currentFilters).length === 0 && (
269
302
  <p className="mt-2 text-xs text-muted-foreground">
270
303
  {t("sqlDashboard.noActiveFilters")}
@@ -275,7 +308,11 @@ export function ViewsMenu({ dashboardId, canEdit = true }: ViewsMenuProps) {
275
308
  <Button
276
309
  variant="outline"
277
310
  size="sm"
278
- onClick={() => setSaveDialogOpen(false)}
311
+ onClick={() => {
312
+ setSaveDialogOpen(false);
313
+ setViewName("");
314
+ setSetAsDefault(false);
315
+ }}
279
316
  disabled={savingView}
280
317
  >
281
318
  {t("sidebar.cancel")}
@@ -0,0 +1,196 @@
1
+ import { FILTER_PARAM_PREFIX } from "./filter-vars";
2
+
3
+ export interface DashboardFilterRestore {
4
+ filters: Record<string, string>;
5
+ viewId?: string;
6
+ source: "dashboard-default" | "personal";
7
+ }
8
+
9
+ export type DashboardFilterRestoreProgress =
10
+ | { status: "pending" }
11
+ | {
12
+ status: "fallback-applied";
13
+ filters: Record<string, string>;
14
+ viewId?: string;
15
+ }
16
+ | {
17
+ status: "views-failed";
18
+ filters: Record<string, string>;
19
+ viewId?: string;
20
+ }
21
+ | { status: "complete" };
22
+
23
+ export interface DashboardFilterPreferenceSaveGuard {
24
+ filters: Record<string, string>;
25
+ viewId?: string;
26
+ previousFilters: Record<string, string>;
27
+ previousViewId?: string;
28
+ }
29
+
30
+ export interface DashboardFilterRestoreStep {
31
+ progress: DashboardFilterRestoreProgress;
32
+ restore: DashboardFilterRestore | null;
33
+ }
34
+
35
+ export function canPersistDashboardFilterPreference(
36
+ progress: DashboardFilterRestoreProgress,
37
+ currentFilters: Record<string, string>,
38
+ currentViewId: string | undefined,
39
+ ): boolean {
40
+ if (progress.status === "complete") return true;
41
+ if (progress.status !== "views-failed") return false;
42
+ return (
43
+ !sameDashboardFilterMap(progress.filters, currentFilters) ||
44
+ progress.viewId !== currentViewId
45
+ );
46
+ }
47
+
48
+ export function dashboardFilterPreferenceSaveState(
49
+ guard: DashboardFilterPreferenceSaveGuard | null,
50
+ currentFilters: Record<string, string>,
51
+ currentViewId: string | undefined,
52
+ ): "none" | "pending" | "applied" | "changed" {
53
+ if (!guard) return "none";
54
+ if (
55
+ sameDashboardFilterMap(guard.filters, currentFilters) &&
56
+ guard.viewId === currentViewId
57
+ ) {
58
+ return "applied";
59
+ }
60
+ if (
61
+ sameDashboardFilterMap(guard.previousFilters, currentFilters) &&
62
+ guard.previousViewId === currentViewId
63
+ ) {
64
+ return "pending";
65
+ }
66
+ return "changed";
67
+ }
68
+
69
+ export function dashboardFilterParams(
70
+ searchParams: URLSearchParams,
71
+ ): Record<string, string> {
72
+ const filters: Record<string, string> = {};
73
+ searchParams.forEach((value, key) => {
74
+ if (key.startsWith(FILTER_PARAM_PREFIX)) filters[key] = value;
75
+ });
76
+ return filters;
77
+ }
78
+
79
+ export function sameDashboardFilterMap(
80
+ a: Record<string, string> | undefined,
81
+ b: Record<string, string> | undefined,
82
+ ): boolean {
83
+ const aKeys = Object.keys(a ?? {});
84
+ if (aKeys.length !== Object.keys(b ?? {}).length) return false;
85
+ return aKeys.every((key) => a![key] === b![key]);
86
+ }
87
+
88
+ export function matchesDashboardFilterRestoreState(
89
+ searchParams: URLSearchParams,
90
+ filters: Record<string, string>,
91
+ viewId: string | undefined,
92
+ ): boolean {
93
+ return (
94
+ sameDashboardFilterMap(filters, dashboardFilterParams(searchParams)) &&
95
+ (searchParams.get("view") ?? undefined) === viewId
96
+ );
97
+ }
98
+
99
+ function hasExplicitFilterState(searchParams: URLSearchParams): boolean {
100
+ return (
101
+ searchParams.has("view") ||
102
+ Array.from(searchParams.keys()).some((key) =>
103
+ key.startsWith(FILTER_PARAM_PREFIX),
104
+ )
105
+ );
106
+ }
107
+
108
+ function nonEmptyFilters(
109
+ filters: Record<string, string> | undefined,
110
+ ): Record<string, string> {
111
+ return Object.fromEntries(
112
+ Object.entries(filters ?? {}).filter(([, value]) => value),
113
+ );
114
+ }
115
+
116
+ export function resolveDashboardFilterRestoreStep({
117
+ searchParams,
118
+ defaultView,
119
+ savedFilters,
120
+ viewsState,
121
+ savedFiltersState,
122
+ progress,
123
+ }: {
124
+ searchParams: URLSearchParams;
125
+ defaultView: { id: string; filters: Record<string, string> } | undefined;
126
+ savedFilters: Record<string, string> | undefined;
127
+ viewsState: "loading" | "error" | "success";
128
+ savedFiltersState: "loading" | "error" | "success";
129
+ progress: DashboardFilterRestoreProgress;
130
+ }): DashboardFilterRestoreStep {
131
+ if (progress.status === "complete") return { progress, restore: null };
132
+
133
+ if (
134
+ progress.status === "fallback-applied" ||
135
+ progress.status === "views-failed"
136
+ ) {
137
+ if (
138
+ !matchesDashboardFilterRestoreState(
139
+ searchParams,
140
+ progress.filters,
141
+ progress.viewId,
142
+ )
143
+ ) {
144
+ return { progress: { status: "complete" }, restore: null };
145
+ }
146
+ } else if (hasExplicitFilterState(searchParams)) {
147
+ return { progress: { status: "complete" }, restore: null };
148
+ }
149
+
150
+ if (viewsState === "loading") return { progress, restore: null };
151
+
152
+ if (viewsState === "error") {
153
+ if (progress.status === "fallback-applied") {
154
+ return { progress, restore: null };
155
+ }
156
+ const failedProgress =
157
+ progress.status === "views-failed"
158
+ ? progress
159
+ : {
160
+ status: "views-failed" as const,
161
+ filters: dashboardFilterParams(searchParams),
162
+ viewId: searchParams.get("view") ?? undefined,
163
+ };
164
+ if (savedFiltersState !== "success") {
165
+ return { progress: failedProgress, restore: null };
166
+ }
167
+ const filters = nonEmptyFilters(savedFilters);
168
+ if (Object.keys(filters).length === 0) {
169
+ return { progress: failedProgress, restore: null };
170
+ }
171
+ return {
172
+ progress: { status: "fallback-applied", filters },
173
+ restore: { filters, source: "personal" },
174
+ };
175
+ }
176
+
177
+ if (defaultView) {
178
+ return {
179
+ progress: { status: "complete" },
180
+ restore: {
181
+ filters: nonEmptyFilters(defaultView.filters),
182
+ viewId: defaultView.id,
183
+ source: "dashboard-default",
184
+ },
185
+ };
186
+ }
187
+
188
+ if (savedFiltersState !== "success") return { progress, restore: null };
189
+
190
+ const filters = nonEmptyFilters(savedFilters);
191
+ return {
192
+ progress: { status: "complete" },
193
+ restore:
194
+ Object.keys(filters).length > 0 ? { filters, source: "personal" } : null,
195
+ };
196
+ }
@@ -1,5 +1,7 @@
1
1
  import type { DashboardFilter, FilterType } from "./types";
2
2
 
3
+ export const FILTER_PARAM_PREFIX = "f_";
4
+
3
5
  const ALL_TIME_START = "1970-01-01";
4
6
  const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
5
7
  const DATE_FILTER_TYPES: ReadonlySet<FilterType> = new Set([
@@ -139,6 +139,16 @@ import { useAutoFocusSelect } from "@/lib/use-auto-focus-select";
139
139
 
140
140
  import BlankDashboard from "../BlankDashboard";
141
141
  import { DashboardSkeleton } from "../DashboardSkeleton";
142
+ import {
143
+ canPersistDashboardFilterPreference,
144
+ dashboardFilterParams,
145
+ dashboardFilterPreferenceSaveState,
146
+ matchesDashboardFilterRestoreState,
147
+ resolveDashboardFilterRestoreStep,
148
+ sameDashboardFilterMap,
149
+ type DashboardFilterPreferenceSaveGuard,
150
+ type DashboardFilterRestoreProgress,
151
+ } from "./dashboard-filter-restore";
142
152
  import {
143
153
  availableDropSlotIdsForPanel,
144
154
  buildDashboardPanelGroups,
@@ -177,6 +187,7 @@ import {
177
187
  clampDashboardColumns,
178
188
  DEFAULT_DASHBOARD_COLUMNS,
179
189
  type DashboardCertification,
190
+ type DashboardFilter,
180
191
  type SqlDashboardConfig,
181
192
  type SqlPanel,
182
193
  } from "./types";
@@ -189,15 +200,6 @@ type DashboardTabGroup = {
189
200
  tabs: Array<{ value: string; label: string }>;
190
201
  };
191
202
 
192
- function sameFilterMap(
193
- a: Record<string, string> | undefined,
194
- b: Record<string, string>,
195
- ): boolean {
196
- const aKeys = Object.keys(a ?? {});
197
- if (aKeys.length !== Object.keys(b).length) return false;
198
- return aKeys.every((key) => a![key] === b[key]);
199
- }
200
-
201
203
  function groupDashboardTabs(tabs: string[]): {
202
204
  groups: DashboardTabGroup[];
203
205
  hasNestedTabs: boolean;
@@ -265,6 +267,7 @@ function DashboardDragPreview({ panel }: { panel: SqlPanel | null }) {
265
267
  const PanelCell = memo(function PanelCell({
266
268
  panel,
267
269
  vars,
270
+ timeRangeFilter,
268
271
  remoteEditor,
269
272
  editable,
270
273
  eagerLoad,
@@ -279,6 +282,7 @@ const PanelCell = memo(function PanelCell({
279
282
  }: {
280
283
  panel: SqlPanel;
281
284
  vars: Record<string, string>;
285
+ timeRangeFilter: DashboardFilter | undefined;
282
286
  remoteEditor: { color: string; name: string } | undefined;
283
287
  editable: boolean;
284
288
  eagerLoad: boolean;
@@ -294,6 +298,14 @@ const PanelCell = memo(function PanelCell({
294
298
  onSavePanel: (panel: SqlPanel) => Promise<void>;
295
299
  dashboardExtensionContext: Record<string, unknown>;
296
300
  }) {
301
+ const [timeRangeOverride, setTimeRangeOverride] = useState<string | null>(
302
+ null,
303
+ );
304
+ const effectiveVars = useMemo(
305
+ () =>
306
+ timeRangeOverride ? { ...vars, timeRange: timeRangeOverride } : vars,
307
+ [vars, timeRangeOverride],
308
+ );
297
309
  const resolved = useMemo(
298
310
  () =>
299
311
  panel.config?.description
@@ -301,16 +313,20 @@ const PanelCell = memo(function PanelCell({
301
313
  ...panel,
302
314
  config: {
303
315
  ...panel.config,
304
- description: interpolate(panel.config.description, vars),
316
+ description: interpolate(panel.config.description, effectiveVars),
305
317
  },
306
318
  }
307
319
  : panel,
308
- [panel, vars],
320
+ [panel, effectiveVars],
309
321
  );
310
322
  const resolvedSql = useMemo(
311
323
  () =>
312
- interpolateDashboardPanelSql(serializePanelSql(panel.sql), vars, panel),
313
- [panel.sql, vars],
324
+ interpolateDashboardPanelSql(
325
+ serializePanelSql(panel.sql),
326
+ effectiveVars,
327
+ panel,
328
+ ),
329
+ [panel.sql, effectiveVars],
314
330
  );
315
331
  const handleSelectForChat = useCallback(
316
332
  (options?: SelectDashboardPanelOptions) => {
@@ -377,7 +393,10 @@ const PanelCell = memo(function PanelCell({
377
393
  ? dashboardExtensionContext.dashboardId
378
394
  : ""
379
395
  }
380
- filters={vars}
396
+ filters={effectiveVars}
397
+ timeRangeFilter={timeRangeFilter}
398
+ timeRangeOverride={timeRangeOverride}
399
+ onTimeRangeOverrideChange={setTimeRangeOverride}
381
400
  extensionContext={
382
401
  panel.chartType === "extension"
383
402
  ? {
@@ -905,17 +924,28 @@ function SqlDashboardPageContent({
905
924
  const filterPrefKey = dashboardId ? `dashboard-filters:${dashboardId}` : "";
906
925
  const {
907
926
  data: savedFilters,
908
- isLoading: filtersLoading,
909
927
  isSuccess: filtersLoaded,
928
+ isError: filtersError,
910
929
  save: saveFilterPref,
911
930
  } = useUserPref<{ filters: Record<string, string> }>(filterPrefKey);
912
931
 
913
- const { saveView } = useDashboardViews(dashboardId ?? undefined);
914
-
915
- const appliedSaved = useRef(false);
932
+ const {
933
+ views,
934
+ isSuccess: dashboardViewsLoaded,
935
+ isSettled: dashboardViewsSettled,
936
+ saveView,
937
+ } = useDashboardViews(dashboardId ?? undefined);
938
+ const defaultView = views.find((view) => view.isDefault);
939
+
940
+ const filterRestoreProgress = useRef<DashboardFilterRestoreProgress>({
941
+ status: "pending",
942
+ });
943
+ const skipFilterPreferenceSave =
944
+ useRef<DashboardFilterPreferenceSaveGuard | null>(null);
916
945
 
917
946
  useEffect(() => {
918
- appliedSaved.current = false;
947
+ filterRestoreProgress.current = { status: "pending" };
948
+ skipFilterPreferenceSave.current = null;
919
949
  setLoaded(false);
920
950
  setDashboard(null);
921
951
  setArchivedAt(null);
@@ -1018,49 +1048,98 @@ function SqlDashboardPageContent({
1018
1048
  useEffect(() => {
1019
1049
  if (
1020
1050
  reportScreenshot ||
1021
- appliedSaved.current ||
1022
- filtersLoading ||
1023
- !filtersLoaded ||
1051
+ filterRestoreProgress.current.status === "complete" ||
1024
1052
  !loaded ||
1025
1053
  !dashboard
1026
1054
  )
1027
1055
  return;
1028
- appliedSaved.current = true;
1029
-
1030
- const viewId = searchParams.get("view");
1031
- if (viewId) return;
1032
1056
 
1033
- const hasUrlFilters = Array.from(searchParams.keys()).some((k) =>
1034
- k.startsWith(FILTER_PARAM_PREFIX),
1035
- );
1036
- if (hasUrlFilters) return;
1057
+ const previousProgress = filterRestoreProgress.current;
1058
+ const restoreStep = resolveDashboardFilterRestoreStep({
1059
+ searchParams,
1060
+ defaultView,
1061
+ savedFilters: savedFilters?.filters,
1062
+ viewsState: dashboardViewsLoaded
1063
+ ? "success"
1064
+ : dashboardViewsSettled
1065
+ ? "error"
1066
+ : "loading",
1067
+ savedFiltersState: filtersLoaded
1068
+ ? "success"
1069
+ : filtersError
1070
+ ? "error"
1071
+ : "loading",
1072
+ progress: previousProgress,
1073
+ });
1074
+ filterRestoreProgress.current = restoreStep.progress;
1075
+ const filterRestore = restoreStep.restore;
1076
+ if (!filterRestore) return;
1037
1077
 
1038
1078
  try {
1039
1079
  const appliedAt = Number(
1040
1080
  sessionStorage.getItem("__agentUrlAppliedAt__") || 0,
1041
1081
  );
1042
- if (appliedAt && Date.now() - appliedAt < 5000) return;
1082
+ if (appliedAt && Date.now() - appliedAt < 5000) {
1083
+ filterRestoreProgress.current = { status: "complete" };
1084
+ return;
1085
+ }
1043
1086
  } catch {
1044
1087
  // sessionStorage unavailable — fall through.
1045
1088
  }
1046
1089
 
1047
- if (savedFilters?.filters && Object.keys(savedFilters.filters).length > 0) {
1048
- setSearchParams(
1049
- (prev) => {
1050
- const next = new URLSearchParams(prev);
1051
- for (const [key, value] of Object.entries(savedFilters.filters)) {
1052
- if (value) next.set(key, value);
1053
- }
1054
- return next;
1055
- },
1056
- { replace: true },
1057
- );
1090
+ if (filterRestore.source === "dashboard-default") {
1091
+ skipFilterPreferenceSave.current = {
1092
+ filters: filterRestore.filters,
1093
+ viewId: filterRestore.viewId,
1094
+ previousFilters: dashboardFilterParams(searchParams),
1095
+ previousViewId: searchParams.get("view") ?? undefined,
1096
+ };
1058
1097
  }
1098
+
1099
+ setSearchParams(
1100
+ (prev) => {
1101
+ if (previousProgress.status === "fallback-applied") {
1102
+ if (
1103
+ !matchesDashboardFilterRestoreState(
1104
+ prev,
1105
+ previousProgress.filters,
1106
+ previousProgress.viewId,
1107
+ )
1108
+ ) {
1109
+ return prev;
1110
+ }
1111
+ } else if (
1112
+ prev.has("view") ||
1113
+ Array.from(prev.keys()).some((key) =>
1114
+ key.startsWith(FILTER_PARAM_PREFIX),
1115
+ )
1116
+ ) {
1117
+ return prev;
1118
+ }
1119
+ const next = new URLSearchParams(prev);
1120
+ for (const key of Array.from(next.keys())) {
1121
+ if (key.startsWith(FILTER_PARAM_PREFIX)) next.delete(key);
1122
+ }
1123
+ for (const [key, value] of Object.entries(filterRestore.filters)) {
1124
+ if (value) next.set(key, value);
1125
+ }
1126
+ if (filterRestore.viewId) {
1127
+ next.set("view", filterRestore.viewId);
1128
+ } else {
1129
+ next.delete("view");
1130
+ }
1131
+ return next;
1132
+ },
1133
+ { replace: true },
1134
+ );
1059
1135
  }, [
1060
- filtersLoading,
1061
1136
  filtersLoaded,
1137
+ filtersError,
1138
+ dashboardViewsLoaded,
1139
+ dashboardViewsSettled,
1062
1140
  loaded,
1063
1141
  dashboard,
1142
+ defaultView,
1064
1143
  savedFilters,
1065
1144
  reportScreenshot,
1066
1145
  searchParams,
@@ -1073,7 +1152,12 @@ function SqlDashboardPageContent({
1073
1152
  reportScreenshot ||
1074
1153
  !loaded ||
1075
1154
  !dashboard?.filters?.length ||
1076
- !dashboardId
1155
+ !dashboardId ||
1156
+ !canPersistDashboardFilterPreference(
1157
+ filterRestoreProgress.current,
1158
+ dashboardFilterParams(searchParams),
1159
+ searchParams.get("view") ?? undefined,
1160
+ )
1077
1161
  )
1078
1162
  return;
1079
1163
  clearTimeout(saveTimer.current);
@@ -1084,7 +1168,20 @@ function SqlDashboardPageContent({
1084
1168
  currentFilters[k] = v;
1085
1169
  }
1086
1170
  });
1087
- if (sameFilterMap(savedFilters?.filters, currentFilters)) return;
1171
+ const filterPreferenceSaveState = dashboardFilterPreferenceSaveState(
1172
+ skipFilterPreferenceSave.current,
1173
+ currentFilters,
1174
+ searchParams.get("view") ?? undefined,
1175
+ );
1176
+ if (filterPreferenceSaveState === "applied") {
1177
+ skipFilterPreferenceSave.current = null;
1178
+ return;
1179
+ }
1180
+ if (filterPreferenceSaveState === "pending") return;
1181
+ if (filterPreferenceSaveState === "changed") {
1182
+ skipFilterPreferenceSave.current = null;
1183
+ }
1184
+ if (sameDashboardFilterMap(savedFilters?.filters, currentFilters)) return;
1088
1185
  saveFilterPref({ filters: currentFilters });
1089
1186
  }, 1500);
1090
1187
  return () => clearTimeout(saveTimer.current);
@@ -1096,6 +1193,10 @@ function SqlDashboardPageContent({
1096
1193
  savedFilters,
1097
1194
  saveFilterPref,
1098
1195
  reportScreenshot,
1196
+ filtersLoaded,
1197
+ filtersError,
1198
+ dashboardViewsLoaded,
1199
+ dashboardViewsSettled,
1099
1200
  ]);
1100
1201
 
1101
1202
  const enqueueDashboardSave = useCallback(
@@ -1509,6 +1610,11 @@ function SqlDashboardPageContent({
1509
1610
  return { ...(dashboard?.variables ?? {}), ...filterValues };
1510
1611
  }, [dashboard?.variables, dashboard?.filters, searchParams]);
1511
1612
 
1613
+ const timeRangeFilter = useMemo<DashboardFilter | undefined>(
1614
+ () => dashboard?.filters?.find((f) => f.id === "timeRange"),
1615
+ [dashboard?.filters],
1616
+ );
1617
+
1512
1618
  const dashboardExtensionContext = useMemo<Record<string, unknown>>(
1513
1619
  () => ({
1514
1620
  dashboardId,
@@ -1839,13 +1945,17 @@ function SqlDashboardPageContent({
1839
1945
  ]);
1840
1946
 
1841
1947
  const handleSaveView = useCallback(
1842
- async (name: string, filters: Record<string, string>) => {
1948
+ async (
1949
+ name: string,
1950
+ filters: Record<string, string>,
1951
+ isDefault: boolean,
1952
+ ) => {
1843
1953
  const id = name
1844
1954
  .toLowerCase()
1845
1955
  .replace(/[^a-z0-9]+/g, "-")
1846
1956
  .replace(/^-|-$/g, "")
1847
1957
  .slice(0, 60);
1848
- await saveView({ id, name, filters });
1958
+ await saveView({ id, name, filters, isDefault });
1849
1959
  },
1850
1960
  [saveView],
1851
1961
  );
@@ -2547,6 +2657,7 @@ function SqlDashboardPageContent({
2547
2657
  <PanelCell
2548
2658
  panel={panel}
2549
2659
  vars={vars}
2660
+ timeRangeFilter={timeRangeFilter}
2550
2661
  remoteEditor={
2551
2662
  reportScreenshot
2552
2663
  ? undefined
@@ -11,6 +11,7 @@ export type ChartType =
11
11
  | "line"
12
12
  | "area"
13
13
  | "bar"
14
+ | "combo"
14
15
  | "metric"
15
16
  | "table"
16
17
  | "pie"
@@ -80,6 +81,7 @@ export interface SqlPanelConfig {
80
81
  yFormatter?: "number" | "currency" | "percent";
81
82
  rightYKeys?: string[];
82
83
  rightYFormatter?: "number" | "currency" | "percent";
84
+ barKeys?: string[];
83
85
  seriesLabels?: Record<string, string>;
84
86
  description?: string;
85
87
  pivot?: PivotConfig;
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-10-05
4
+ ---
5
+
6
+ Custom date controls stay beside the range selector, and dashboards can save a shared default view.