@agent-native/core 0.72.4 → 0.74.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +67 -0
- package/corpus/core/docs/content/skills-guide.md +14 -0
- package/corpus/core/docs/content/template-clips.md +7 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/durable-background.ts +35 -12
- package/corpus/core/src/agent/production-agent.ts +55 -8
- package/corpus/core/src/cli/create.ts +1 -1
- package/corpus/core/src/cli/skills.ts +450 -19
- package/corpus/core/src/client/ConnectBuilderCard.tsx +7 -7
- package/corpus/core/src/client/NewWorkspaceAppFlow.tsx +1 -1
- package/corpus/core/src/client/blocks/library/AnnotatedCodeBlock.tsx +33 -18
- package/corpus/core/src/client/blocks/library/annotation-rail.tsx +3 -4
- package/corpus/core/src/client/components/CodeRequiredDialog.tsx +5 -8
- package/corpus/core/src/server/agent-chat-plugin.ts +7 -7
- package/corpus/core/src/server/auth.ts +7 -0
- package/corpus/core/src/server/onboarding-html.ts +95 -9
- package/corpus/core/src/server/self-dispatch.ts +8 -1
- package/corpus/core/src/templates/default/AGENTS.md +4 -0
- package/corpus/core/src/templates/default/package.json +2 -1
- package/corpus/core/src/templates/headless/AGENTS.md +4 -0
- package/corpus/core/src/templates/headless/package.json +1 -0
- package/corpus/core/src/templates/workspace-root/AGENTS.md +4 -0
- package/corpus/core/src/templates/workspace-root/README.md +4 -0
- package/corpus/core/src/templates/workspace-root/package.json +1 -0
- package/corpus/templates/analytics/actions/compose-dashboard.ts +34 -0
- package/corpus/templates/analytics/actions/install-dashboard-template.ts +51 -3
- package/corpus/templates/analytics/actions/update-dashboard.ts +3 -4
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +175 -48
- package/corpus/templates/analytics/app/global.css +15 -13
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +9 -32
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +0 -26
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +0 -23
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/pivot.ts +65 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +3 -4
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-charts-now-fill-sparse-template-time-series-suppor.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-filters-now-apply-consistently-to-first-party-traf.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-rows-now-auto-fit-panels-so-one-two-or-three-chart.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-traffic-tables-now-show-top-urls-and-shared-clips-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-first-party-signup-and-pageview-panels-now-honor-dashboard-filters.md +5 -0
- package/corpus/templates/analytics/changelog/2026-06-24-first-party-template-dashboards-now-include-repeat-users-ret.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-retention-charts-now-break-out-1-day-and-7-day-return-rates-.md +6 -0
- package/corpus/templates/analytics/seeds/dashboards/agent-native-templates-first-party.json +330 -49
- package/corpus/templates/analytics/server/lib/dashboard-catalog.ts +7 -2
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +35 -8
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +337 -45
- package/corpus/templates/calendar/changelog/2026-06-23-added-a-heads-up-explaining-google-s-app-not-verified-screen.md +1 -1
- package/corpus/templates/calendar/server/plugins/auth.ts +12 -0
- package/corpus/templates/clips/.agents/skills/recording/SKILL.md +7 -7
- package/corpus/templates/clips/AGENTS.md +6 -2
- package/corpus/templates/clips/app/components/capture-install-options.tsx +20 -4
- package/corpus/templates/clips/app/components/library/library-layout.tsx +7 -0
- package/corpus/templates/clips/app/components/player/player-controls.tsx +62 -32
- package/corpus/templates/clips/app/lib/capture-install-options.ts +39 -5
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +786 -323
- package/corpus/templates/clips/app/routes/download.tsx +2 -2
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +1 -1
- package/corpus/templates/clips/changelog/2026-06-24-inline-slack-playback-controls-no-longer-shift-when-adjustin.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-24-settings-now-puts-what-s-new-in-a-compact-sidebar-prioritize.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-console-log-stream-all-le.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-24-shared-clips-now-expose-their-full-fetch-xhr-network-request.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-24-the-chrome-extension-option-now-only-appears-on-supported-hosts.md +5 -0
- package/corpus/templates/clips/chrome-extension/STORE_DRAFT.md +13 -8
- package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
- package/corpus/templates/clips/chrome-extension/scripts/package.ts +15 -2
- package/corpus/templates/clips/chrome-extension/src/overlay.css +4 -25
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +1 -3
- package/corpus/templates/clips/server/lib/public-agent-context.ts +67 -24
- package/corpus/templates/mail/server/plugins/auth.ts +4 -1
- package/corpus/templates/plan/app/pages/PlansPage.tsx +271 -197
- package/corpus/templates/plan/changelog/2026-06-24-annotated-code-callouts-now-use-numbered-gutter-markers-that.md +6 -0
- package/corpus/templates/plan/server/plan-content.ts +45 -2
- package/dist/agent/durable-background.d.ts +9 -4
- package/dist/agent/durable-background.d.ts.map +1 -1
- package/dist/agent/durable-background.js +34 -11
- package/dist/agent/durable-background.js.map +1 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +52 -8
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/create.js +1 -1
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +361 -19
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/ConnectBuilderCard.d.ts.map +1 -1
- package/dist/client/ConnectBuilderCard.js +6 -6
- package/dist/client/ConnectBuilderCard.js.map +1 -1
- package/dist/client/NewWorkspaceAppFlow.js +1 -1
- package/dist/client/NewWorkspaceAppFlow.js.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/AnnotatedCodeBlock.js +14 -10
- package/dist/client/blocks/library/AnnotatedCodeBlock.js.map +1 -1
- package/dist/client/blocks/library/annotation-rail.d.ts +3 -4
- package/dist/client/blocks/library/annotation-rail.d.ts.map +1 -1
- package/dist/client/blocks/library/annotation-rail.js +3 -4
- package/dist/client/blocks/library/annotation-rail.js.map +1 -1
- package/dist/client/components/CodeRequiredDialog.d.ts.map +1 -1
- package/dist/client/components/CodeRequiredDialog.js +2 -1
- package/dist/client/components/CodeRequiredDialog.js.map +1 -1
- package/dist/server/agent-chat-plugin.js +7 -7
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/auth.d.ts +7 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +1 -0
- package/dist/server/auth.js.map +1 -1
- package/dist/server/onboarding-html.d.ts +15 -0
- package/dist/server/onboarding-html.d.ts.map +1 -1
- package/dist/server/onboarding-html.js +73 -9
- package/dist/server/onboarding-html.js.map +1 -1
- package/dist/server/self-dispatch.d.ts.map +1 -1
- package/dist/server/self-dispatch.js +5 -1
- package/dist/server/self-dispatch.js.map +1 -1
- package/dist/templates/default/AGENTS.md +4 -0
- package/dist/templates/default/package.json +2 -1
- package/dist/templates/headless/AGENTS.md +4 -0
- package/dist/templates/headless/package.json +1 -0
- package/dist/templates/workspace-root/AGENTS.md +4 -0
- package/dist/templates/workspace-root/README.md +4 -0
- package/dist/templates/workspace-root/package.json +1 -0
- package/docs/content/skills-guide.md +14 -0
- package/docs/content/template-clips.md +7 -1
- package/package.json +1 -1
- package/src/templates/default/AGENTS.md +4 -0
- package/src/templates/default/package.json +2 -1
- package/src/templates/headless/AGENTS.md +4 -0
- package/src/templates/headless/package.json +1 -0
- package/src/templates/workspace-root/AGENTS.md +4 -0
- package/src/templates/workspace-root/README.md +4 -0
- package/src/templates/workspace-root/package.json +1 -0
|
@@ -265,9 +265,8 @@ function validateDashboardConfig(
|
|
|
265
265
|
if (!p || typeof p !== "object") {
|
|
266
266
|
return `panel[${i}] must be an object`;
|
|
267
267
|
}
|
|
268
|
-
// Section panels are pure layout dividers
|
|
269
|
-
//
|
|
270
|
-
// grid renders them.
|
|
268
|
+
// Section panels are pure layout dividers, so source and sql are optional.
|
|
269
|
+
// Width stays required for backward-compatible dashboard payloads.
|
|
271
270
|
const isSection = p.chartType === "section";
|
|
272
271
|
const required = isSection
|
|
273
272
|
? (["id", "title", "chartType", "width"] as const)
|
|
@@ -276,7 +275,7 @@ function validateDashboardConfig(
|
|
|
276
275
|
const v = p[field];
|
|
277
276
|
if (field === "width") {
|
|
278
277
|
if (!isValidColumnCount(v)) {
|
|
279
|
-
return `panel[${i}].width must be an integer between 1 and 6 (
|
|
278
|
+
return `panel[${i}].width must be an integer between 1 and 6 (legacy layout field)`;
|
|
280
279
|
}
|
|
281
280
|
continue;
|
|
282
281
|
}
|
|
@@ -240,39 +240,114 @@ function shouldShowLegend(panel: SqlPanel, seriesCount: number): boolean {
|
|
|
240
240
|
return panel.config?.legend !== false && seriesCount > 0;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
function numericTooltipValue(value: unknown): number {
|
|
244
|
+
const numeric = typeof value === "number" ? value : Number(value);
|
|
245
|
+
return Number.isFinite(numeric) ? numeric : Number.NEGATIVE_INFINITY;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function sortTooltipPayloadItems<
|
|
249
|
+
T extends {
|
|
250
|
+
value?: unknown;
|
|
251
|
+
},
|
|
252
|
+
>(items: T[]): T[] {
|
|
253
|
+
return items
|
|
254
|
+
.map((item, index) => ({ item, index }))
|
|
255
|
+
.sort((a, b) => {
|
|
256
|
+
const diff =
|
|
257
|
+
numericTooltipValue(b.item.value) - numericTooltipValue(a.item.value);
|
|
258
|
+
return diff !== 0 ? diff : a.index - b.index;
|
|
259
|
+
})
|
|
260
|
+
.map(({ item }) => item);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function useSeriesVisibility(keys: string[]): {
|
|
264
|
+
hiddenKeys: Set<string>;
|
|
265
|
+
visibleKeys: string[];
|
|
266
|
+
toggleSeries: (key: string) => void;
|
|
267
|
+
} {
|
|
268
|
+
const [hiddenKeys, setHiddenKeys] = useState<Set<string>>(() => new Set());
|
|
269
|
+
|
|
270
|
+
useEffect(() => {
|
|
271
|
+
setHiddenKeys((prev) => {
|
|
272
|
+
const next = new Set(
|
|
273
|
+
Array.from(prev).filter((key) => keys.includes(key)),
|
|
274
|
+
);
|
|
275
|
+
return next.size === prev.size ? prev : next;
|
|
276
|
+
});
|
|
277
|
+
}, [keys]);
|
|
278
|
+
|
|
279
|
+
const visibleKeys = useMemo(
|
|
280
|
+
() => keys.filter((key) => !hiddenKeys.has(key)),
|
|
281
|
+
[hiddenKeys, keys],
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const toggleSeries = useCallback(
|
|
285
|
+
(key: string) => {
|
|
286
|
+
setHiddenKeys((prev) => {
|
|
287
|
+
const next = new Set(prev);
|
|
288
|
+
if (next.has(key)) {
|
|
289
|
+
next.delete(key);
|
|
290
|
+
return next;
|
|
291
|
+
}
|
|
292
|
+
const visibleCount = keys.filter((k) => !next.has(k)).length;
|
|
293
|
+
if (visibleCount <= 1) return prev;
|
|
294
|
+
next.add(key);
|
|
295
|
+
return next;
|
|
296
|
+
});
|
|
297
|
+
},
|
|
298
|
+
[keys],
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
return { hiddenKeys, visibleKeys, toggleSeries };
|
|
302
|
+
}
|
|
303
|
+
|
|
243
304
|
function SeriesLegend({
|
|
244
305
|
keys,
|
|
245
306
|
colors,
|
|
246
307
|
panel,
|
|
308
|
+
hiddenKeys,
|
|
309
|
+
onToggleKey,
|
|
247
310
|
}: {
|
|
248
311
|
keys: string[];
|
|
249
312
|
colors: string[];
|
|
250
313
|
panel: SqlPanel;
|
|
314
|
+
hiddenKeys?: Set<string>;
|
|
315
|
+
onToggleKey?: (key: string) => void;
|
|
251
316
|
}) {
|
|
252
|
-
if (
|
|
253
|
-
!usesPrometheusPresentation(panel) ||
|
|
254
|
-
!shouldShowLegend(panel, keys.length)
|
|
255
|
-
)
|
|
256
|
-
return null;
|
|
317
|
+
if (!shouldShowLegend(panel, keys.length)) return null;
|
|
257
318
|
|
|
258
319
|
return (
|
|
259
320
|
<div className="mt-2 max-h-16 overflow-y-auto overflow-x-hidden pr-1 text-[11px] leading-4 text-muted-foreground">
|
|
260
321
|
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
|
261
|
-
{keys.map((key, i) =>
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
{
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
322
|
+
{keys.map((key, i) => {
|
|
323
|
+
const hidden = hiddenKeys?.has(key) ?? false;
|
|
324
|
+
const label = formatSeriesLabelForPanel(panel, key);
|
|
325
|
+
const color = colors[i % colors.length];
|
|
326
|
+
return (
|
|
327
|
+
<button
|
|
328
|
+
key={key}
|
|
329
|
+
type="button"
|
|
330
|
+
aria-pressed={!hidden}
|
|
331
|
+
className={`inline-flex max-w-[14rem] items-center gap-1.5 rounded-sm text-left transition-opacity hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring ${
|
|
332
|
+
hidden ? "opacity-35" : "opacity-100"
|
|
333
|
+
} ${onToggleKey ? "cursor-pointer" : "cursor-default"}`}
|
|
334
|
+
title={label}
|
|
335
|
+
onClick={() => onToggleKey?.(key)}
|
|
336
|
+
>
|
|
337
|
+
<span className="relative h-2.5 w-3 shrink-0">
|
|
338
|
+
<span
|
|
339
|
+
className="absolute left-0 right-0 top-1/2 h-px -translate-y-1/2"
|
|
340
|
+
style={{ backgroundColor: color }}
|
|
341
|
+
/>
|
|
342
|
+
<span
|
|
343
|
+
className="absolute left-1/2 top-1/2 h-1.5 w-1.5 -translate-x-1/2 -translate-y-1/2 rounded-full"
|
|
344
|
+
style={{ backgroundColor: color }}
|
|
345
|
+
/>
|
|
346
|
+
</span>
|
|
347
|
+
<span className="truncate">{label}</span>
|
|
348
|
+
</button>
|
|
349
|
+
);
|
|
350
|
+
})}
|
|
276
351
|
</div>
|
|
277
352
|
</div>
|
|
278
353
|
);
|
|
@@ -296,17 +371,25 @@ function ChartFrame({
|
|
|
296
371
|
panel,
|
|
297
372
|
legendKeys,
|
|
298
373
|
colors,
|
|
374
|
+
hiddenKeys,
|
|
375
|
+
onToggleLegendKey,
|
|
376
|
+
showCustomLegend = false,
|
|
299
377
|
children,
|
|
300
378
|
}: {
|
|
301
379
|
panel: SqlPanel;
|
|
302
380
|
legendKeys: string[];
|
|
303
381
|
colors: string[];
|
|
382
|
+
hiddenKeys?: Set<string>;
|
|
383
|
+
onToggleLegendKey?: (key: string) => void;
|
|
384
|
+
showCustomLegend?: boolean;
|
|
304
385
|
children: ReactNode;
|
|
305
386
|
}) {
|
|
306
387
|
const fill = useContext(ChartFillHeightContext);
|
|
307
388
|
const chartHeight = fill ? "h-full min-h-[250px]" : "h-[250px]";
|
|
308
389
|
|
|
309
|
-
|
|
390
|
+
const renderLegend = showCustomLegend || usesPrometheusPresentation(panel);
|
|
391
|
+
|
|
392
|
+
if (!renderLegend) {
|
|
310
393
|
return (
|
|
311
394
|
<div className={`${chartHeight} w-full overflow-visible`}>{children}</div>
|
|
312
395
|
);
|
|
@@ -321,7 +404,13 @@ function ChartFrame({
|
|
|
321
404
|
>
|
|
322
405
|
{children}
|
|
323
406
|
</div>
|
|
324
|
-
<SeriesLegend
|
|
407
|
+
<SeriesLegend
|
|
408
|
+
keys={legendKeys}
|
|
409
|
+
colors={colors}
|
|
410
|
+
panel={panel}
|
|
411
|
+
hiddenKeys={hiddenKeys}
|
|
412
|
+
onToggleKey={onToggleLegendKey}
|
|
413
|
+
/>
|
|
325
414
|
</div>
|
|
326
415
|
);
|
|
327
416
|
}
|
|
@@ -347,8 +436,9 @@ function ChartTooltip({
|
|
|
347
436
|
valueFormatter?: (value: number) => string;
|
|
348
437
|
}) {
|
|
349
438
|
const tooltipRef = useChartTooltipFlip<HTMLDivElement>();
|
|
350
|
-
const items =
|
|
351
|
-
payload?.filter((item) => item.value != null && item.value !== "") ?? []
|
|
439
|
+
const items = sortTooltipPayloadItems(
|
|
440
|
+
payload?.filter((item) => item.value != null && item.value !== "") ?? [],
|
|
441
|
+
);
|
|
352
442
|
if (!active || items.length === 0) return null;
|
|
353
443
|
|
|
354
444
|
const labelText =
|
|
@@ -722,6 +812,24 @@ function formatCell(value: unknown, format: ColumnFormat | undefined): string {
|
|
|
722
812
|
return String(value);
|
|
723
813
|
}
|
|
724
814
|
|
|
815
|
+
export function safeDashboardLinkHref(value: unknown): string | null {
|
|
816
|
+
if (typeof value !== "string") return null;
|
|
817
|
+
const href = value.trim();
|
|
818
|
+
if (!href) return null;
|
|
819
|
+
if (href.startsWith("//")) return null;
|
|
820
|
+
|
|
821
|
+
try {
|
|
822
|
+
const parsed = href.startsWith("/")
|
|
823
|
+
? new URL(href, "https://agent-native.local")
|
|
824
|
+
: new URL(href);
|
|
825
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:"
|
|
826
|
+
? href
|
|
827
|
+
: null;
|
|
828
|
+
} catch {
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
725
833
|
function renderDeltaCell(value: unknown): ReactNode {
|
|
726
834
|
if (value == null || typeof value !== "number" || Number.isNaN(value)) {
|
|
727
835
|
return <span className="text-muted-foreground">-</span>;
|
|
@@ -893,19 +1001,24 @@ function TableRenderer({
|
|
|
893
1001
|
const href = col.linkKey
|
|
894
1002
|
? String(row[col.linkKey] ?? "")
|
|
895
1003
|
: String(raw ?? "");
|
|
1004
|
+
const safeHref = safeDashboardLinkHref(href);
|
|
896
1005
|
return (
|
|
897
1006
|
<td
|
|
898
1007
|
key={col.key}
|
|
899
1008
|
className="py-1.5 px-2 whitespace-nowrap"
|
|
900
1009
|
>
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1010
|
+
{safeHref ? (
|
|
1011
|
+
<a
|
|
1012
|
+
href={safeHref}
|
|
1013
|
+
target="_blank"
|
|
1014
|
+
rel="noopener noreferrer"
|
|
1015
|
+
className="text-primary hover:underline"
|
|
1016
|
+
>
|
|
1017
|
+
{formatted}
|
|
1018
|
+
</a>
|
|
1019
|
+
) : (
|
|
1020
|
+
formatted
|
|
1021
|
+
)}
|
|
909
1022
|
</td>
|
|
910
1023
|
);
|
|
911
1024
|
}
|
|
@@ -1067,9 +1180,17 @@ function BarRenderer({
|
|
|
1067
1180
|
formatXLabel(String(value ?? ""), panel);
|
|
1068
1181
|
const seriesNameFormatter = (name: string) =>
|
|
1069
1182
|
formatSeriesLabelForPanel(panel, name);
|
|
1183
|
+
const { hiddenKeys, toggleSeries } = useSeriesVisibility(yKeys);
|
|
1070
1184
|
|
|
1071
1185
|
return (
|
|
1072
|
-
<ChartFrame
|
|
1186
|
+
<ChartFrame
|
|
1187
|
+
panel={panel}
|
|
1188
|
+
legendKeys={yKeys}
|
|
1189
|
+
colors={colors}
|
|
1190
|
+
hiddenKeys={hiddenKeys}
|
|
1191
|
+
onToggleLegendKey={toggleSeries}
|
|
1192
|
+
showCustomLegend
|
|
1193
|
+
>
|
|
1073
1194
|
<ResponsiveContainer width="100%" height="100%">
|
|
1074
1195
|
<BarChart data={rows}>
|
|
1075
1196
|
<XAxis
|
|
@@ -1104,10 +1225,6 @@ function BarRenderer({
|
|
|
1104
1225
|
}
|
|
1105
1226
|
itemSorter={(item) => -(Number(item.value) || 0)}
|
|
1106
1227
|
/>
|
|
1107
|
-
{!usesPrometheusPresentation(panel) &&
|
|
1108
|
-
shouldShowLegend(panel, yKeys.length) && (
|
|
1109
|
-
<Legend {...CHART_LEGEND_PROPS} />
|
|
1110
|
-
)}
|
|
1111
1228
|
{yKeys.map((key, i) => (
|
|
1112
1229
|
<Bar
|
|
1113
1230
|
key={key}
|
|
@@ -1118,6 +1235,7 @@ function BarRenderer({
|
|
|
1118
1235
|
stacked && i < yKeys.length - 1 ? [0, 0, 0, 0] : [4, 4, 0, 0]
|
|
1119
1236
|
}
|
|
1120
1237
|
stackId={stacked ? "stack" : undefined}
|
|
1238
|
+
hide={hiddenKeys.has(key)}
|
|
1121
1239
|
/>
|
|
1122
1240
|
))}
|
|
1123
1241
|
</BarChart>
|
|
@@ -1149,10 +1267,18 @@ function TimeSeriesRenderer({
|
|
|
1149
1267
|
formatXLabel(String(value ?? ""), panel);
|
|
1150
1268
|
const seriesNameFormatter = (name: string) =>
|
|
1151
1269
|
formatSeriesLabelForPanel(panel, name);
|
|
1270
|
+
const { hiddenKeys, visibleKeys, toggleSeries } = useSeriesVisibility(yKeys);
|
|
1152
1271
|
|
|
1153
1272
|
if (chartType === "line") {
|
|
1154
1273
|
return (
|
|
1155
|
-
<ChartFrame
|
|
1274
|
+
<ChartFrame
|
|
1275
|
+
panel={panel}
|
|
1276
|
+
legendKeys={yKeys}
|
|
1277
|
+
colors={colors}
|
|
1278
|
+
hiddenKeys={hiddenKeys}
|
|
1279
|
+
onToggleLegendKey={toggleSeries}
|
|
1280
|
+
showCustomLegend
|
|
1281
|
+
>
|
|
1156
1282
|
<ResponsiveContainer width="100%" height="100%">
|
|
1157
1283
|
<LineChart data={rows}>
|
|
1158
1284
|
<XAxis
|
|
@@ -1187,10 +1313,6 @@ function TimeSeriesRenderer({
|
|
|
1187
1313
|
}
|
|
1188
1314
|
itemSorter={(item) => -(Number(item.value) || 0)}
|
|
1189
1315
|
/>
|
|
1190
|
-
{!usesPrometheusPresentation(panel) &&
|
|
1191
|
-
shouldShowLegend(panel, yKeys.length) && (
|
|
1192
|
-
<Legend {...CHART_LEGEND_PROPS} />
|
|
1193
|
-
)}
|
|
1194
1316
|
{yKeys.map((key, i) => (
|
|
1195
1317
|
<Line
|
|
1196
1318
|
key={key}
|
|
@@ -1200,6 +1322,7 @@ function TimeSeriesRenderer({
|
|
|
1200
1322
|
stroke={colors[i % colors.length]}
|
|
1201
1323
|
strokeWidth={2}
|
|
1202
1324
|
dot={false}
|
|
1325
|
+
hide={hiddenKeys.has(key)}
|
|
1203
1326
|
/>
|
|
1204
1327
|
))}
|
|
1205
1328
|
</LineChart>
|
|
@@ -1211,10 +1334,17 @@ function TimeSeriesRenderer({
|
|
|
1211
1334
|
// With multiple series, filled areas stack and obscure lines behind them,
|
|
1212
1335
|
// so only draw the gradient fill when there's a single series — unless
|
|
1213
1336
|
// the caller asked for an explicit stacked area.
|
|
1214
|
-
const showFill =
|
|
1337
|
+
const showFill = visibleKeys.length === 1 || stacked;
|
|
1215
1338
|
|
|
1216
1339
|
return (
|
|
1217
|
-
<ChartFrame
|
|
1340
|
+
<ChartFrame
|
|
1341
|
+
panel={panel}
|
|
1342
|
+
legendKeys={yKeys}
|
|
1343
|
+
colors={colors}
|
|
1344
|
+
hiddenKeys={hiddenKeys}
|
|
1345
|
+
onToggleLegendKey={toggleSeries}
|
|
1346
|
+
showCustomLegend
|
|
1347
|
+
>
|
|
1218
1348
|
<ResponsiveContainer width="100%" height="100%">
|
|
1219
1349
|
<AreaChart data={rows}>
|
|
1220
1350
|
{showFill && (
|
|
@@ -1274,10 +1404,6 @@ function TimeSeriesRenderer({
|
|
|
1274
1404
|
}
|
|
1275
1405
|
itemSorter={(item) => -(Number(item.value) || 0)}
|
|
1276
1406
|
/>
|
|
1277
|
-
{!usesPrometheusPresentation(panel) &&
|
|
1278
|
-
shouldShowLegend(panel, yKeys.length) && (
|
|
1279
|
-
<Legend {...CHART_LEGEND_PROPS} />
|
|
1280
|
-
)}
|
|
1281
1407
|
{yKeys.map((key, i) => (
|
|
1282
1408
|
<Area
|
|
1283
1409
|
key={key}
|
|
@@ -1289,6 +1415,7 @@ function TimeSeriesRenderer({
|
|
|
1289
1415
|
fillOpacity={showFill ? 1 : 0}
|
|
1290
1416
|
fill={showFill ? `url(#sql-gradient-${key})` : "none"}
|
|
1291
1417
|
stackId={stacked ? "stack" : undefined}
|
|
1418
|
+
hide={hiddenKeys.has(key)}
|
|
1292
1419
|
/>
|
|
1293
1420
|
))}
|
|
1294
1421
|
</AreaChart>
|
|
@@ -314,31 +314,33 @@
|
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
/* The dashboard grid responds to the available content width — which shrinks
|
|
317
|
-
when the agent sidebar is open — not the viewport.
|
|
318
|
-
|
|
319
|
-
configured column count. Mirrors templates/macros/app/global.css. */
|
|
317
|
+
when the agent sidebar is open — not the viewport. Rows auto-fit like
|
|
318
|
+
Amplitude: a partial row stretches its cards across the available width. */
|
|
320
319
|
.dashboard-grid-container {
|
|
321
320
|
container: dashboard-grid / inline-size;
|
|
322
321
|
}
|
|
323
322
|
|
|
324
323
|
.dashboard-grid {
|
|
325
|
-
|
|
326
|
-
|
|
324
|
+
--dashboard-grid-gap: 1rem;
|
|
325
|
+
display: flex;
|
|
326
|
+
flex-wrap: wrap;
|
|
327
327
|
align-items: stretch;
|
|
328
|
-
gap:
|
|
329
|
-
grid-template-columns: minmax(0, 1fr);
|
|
328
|
+
gap: var(--dashboard-grid-gap);
|
|
330
329
|
}
|
|
331
330
|
|
|
332
331
|
.dashboard-grid > .dashboard-grid-cell {
|
|
333
|
-
|
|
332
|
+
flex: 1 1 100%;
|
|
333
|
+
min-width: 0;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
@container dashboard-grid (min-width: 768px) {
|
|
337
|
-
.dashboard-grid {
|
|
338
|
-
grid-template-columns: repeat(var(--dash-cols), minmax(0, 1fr));
|
|
339
|
-
}
|
|
340
|
-
|
|
341
337
|
.dashboard-grid > .dashboard-grid-cell {
|
|
342
|
-
|
|
338
|
+
flex-basis: max(
|
|
339
|
+
0px,
|
|
340
|
+
calc(
|
|
341
|
+
(100% - (var(--dash-cols) - 1) * var(--dashboard-grid-gap)) /
|
|
342
|
+
var(--dash-cols)
|
|
343
|
+
)
|
|
344
|
+
);
|
|
343
345
|
}
|
|
344
346
|
}
|
|
@@ -73,6 +73,8 @@ export interface PanelFormValues {
|
|
|
73
73
|
title: string;
|
|
74
74
|
chartType: ChartType;
|
|
75
75
|
source: DataSourceType;
|
|
76
|
+
/** Legacy storage field retained for existing dashboards. Row widths are
|
|
77
|
+
* now inferred from how many panels share the row. */
|
|
76
78
|
width: number;
|
|
77
79
|
/** Section panels only: number of grid columns the panels following this
|
|
78
80
|
* section should use. Ignored when `chartType` is not `"section"`. */
|
|
@@ -226,7 +228,8 @@ function PanelEditorContent({
|
|
|
226
228
|
`If no source can answer, report the exact unavailable/error result instead of saving a panel with guessed schema or metrics. ` +
|
|
227
229
|
`Use the \`update-dashboard\` action with ops=[{op:'insert', path:'/panels/-', value: <panel>}] ` +
|
|
228
230
|
`to append, or an appropriate index to place the panel in the right spot. ` +
|
|
229
|
-
`Panel shape: { id (unique slug), title, sql, source ('bigquery'|'ga4'|'amplitude'|'first-party'|'demo'|'prometheus'), chartType ('line'|'area'|'bar'|'metric'|'table'|'pie'|'section'), width (
|
|
231
|
+
`Panel shape: { id (unique slug), title, sql, source ('bigquery'|'ga4'|'amplitude'|'first-party'|'demo'|'prometheus'), chartType ('line'|'area'|'bar'|'metric'|'table'|'pie'|'section'), 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? }. ` +
|
|
232
|
+
`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. ` +
|
|
230
233
|
`For amplitude panels, sql is a JSON descriptor: {"event":"event name","groupBy":"property","days":30}. ` +
|
|
231
234
|
`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. ` +
|
|
232
235
|
`For demo panels, sql uses the same Prometheus JSON descriptor shape as source 'prometheus': {"promql":"rate(http_requests_total[5m])","mode":"range","range":"1h","step":"30s"}. ` +
|
|
@@ -310,11 +313,9 @@ function PanelEditorContent({
|
|
|
310
313
|
</Select>
|
|
311
314
|
</div>
|
|
312
315
|
|
|
313
|
-
|
|
314
|
-
<
|
|
315
|
-
|
|
316
|
-
</Label>
|
|
317
|
-
{form.chartType === "section" ? (
|
|
316
|
+
{form.chartType === "section" ? (
|
|
317
|
+
<div className="grid gap-1.5">
|
|
318
|
+
<Label>Section columns</Label>
|
|
318
319
|
<ToggleGroup
|
|
319
320
|
type="single"
|
|
320
321
|
value={String(form.columns)}
|
|
@@ -338,32 +339,8 @@ function PanelEditorContent({
|
|
|
338
339
|
</ToggleGroupItem>
|
|
339
340
|
))}
|
|
340
341
|
</ToggleGroup>
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
type="single"
|
|
344
|
-
value={String(form.width)}
|
|
345
|
-
onValueChange={(v) => {
|
|
346
|
-
if (!v) return;
|
|
347
|
-
const next = clampPanelWidth(Number(v), MAX_DASHBOARD_COLUMNS);
|
|
348
|
-
setForm((f) => ({ ...f, width: next }));
|
|
349
|
-
}}
|
|
350
|
-
className="justify-start h-9"
|
|
351
|
-
>
|
|
352
|
-
{Array.from(
|
|
353
|
-
{ length: MAX_DASHBOARD_COLUMNS },
|
|
354
|
-
(_, i) => i + 1,
|
|
355
|
-
).map((n) => (
|
|
356
|
-
<ToggleGroupItem
|
|
357
|
-
key={n}
|
|
358
|
-
value={String(n)}
|
|
359
|
-
className="h-9 w-9 px-0 text-xs"
|
|
360
|
-
>
|
|
361
|
-
{n}
|
|
362
|
-
</ToggleGroupItem>
|
|
363
|
-
))}
|
|
364
|
-
</ToggleGroup>
|
|
365
|
-
)}
|
|
366
|
-
</div>
|
|
342
|
+
</div>
|
|
343
|
+
) : null}
|
|
367
344
|
</div>
|
|
368
345
|
|
|
369
346
|
<div className="grid gap-1.5">
|
|
@@ -2,8 +2,6 @@ import { useCallback, useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
3
3
|
import {
|
|
4
4
|
IconGripVertical,
|
|
5
|
-
IconArrowsMaximize,
|
|
6
|
-
IconArrowsMinimize,
|
|
7
5
|
IconDotsVertical,
|
|
8
6
|
IconMaximize,
|
|
9
7
|
IconPencil,
|
|
@@ -49,13 +47,6 @@ interface SqlChartCardProps {
|
|
|
49
47
|
panel: SqlPanel;
|
|
50
48
|
resolvedSql?: string;
|
|
51
49
|
onRemove: () => void;
|
|
52
|
-
/** Toggle between "span 1 column" and "span all columns of the current
|
|
53
|
-
* section". Optional because section panels never expose this control. */
|
|
54
|
-
onToggleWidth?: () => void;
|
|
55
|
-
/** Number of columns in the section this panel currently lives in. Used to
|
|
56
|
-
* decide the toggle label / icon: when the panel already spans the full
|
|
57
|
-
* row, we offer to shrink; otherwise we offer to expand. */
|
|
58
|
-
gridColumns?: number;
|
|
59
50
|
onEdit?: () => void;
|
|
60
51
|
/** Persist a SQL-only edit from the inline View SQL popover. Should throw on
|
|
61
52
|
* validation failure so the popover can stay open and surface the error. */
|
|
@@ -67,8 +58,6 @@ export function SqlChartCard({
|
|
|
67
58
|
panel,
|
|
68
59
|
resolvedSql,
|
|
69
60
|
onRemove,
|
|
70
|
-
onToggleWidth,
|
|
71
|
-
gridColumns,
|
|
72
61
|
onEdit,
|
|
73
62
|
onSaveSql,
|
|
74
63
|
editable = true,
|
|
@@ -300,21 +289,6 @@ export function SqlChartCard({
|
|
|
300
289
|
</DropdownMenuItem>
|
|
301
290
|
</ViewSqlPopover>
|
|
302
291
|
) : null}
|
|
303
|
-
{editable && onToggleWidth && (gridColumns ?? 2) > 1 && (
|
|
304
|
-
<DropdownMenuItem onSelect={onToggleWidth}>
|
|
305
|
-
{panel.width >= (gridColumns ?? 2) ? (
|
|
306
|
-
<>
|
|
307
|
-
<IconArrowsMinimize className="h-4 w-4 mr-2" />
|
|
308
|
-
Span 1 column
|
|
309
|
-
</>
|
|
310
|
-
) : (
|
|
311
|
-
<>
|
|
312
|
-
<IconArrowsMaximize className="h-4 w-4 mr-2" />
|
|
313
|
-
Span full row
|
|
314
|
-
</>
|
|
315
|
-
)}
|
|
316
|
-
</DropdownMenuItem>
|
|
317
|
-
)}
|
|
318
292
|
{editable ? <DropdownMenuSeparator /> : null}
|
|
319
293
|
{editable && onEdit && (
|
|
320
294
|
<DropdownMenuItem onSelect={() => onEdit()}>
|
|
@@ -79,7 +79,6 @@ import { ViewsMenu } from "./ViewsMenu";
|
|
|
79
79
|
import BlankDashboard from "../BlankDashboard";
|
|
80
80
|
import {
|
|
81
81
|
clampDashboardColumns,
|
|
82
|
-
clampPanelWidth,
|
|
83
82
|
DEFAULT_DASHBOARD_COLUMNS,
|
|
84
83
|
type SqlDashboardConfig,
|
|
85
84
|
type SqlPanel,
|
|
@@ -632,22 +631,6 @@ export default function SqlDashboardPage() {
|
|
|
632
631
|
[dashboard, persist],
|
|
633
632
|
);
|
|
634
633
|
|
|
635
|
-
const toggleWidth = useCallback(
|
|
636
|
-
(panelId: string, gridColumns: number) => {
|
|
637
|
-
if (!dashboard) return;
|
|
638
|
-
const max = clampDashboardColumns(gridColumns);
|
|
639
|
-
persist({
|
|
640
|
-
...dashboard,
|
|
641
|
-
panels: dashboard.panels.map((p) => {
|
|
642
|
-
if (p.id !== panelId) return p;
|
|
643
|
-
const current = clampPanelWidth(p.width, max);
|
|
644
|
-
return { ...p, width: current >= max ? 1 : max };
|
|
645
|
-
}),
|
|
646
|
-
});
|
|
647
|
-
},
|
|
648
|
-
[dashboard, persist],
|
|
649
|
-
);
|
|
650
|
-
|
|
651
634
|
const handleSavePanel = useCallback(
|
|
652
635
|
async (panel: SqlPanel) => {
|
|
653
636
|
if (!dashboard) return;
|
|
@@ -1349,14 +1332,12 @@ export default function SqlDashboardPage() {
|
|
|
1349
1332
|
}
|
|
1350
1333
|
: panel;
|
|
1351
1334
|
const remoteEditor = remoteEditingPanels.get(panel.id);
|
|
1352
|
-
const span = clampPanelWidth(panel.width, group.columns);
|
|
1353
1335
|
return (
|
|
1354
1336
|
<div
|
|
1355
1337
|
key={panel.id}
|
|
1356
1338
|
className="dashboard-grid-cell relative h-full"
|
|
1357
1339
|
style={
|
|
1358
1340
|
{
|
|
1359
|
-
"--panel-span": span,
|
|
1360
1341
|
...(remoteEditor
|
|
1361
1342
|
? {
|
|
1362
1343
|
outline: `2px solid ${remoteEditor.color}`,
|
|
@@ -1384,11 +1365,7 @@ export default function SqlDashboardPage() {
|
|
|
1384
1365
|
serializePanelSql(panel.sql),
|
|
1385
1366
|
vars,
|
|
1386
1367
|
)}
|
|
1387
|
-
gridColumns={group.columns}
|
|
1388
1368
|
onRemove={() => removePanel(panel.id)}
|
|
1389
|
-
onToggleWidth={() =>
|
|
1390
|
-
toggleWidth(panel.id, group.columns)
|
|
1391
|
-
}
|
|
1392
1369
|
onEdit={() => openEditPanel(panel)}
|
|
1393
1370
|
onSaveSql={(sql) => handleSavePanel({ ...panel, sql })}
|
|
1394
1371
|
editable={canEdit}
|