@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.
- 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/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
package/corpus/README.md
CHANGED
|
@@ -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
|
-
{
|
|
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
|
-
![
|
|
97
|
-
|
|
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" &&
|
|
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?: (
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
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={() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|