@agent-native/core-corpus 0.201.1-nightly-20261005213104 → 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 (39) 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/app/components/home/SlidesHomeLibrary.tsx +11 -11
  30. package/corpus/templates/slides/app/context/DeckContext.tsx +16 -12
  31. package/corpus/templates/slides/app/hooks/use-new-deck-generation.ts +129 -6
  32. package/corpus/templates/slides/app/lib/create-deck-generation.ts +4 -1
  33. package/corpus/templates/slides/app/pages/DeckEditor.tsx +25 -8
  34. package/corpus/templates/slides/app/pages/Index.tsx +85 -42
  35. package/corpus/templates/slides/changelog/2026-10-05-file-uploads-explain-the-free-builder-io-option-and-how-to-c.md +6 -0
  36. package/corpus/templates/slides/changelog/2026-10-05-slides-restores-pending-generation-questions-when-reopened.md +6 -0
  37. package/corpus/templates/slides/changelog/2026-10-05-the-home-page-shows-both-library-tabs-and-starter-prompts-im.md +6 -0
  38. package/corpus/templates/slides/changelog/2026-10-05-undo-stops-at-generated-deck.md +6 -0
  39. package/package.json +1 -1
package/corpus/README.md CHANGED
@@ -32,4 +32,4 @@ rg -n "defineAction|useActionQuery" node_modules/@agent-native/core-corpus/corpu
32
32
 
33
33
  ## Generated Counts
34
34
 
35
- - template files: 11150
35
+ - template files: 11159
@@ -3,6 +3,12 @@
3
3
  All notable user-facing changes to Agent-Native Analytics are documented here. Open it any
4
4
  time from the command menu (Cmd+K → "What's new") or from Settings.
5
5
 
6
+ ## 2026-10-05
7
+
8
+ ### Improved
9
+
10
+ - Custom date controls stay beside the range selector, and dashboards can save a shared default view.
11
+
6
12
  ## 2026-10-02
7
13
 
8
14
  ### Improved
@@ -14,6 +14,10 @@ export default defineAction({
14
14
  dashboardId: z.string().min(1).describe("The dashboard ID"),
15
15
  id: z.string().optional().describe("Existing view ID when updating"),
16
16
  name: z.string().min(1).describe("View name"),
17
+ isDefault: z
18
+ .boolean()
19
+ .optional()
20
+ .describe("Whether this view becomes the dashboard's default view"),
17
21
  filters: z
18
22
  .record(z.string(), z.string())
19
23
  .optional()
@@ -25,7 +29,12 @@ export default defineAction({
25
29
  const orgId = getRequestOrgId() || null;
26
30
  const view = await saveDashboardView(
27
31
  args.dashboardId,
28
- { id: args.id, name: args.name, filters: args.filters ?? {} },
32
+ {
33
+ id: args.id,
34
+ name: args.name,
35
+ filters: args.filters ?? {},
36
+ isDefault: args.isDefault,
37
+ },
29
38
  { email, orgId },
30
39
  );
31
40
  return { success: true, view };
@@ -37,6 +37,7 @@ import {
37
37
  BarChart,
38
38
  CartesianGrid,
39
39
  Cell,
40
+ ComposedChart,
40
41
  Legend,
41
42
  Line,
42
43
  LineChart,
@@ -93,13 +94,23 @@ export function limitChartRows(
93
94
  ): Record<string, unknown>[] {
94
95
  if (
95
96
  rows.length <= MAX_CHART_POINTS ||
96
- !["line", "area", "bar", "pie", "heatmap", "funnel", "callout"].includes(
97
- chartType,
98
- )
97
+ ![
98
+ "line",
99
+ "area",
100
+ "bar",
101
+ "combo",
102
+ "pie",
103
+ "heatmap",
104
+ "funnel",
105
+ "callout",
106
+ ].includes(chartType)
99
107
  ) {
100
108
  return rows;
101
109
  }
102
- return chartType !== "line" && chartType !== "area" && chartType !== "heatmap"
110
+ return chartType !== "line" &&
111
+ chartType !== "area" &&
112
+ chartType !== "combo" &&
113
+ chartType !== "heatmap"
103
114
  ? rows.slice(0, MAX_CHART_POINTS)
104
115
  : rows.slice(-MAX_CHART_POINTS);
105
116
  }
@@ -946,12 +957,14 @@ function chartTypeReservesLegend(panel: SqlPanel): boolean {
946
957
  panel.chartType === "line" ||
947
958
  panel.chartType === "area" ||
948
959
  panel.chartType === "bar" ||
960
+ panel.chartType === "combo" ||
949
961
  panel.chartType === "pie";
950
962
  if (!chartUsesFrame) return false;
951
963
  return (
952
964
  panel.chartType === "line" ||
953
965
  panel.chartType === "area" ||
954
966
  panel.chartType === "bar" ||
967
+ panel.chartType === "combo" ||
955
968
  usesPrometheusPresentation(panel)
956
969
  );
957
970
  }
@@ -1463,6 +1476,20 @@ export function SqlChart({
1463
1476
  );
1464
1477
  }
1465
1478
 
1479
+ if (chartType === "combo") {
1480
+ return withConfigWarning(
1481
+ <ComboRenderer
1482
+ rows={chartRows}
1483
+ xKey={xKey}
1484
+ yKeys={yKeys}
1485
+ colors={colors}
1486
+ yFormatter={yFormatter}
1487
+ barKeys={panel.config?.barKeys ?? []}
1488
+ panel={panel}
1489
+ />,
1490
+ );
1491
+ }
1492
+
1466
1493
  if (chartType === "funnel") {
1467
1494
  return withConfigWarning(
1468
1495
  <FunnelRenderer rows={chartRows} panel={panel} colors={colors} />,
@@ -2223,6 +2250,111 @@ function BarRenderer({
2223
2250
  );
2224
2251
  }
2225
2252
 
2253
+ function ComboRenderer({
2254
+ rows,
2255
+ xKey,
2256
+ yKeys,
2257
+ colors,
2258
+ yFormatter,
2259
+ barKeys,
2260
+ panel,
2261
+ }: {
2262
+ rows: Record<string, unknown>[];
2263
+ xKey: string;
2264
+ yKeys: string[];
2265
+ colors: string[];
2266
+ yFormatter?: "number" | "currency" | "percent";
2267
+ barKeys: string[];
2268
+ panel: SqlPanel;
2269
+ }) {
2270
+ const xLabelFormatter = (value: any) =>
2271
+ formatXLabel(String(value ?? ""), panel);
2272
+ const xTooltipLabelFormatter = (value: any) =>
2273
+ formatSqlChartTooltipLabel(String(value ?? ""), panel, xKey);
2274
+ const seriesNameFormatter = (name: string) =>
2275
+ formatSeriesLabelForPanel(panel, name);
2276
+ const { hiddenKeys, toggleSeries, filterSeries } = useSeriesVisibility(yKeys);
2277
+ const dualAxis = resolveDualAxis(yKeys, panel.config, seriesNameFormatter);
2278
+ const valueFormatter = seriesValueFormatter(
2279
+ yKeys,
2280
+ dualAxis,
2281
+ seriesNameFormatter,
2282
+ yFormatter,
2283
+ );
2284
+ const barKeySet = new Set(barKeys);
2285
+
2286
+ return (
2287
+ <ChartFrame
2288
+ panel={panel}
2289
+ legendKeys={yKeys}
2290
+ colors={colors}
2291
+ hiddenKeys={hiddenKeys}
2292
+ onToggleLegendKey={toggleSeries}
2293
+ onFilterLegendKey={filterSeries}
2294
+ showCustomLegend
2295
+ >
2296
+ <ChartResponsiveContainer>
2297
+ <ComposedChart data={rows}>
2298
+ <XAxis
2299
+ dataKey={xKey}
2300
+ stroke="hsl(var(--muted-foreground))"
2301
+ fontSize={12}
2302
+ tickLine={false}
2303
+ axisLine={false}
2304
+ tickFormatter={xLabelFormatter}
2305
+ />
2306
+ {renderChartYAxes(dualAxis, yFormatter)}
2307
+ <CartesianGrid
2308
+ strokeDasharray="3 3"
2309
+ stroke="hsl(var(--border))"
2310
+ vertical={false}
2311
+ />
2312
+ <Tooltip
2313
+ {...CHART_TOOLTIP_PROPS}
2314
+ cursor={BAR_TOOLTIP_CURSOR_PROPS}
2315
+ labelFormatter={xTooltipLabelFormatter}
2316
+ content={
2317
+ <ChartTooltip
2318
+ labelFormatter={xTooltipLabelFormatter}
2319
+ seriesNameFormatter={seriesNameFormatter}
2320
+ valueFormatter={valueFormatter}
2321
+ />
2322
+ }
2323
+ itemSorter={(item) => -(Number(item.value) || 0)}
2324
+ />
2325
+ {yKeys.map((key, i) =>
2326
+ barKeySet.has(key) ? (
2327
+ <Bar
2328
+ key={key}
2329
+ dataKey={key}
2330
+ name={seriesNameFormatter(key)}
2331
+ yAxisId={seriesAxisId(dualAxis, key)}
2332
+ fill={colors[i % colors.length]}
2333
+ radius={[4, 4, 0, 0]}
2334
+ hide={hiddenKeys.has(key)}
2335
+ isAnimationActive={false}
2336
+ />
2337
+ ) : (
2338
+ <Line
2339
+ key={key}
2340
+ type="monotone"
2341
+ dataKey={key}
2342
+ name={seriesNameFormatter(key)}
2343
+ yAxisId={seriesAxisId(dualAxis, key)}
2344
+ stroke={colors[i % colors.length]}
2345
+ strokeWidth={2}
2346
+ dot={false}
2347
+ hide={hiddenKeys.has(key)}
2348
+ isAnimationActive={false}
2349
+ />
2350
+ ),
2351
+ )}
2352
+ </ComposedChart>
2353
+ </ChartResponsiveContainer>
2354
+ </ChartFrame>
2355
+ );
2356
+ }
2357
+
2226
2358
  function TimeSeriesRenderer({
2227
2359
  rows,
2228
2360
  xKey,
@@ -7,6 +7,7 @@ export interface DashboardView {
7
7
  id: string;
8
8
  name: string;
9
9
  filters: Record<string, string>;
10
+ isDefault?: boolean;
10
11
  createdBy?: string;
11
12
  createdAt?: string;
12
13
  }
@@ -48,6 +49,7 @@ export function useDashboardViews(dashboardId: string | undefined) {
48
49
  id: view.id,
49
50
  name: view.name,
50
51
  filters: view.filters,
52
+ isDefault: view.isDefault,
51
53
  });
52
54
  },
53
55
  onSettled: () => {
@@ -74,6 +76,8 @@ export function useDashboardViews(dashboardId: string | undefined) {
74
76
  return {
75
77
  views,
76
78
  isLoading: viewsQuery.isLoading,
79
+ isSuccess: viewsQuery.isSuccess,
80
+ isSettled: viewsQuery.isSuccess || viewsQuery.isError,
77
81
  error: viewsQuery.error,
78
82
  refetch: viewsQuery.refetch,
79
83
  saveView,
@@ -795,10 +795,14 @@ export default {
795
795
  filters: "Filters",
796
796
  autoApplied: "auto-applied",
797
797
  saveView: "Save view",
798
+ saveViewFailed: "Couldn't save view",
799
+ saveViewFailedWithMessage: "Couldn't save view: {{message}}",
798
800
  clearAll: "Clear all",
799
801
  collapseFilters: "Collapse filters",
800
802
  expandFilters: "Expand filters",
801
803
  filterSeries: "Filter",
804
+ chartTimeRangeOverride: "Override time range for this chart",
805
+ chartTimeRangeInherit: "Dashboard range",
802
806
  hide: "Hide",
803
807
  show: "Show",
804
808
  saveAsView: "Save as View",
@@ -858,6 +862,8 @@ export default {
858
862
  savedViews: "Saved views",
859
863
  views: "Views",
860
864
  noSavedViews: "No saved views yet.",
865
+ defaultView: "Default",
866
+ setAsDefault: "Set as default for this dashboard",
861
867
  deleteView: "Delete {{name}}",
862
868
  saveCurrentView: "Save current view",
863
869
  viewNameEnterprisePlaceholder: "View name (e.g. 'Enterprise only')",
@@ -1218,6 +1224,7 @@ export default {
1218
1224
  chartTypeLine: "Line",
1219
1225
  chartTypeArea: "Area",
1220
1226
  chartTypeBar: "Bar",
1227
+ chartTypeCombo: "Combo (bar + line)",
1221
1228
  chartTypePie: "Pie",
1222
1229
  chartTypeMetric: "Metric",
1223
1230
  chartTypeTable: "Table",
@@ -4,10 +4,12 @@ import {
4
4
  IconDeviceFloppy,
5
5
  IconFilterOff,
6
6
  } from "@tabler/icons-react";
7
- import { useCallback, useMemo, useState } from "react";
7
+ import { useCallback, useId, useMemo, useState } from "react";
8
8
  import { useSearchParams } from "react-router";
9
+ import { toast } from "sonner";
9
10
 
10
11
  import { Button } from "@/components/ui/button";
12
+ import { Checkbox } from "@/components/ui/checkbox";
11
13
  import {
12
14
  Collapsible,
13
15
  CollapsibleContent,
@@ -33,15 +35,14 @@ import { cn } from "@/lib/utils";
33
35
 
34
36
  import { DateRangeInput } from "../_shared/components/DateRangeInput";
35
37
  import {
38
+ FILTER_PARAM_PREFIX,
36
39
  isDateRangePresetFilter,
37
40
  resolveDefault,
38
41
  resolveFilterVars,
39
42
  } from "./filter-vars";
40
43
  import type { DashboardFilter } from "./types";
41
44
 
42
- export { resolveFilterVars } from "./filter-vars";
43
-
44
- export const FILTER_PARAM_PREFIX = "f_";
45
+ export { FILTER_PARAM_PREFIX, resolveFilterVars } from "./filter-vars";
45
46
 
46
47
  /** Check if any filter param in the URL differs from the defaults */
47
48
  function hasActiveFilters(
@@ -95,7 +96,11 @@ export function extractFilterParams(
95
96
 
96
97
  interface DashboardFilterBarProps {
97
98
  filters: DashboardFilter[];
98
- onSaveView?: (name: string, filters: Record<string, string>) => void;
99
+ onSaveView?: (
100
+ name: string,
101
+ filters: Record<string, string>,
102
+ isDefault: boolean,
103
+ ) => void | Promise<void>;
99
104
  }
100
105
 
101
106
  export function DashboardFilterBar({
@@ -106,6 +111,9 @@ export function DashboardFilterBar({
106
111
  const [searchParams, setSearchParams] = useSearchParams();
107
112
  const [saveDialogOpen, setSaveDialogOpen] = useState(false);
108
113
  const [viewName, setViewName] = useState("");
114
+ const [setAsDefault, setSetAsDefault] = useState(false);
115
+ const [savingView, setSavingView] = useState(false);
116
+ const defaultCheckboxId = useId();
109
117
  const [filtersOpen, setFiltersOpen] = useState(true);
110
118
  const uniqueFilters = useMemo(() => {
111
119
  const seen = new Set<string>();
@@ -160,13 +168,36 @@ export function DashboardFilterBar({
160
168
  });
161
169
  }, [setSearchParams]);
162
170
 
163
- const handleSaveView = useCallback(() => {
164
- if (!viewName.trim() || !onSaveView) return;
165
- const currentFilters = extractFilterParams(uniqueFilters, searchParams);
166
- onSaveView(viewName.trim(), currentFilters);
167
- setViewName("");
168
- setSaveDialogOpen(false);
169
- }, [viewName, onSaveView, uniqueFilters, searchParams]);
171
+ const handleSaveView = useCallback(async () => {
172
+ if (!viewName.trim() || !onSaveView || savingView) return;
173
+ setSavingView(true);
174
+ try {
175
+ const currentFilters = extractFilterParams(uniqueFilters, searchParams);
176
+ await onSaveView(viewName.trim(), currentFilters, setAsDefault);
177
+ setViewName("");
178
+ setSetAsDefault(false);
179
+ setSaveDialogOpen(false);
180
+ } catch (error) {
181
+ toast.error(
182
+ t("sqlDashboard.saveViewFailedWithMessage", {
183
+ message:
184
+ error instanceof Error
185
+ ? error.message
186
+ : t("sqlDashboard.saveViewFailed"),
187
+ }),
188
+ );
189
+ } finally {
190
+ setSavingView(false);
191
+ }
192
+ }, [
193
+ viewName,
194
+ onSaveView,
195
+ savingView,
196
+ uniqueFilters,
197
+ searchParams,
198
+ setAsDefault,
199
+ t,
200
+ ]);
170
201
 
171
202
  const vars = useMemo(
172
203
  () => resolveFilterVars(uniqueFilters, getParam),
@@ -248,7 +279,16 @@ export function DashboardFilterBar({
248
279
  </div>
249
280
  </Collapsible>
250
281
 
251
- <Dialog open={saveDialogOpen} onOpenChange={setSaveDialogOpen}>
282
+ <Dialog
283
+ open={saveDialogOpen}
284
+ onOpenChange={(open) => {
285
+ setSaveDialogOpen(open);
286
+ if (!open) {
287
+ setViewName("");
288
+ setSetAsDefault(false);
289
+ }
290
+ }}
291
+ >
252
292
  <DialogContent className="sm:max-w-[400px]">
253
293
  <DialogHeader>
254
294
  <DialogTitle>{t("sqlDashboard.saveAsView")}</DialogTitle>
@@ -258,24 +298,40 @@ export function DashboardFilterBar({
258
298
  placeholder={t("sqlDashboard.viewNameRecentPlaceholder")}
259
299
  value={viewName}
260
300
  onChange={(e) => setViewName(e.target.value)}
261
- onKeyDown={(e) => e.key === "Enter" && handleSaveView()}
301
+ onKeyDown={(e) => e.key === "Enter" && void handleSaveView()}
262
302
  autoFocus
263
303
  />
304
+ <label
305
+ htmlFor={defaultCheckboxId}
306
+ className="mt-3 flex cursor-pointer items-center gap-2 text-sm"
307
+ >
308
+ <Checkbox
309
+ id={defaultCheckboxId}
310
+ checked={setAsDefault}
311
+ onCheckedChange={(checked) => setSetAsDefault(checked === true)}
312
+ />
313
+ <span>{t("sqlDashboard.setAsDefault")}</span>
314
+ </label>
264
315
  </div>
265
316
  <DialogFooter>
266
317
  <Button
267
318
  variant="outline"
268
319
  size="sm"
269
- onClick={() => setSaveDialogOpen(false)}
320
+ onClick={() => {
321
+ setSaveDialogOpen(false);
322
+ setViewName("");
323
+ setSetAsDefault(false);
324
+ }}
325
+ disabled={savingView}
270
326
  >
271
327
  {t("sidebar.cancel")}
272
328
  </Button>
273
329
  <Button
274
330
  size="sm"
275
- onClick={handleSaveView}
276
- disabled={!viewName.trim()}
331
+ onClick={() => void handleSaveView()}
332
+ disabled={!viewName.trim() || savingView}
277
333
  >
278
- {t("explorer.save")}
334
+ {savingView ? t("sqlDashboard.saving") : t("explorer.save")}
279
335
  </Button>
280
336
  </DialogFooter>
281
337
  </DialogContent>
@@ -332,7 +388,7 @@ function FilterControl({
332
388
  const supportsCustomRange = isDateRangePresetFilter(filter);
333
389
  const startKey = filter.id + "Start";
334
390
  const endKey = filter.id + "End";
335
- return (
391
+ const selectControl = (
336
392
  <div className="flex flex-col gap-1">
337
393
  <label className="text-xs text-muted-foreground font-medium">
338
394
  {filter.label}
@@ -371,17 +427,24 @@ function FilterControl({
371
427
  )}
372
428
  </SelectContent>
373
429
  </Select>
374
- {supportsCustomRange && current === "custom" && (
430
+ </div>
431
+ );
432
+ if (supportsCustomRange && current === "custom") {
433
+ return (
434
+ <div className="flex min-w-0 flex-wrap items-end gap-3">
435
+ {selectControl}
375
436
  <DateRangeInput
376
437
  label={t("sqlDashboard.customRange")}
377
438
  startDate={vars[startKey] || ""}
378
439
  endDate={vars[endKey] || ""}
379
440
  onStartChange={(v) => setValue({ [startKey]: v })}
380
441
  onEndChange={(v) => setValue({ [endKey]: v })}
442
+ className="shrink-0"
381
443
  />
382
- )}
383
- </div>
384
- );
444
+ </div>
445
+ );
446
+ }
447
+ return selectControl;
385
448
  }
386
449
 
387
450
  if (filter.type === "toggle") {
@@ -54,6 +54,7 @@ const CHART_TYPES: { value: ChartType; labelKey: string }[] = [
54
54
  { value: "line", labelKey: "panelEditor.chartTypeLine" },
55
55
  { value: "area", labelKey: "panelEditor.chartTypeArea" },
56
56
  { value: "bar", labelKey: "panelEditor.chartTypeBar" },
57
+ { value: "combo", labelKey: "panelEditor.chartTypeCombo" },
57
58
  { value: "pie", labelKey: "panelEditor.chartTypePie" },
58
59
  { value: "metric", labelKey: "panelEditor.chartTypeMetric" },
59
60
  { value: "table", labelKey: "panelEditor.chartTypeTable" },
@@ -388,7 +389,7 @@ function PanelEditorContent({
388
389
  `Inspect the dashboard's declared filters and use their exact ids in first-party SQL. The seeded dashboard uses \`{{timeRange}}\`, with \`{{timeRangeStart}}\` and \`{{timeRangeEnd}}\` for the custom range. ` +
389
390
  `Use the \`mutate-dashboard\` action with one structured operation like {op:"insertPanel",panel:{id:"new-panel",title:"New Panel",source:"first-party",chartType:"metric",width:1,config:{timeScope:"dashboard"},sql:"SELECT COUNT(*) AS value FROM analytics_events WHERE ${firstPartyTimeFilter}"},position:"bottom"}. The width must be a JSON integer from 1 to 6, never a string. ` + // i18n-ignore i18n-copy-ignore: Internal agent context is not displayed as UI copy.
390
391
  `For placement, use position, index, beforePanelId, afterPanelId, nextToPanelId, rowNumber, or rowPosition. Prefer nextToPanelId or rowNumber for visible row placement requests; they keep the chart in the intended rendered row and expand/rebalance that row when needed. ` +
391
- `Panel shape: { id (unique slug), title, sql, source ('bigquery'|'ga4'|'amplitude'|'first-party'|'demo'|'prometheus'|'program'), chartType ('line'|'area'|'bar'|'metric'|'table'|'pie'|'funnel'|'heatmap'|'callout'|'section'|'extension'), width (legacy integer 1..6; set to 1 unless editing existing data), tab? (use 'Group / Tab' for grouped tabs), columns? (section panels only - 1..6 max panels per row for panels following this section), config? }. ` +
392
+ `Panel shape: { id (unique slug), title, sql, source ('bigquery'|'ga4'|'amplitude'|'first-party'|'demo'|'prometheus'|'program'), chartType ('line'|'area'|'bar'|'combo'|'metric'|'table'|'pie'|'funnel'|'heatmap'|'callout'|'section'|'extension'), width (legacy integer 1..6; set to 1 unless editing existing data), tab? (use 'Group / Tab' for grouped tabs), columns? (section panels only - 1..6 max panels per row for panels following this section), config? }. ` +
392
393
  `Visible layout auto-fits by row: one panel in a row spans the row, two split it, three split it into thirds, up to the section column limit. ` +
393
394
  `For amplitude panels, sql is a JSON descriptor: {"event":"event name","groupBy":"property","days":30}. ` +
394
395
  `For first-party panels, sql is read-only SQL over analytics_events only; use source 'first-party' and do not call db-query for this datasource. ` +
@@ -396,8 +397,8 @@ function PanelEditorContent({
396
397
  `For prometheus panels, sql is a JSON descriptor: {"promql":"rate(http_requests_total[5m])","mode":"range","range":"1h","step":"30s"}. mode defaults to "range"; range defaults to "1h"; step is auto if omitted. Returned rows have shape {timestamp, series, value} — set config.xKey="timestamp", config.yKey="value", and a single series in config.yKeys for clean charting. ` +
397
398
  `For program panels (arbitrary provider data joins/cohorts not expressible in the other sources), first save-data-program (or reuse an existing one via list-data-programs), then set sql to a JSON descriptor: {"programId":"<id>","params":{...}}. See the data-programs skill for the emit(rows, schema) contract and the Risk Meeting worked example. ` +
398
399
  `Native dashboard panels and Data Programs come first. Add an extension panel only when the user explicitly asks for a genuinely bespoke, one-off Custom Block for this dashboard and native panels cannot represent it faithfully. For a reusable/native capability call connect-builder instead. New agent-authored Custom Blocks use config.extensionId plus config.customBlock={authoredBy:"agent",intent:"one-off",scope:"dashboard",nativeGapReason:"custom-visualization"|"custom-interaction"|"custom-layout"|"other"}; never put prompt/customer text in that metadata. Use config.extensionSlotId only when the user explicitly asks for a personal/per-viewer slot; slot installs are per-user and automated report identities may have no install. ` +
399
- `Config is optional: { xKey, yKey, yKeys, yFormatter ('number'|'currency'|'percent'), rightYKeys, rightYFormatter, seriesLabels (exact series key -> display label), description, columns, pivot, limit, color, colors, stacked, legend, valueLabels }. For funnel panels, use config.xKey for the stage label, config.yKey for the non-negative count/value, and keep the SQL ORDER BY in the intended funnel order. ` +
400
- `For line/area/bar series that share an x-axis but not a unit (a count next to a rate), put the smaller-unit series on a second y-axis with config.rightYKeys (a subset of yKeys) and an optional config.rightYFormatter — do not build an extension for a dual-axis chart. Use heatmap, callout, and section panels when their native contracts fit; do not create a Custom Block for a supported native panel. ` +
400
+ `Config is optional: { xKey, yKey, yKeys, yFormatter ('number'|'currency'|'percent'), rightYKeys, rightYFormatter, barKeys, seriesLabels (exact series key -> display label), description, columns, pivot, limit, color, colors, stacked, legend, valueLabels }. For funnel panels, use config.xKey for the stage label, config.yKey for the non-negative count/value, and keep the SQL ORDER BY in the intended funnel order. ` +
401
+ `For line/area/bar series that share an x-axis but not a unit (a count next to a rate), put the smaller-unit series on a second y-axis with config.rightYKeys (a subset of yKeys) and an optional config.rightYFormatter — do not build an extension for a dual-axis chart. Use chartType 'combo' to render some yKeys as bars and the rest as lines on one chart: set config.barKeys to the subset of yKeys that should render as bars; any yKey not in barKeys renders as a line. Use heatmap, callout, and section panels when their native contracts fit; do not create a Custom Block for a supported native panel. ` +
401
402
  `Chart legends render automatically; set config.legend=false only when the user explicitly asks to hide the legend. ` +
402
403
  `Use \`get-sql-dashboard.layout.groups[].rows[].rowNumber/panelIds\` to identify and verify visible rows. ` +
403
404
  `Consult the data dictionary first via \`list-data-dictionary --search <topic>\`, then use AGENTS.md, .agents/skills, and connected data-source instructions before writing SQL. ` +
@@ -46,6 +46,13 @@ import {
46
46
  DropdownMenuSeparator,
47
47
  DropdownMenuTrigger,
48
48
  } from "@/components/ui/dropdown-menu";
49
+ import {
50
+ Select,
51
+ SelectContent,
52
+ SelectItem,
53
+ SelectTrigger,
54
+ SelectValue,
55
+ } from "@/components/ui/select";
49
56
  import {
50
57
  Tooltip,
51
58
  TooltipContent,
@@ -57,9 +64,11 @@ import { cn } from "@/lib/utils";
57
64
 
58
65
  import { serializePanelSql } from "./panel-sql";
59
66
  import { timeRangeDays } from "./pivot";
60
- import type { SqlPanel } from "./types";
67
+ import type { DashboardFilter, SqlPanel } from "./types";
61
68
  import { ViewSqlPopover } from "./ViewSqlPopover";
62
69
 
70
+ const INHERIT_TIME_RANGE = "__inherit__";
71
+
63
72
  interface SqlChartCardProps {
64
73
  panel: SqlPanel;
65
74
  resolvedSql?: string;
@@ -75,6 +84,9 @@ interface SqlChartCardProps {
75
84
  extensionContext?: Record<string, unknown> | null;
76
85
  dashboardId?: string;
77
86
  filters?: Record<string, string>;
87
+ timeRangeFilter?: DashboardFilter;
88
+ timeRangeOverride?: string | null;
89
+ onTimeRangeOverrideChange?: (value: string | null) => void;
78
90
  }
79
91
 
80
92
  const PanelDragHandle = memo(function PanelDragHandle({
@@ -134,6 +146,9 @@ export function SqlChartCard({
134
146
  extensionContext,
135
147
  dashboardId,
136
148
  filters,
149
+ timeRangeFilter,
150
+ timeRangeOverride,
151
+ onTimeRangeOverrideChange,
137
152
  }: SqlChartCardProps) {
138
153
  const t = useT();
139
154
  const timeRange = timeRangeDays(filters?.timeRange);
@@ -605,6 +620,45 @@ export function SqlChartCard({
605
620
  <CardTitle className="text-sm font-medium flex-1 truncate">
606
621
  {panel.title}
607
622
  </CardTitle>
623
+ {timeRangeFilter?.options?.length ? (
624
+ <Tooltip>
625
+ <TooltipTrigger asChild>
626
+ <Select
627
+ value={timeRangeOverride ?? INHERIT_TIME_RANGE}
628
+ onValueChange={(value) =>
629
+ onTimeRangeOverrideChange?.(
630
+ value === INHERIT_TIME_RANGE ? null : value,
631
+ )
632
+ }
633
+ >
634
+ <SelectTrigger
635
+ size="sm"
636
+ className="h-6 w-[100px] text-xs"
637
+ aria-label={t("sqlDashboard.chartTimeRangeOverride")}
638
+ >
639
+ <SelectValue />
640
+ </SelectTrigger>
641
+ <SelectContent align="end">
642
+ <SelectItem value={INHERIT_TIME_RANGE} className="text-xs">
643
+ {t("sqlDashboard.chartTimeRangeInherit")}
644
+ </SelectItem>
645
+ {timeRangeFilter.options.map((opt) => (
646
+ <SelectItem
647
+ key={opt.value}
648
+ value={opt.value}
649
+ className="text-xs"
650
+ >
651
+ {opt.label}
652
+ </SelectItem>
653
+ ))}
654
+ </SelectContent>
655
+ </Select>
656
+ </TooltipTrigger>
657
+ <TooltipContent>
658
+ {t("sqlDashboard.chartTimeRangeOverride")}
659
+ </TooltipContent>
660
+ </Tooltip>
661
+ ) : null}
608
662
  <div className="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
609
663
  {!editable || onSaveSql ? (
610
664
  <ViewSqlPopover