@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.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/CHANGELOG.md +6 -0
- package/corpus/templates/analytics/actions/save-dashboard-view.ts +10 -1
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +136 -4
- package/corpus/templates/analytics/app/hooks/use-dashboard-views.ts +4 -0
- package/corpus/templates/analytics/app/i18n/en-US.ts +7 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/DashboardFilterBar.tsx +86 -23
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +4 -3
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +55 -1
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewsMenu.tsx +44 -7
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/dashboard-filter-restore.ts +196 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/filter-vars.ts +2 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +158 -47
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +2 -0
- package/corpus/templates/analytics/changelog/2026-10-05-custom-date-controls-stay-beside-the-range-selector-and-dash.md +6 -0
- package/corpus/templates/analytics/server/db/schema.ts +20 -8
- package/corpus/templates/analytics/server/lib/canonical-first-party-dashboard-repair.ts +50 -3
- package/corpus/templates/analytics/server/lib/dashboard-view-remap.ts +52 -0
- package/corpus/templates/analytics/server/lib/dashboards-store.ts +77 -42
- package/corpus/templates/analytics/server/lib/migrate-analytics-artifacts.ts +23 -8
- package/corpus/templates/analytics/server/plugins/db.ts +7 -0
- package/corpus/templates/clips/CHANGELOG.md +4 -0
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-10-05-the-agent-composer-is-easier-to-distinguish-from-the-shared-.md +6 -0
- package/corpus/templates/design/CHANGELOG.md +1 -0
- package/corpus/templates/design/app/components/design/multi-screen/chrome-transitions.ts +1 -1
- package/corpus/templates/design/changelog/2026-10-05-design-frame-labels-apply-truncation-as-soon-as-resizing-end.md +6 -0
- package/corpus/templates/slides/CHANGELOG.md +7 -0
- package/corpus/templates/slides/agent-native.app-skill.json +2 -2
- package/corpus/templates/slides/app/components/home/SlidesHomeLibrary.tsx +11 -11
- package/corpus/templates/slides/app/context/DeckContext.tsx +16 -12
- package/corpus/templates/slides/app/hooks/use-new-deck-generation.ts +129 -6
- package/corpus/templates/slides/app/lib/create-deck-generation.ts +4 -1
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +25 -8
- package/corpus/templates/slides/app/pages/Index.tsx +85 -42
- package/corpus/templates/slides/changelog/2026-10-05-file-uploads-explain-the-free-builder-io-option-and-how-to-c.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-05-slides-restores-pending-generation-questions-when-reopened.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-05-the-home-page-shows-both-library-tabs-and-starter-prompts-im.md +6 -0
- package/corpus/templates/slides/changelog/2026-10-05-undo-stops-at-generated-deck.md +6 -0
- 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 "./
|
|
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.
|
|
155
|
+
t("sqlDashboard.saveViewFailedWithMessage", {
|
|
150
156
|
message:
|
|
151
157
|
error instanceof Error
|
|
152
158
|
? error.message
|
|
153
|
-
: t("sqlDashboard.
|
|
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="
|
|
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
|
|
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={() =>
|
|
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
|
+
}
|
|
@@ -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,
|
|
316
|
+
description: interpolate(panel.config.description, effectiveVars),
|
|
305
317
|
},
|
|
306
318
|
}
|
|
307
319
|
: panel,
|
|
308
|
-
[panel,
|
|
320
|
+
[panel, effectiveVars],
|
|
309
321
|
);
|
|
310
322
|
const resolvedSql = useMemo(
|
|
311
323
|
() =>
|
|
312
|
-
interpolateDashboardPanelSql(
|
|
313
|
-
|
|
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={
|
|
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 {
|
|
914
|
-
|
|
915
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
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)
|
|
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 (
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
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
|
-
|
|
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 (
|
|
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;
|