@agent-native/core 0.80.5 → 0.80.7
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 +13 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/create.ts +1 -0
- package/corpus/core/src/client/extensions/EmbeddedExtension.tsx +45 -0
- package/corpus/core/src/sharing/registry.ts +17 -0
- package/corpus/core/src/templates/default/pnpm-workspace.yaml +1 -0
- package/corpus/core/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +42 -4
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
- package/corpus/templates/analytics/actions/update-dashboard.ts +23 -9
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +72 -1
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +34 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +88 -2
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +7 -1
- package/corpus/templates/analytics/changelog/2026-06-29-dashboards-can-now-include-extension-panels-that-embed-a-san.md +6 -0
- package/corpus/templates/content/AGENTS.md +45 -44
- package/corpus/templates/content/README.md +16 -0
- package/corpus/templates/content/actions/_builder-cms-read-client.ts +125 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +28 -6
- package/corpus/templates/content/actions/_builder-cms-write-adapter.ts +238 -69
- package/corpus/templates/content/actions/_builder-cms-write-settings.ts +125 -43
- package/corpus/templates/content/actions/_database-source-utils.ts +746 -97
- package/corpus/templates/content/actions/_database-utils.ts +17 -14
- package/corpus/templates/content/actions/_federation-join.ts +14 -2
- package/corpus/templates/content/actions/_join-suggestion.ts +89 -14
- package/corpus/templates/content/actions/_local-file-documents.ts +2 -1
- package/corpus/templates/content/actions/attach-content-database-source.ts +124 -5
- package/corpus/templates/content/actions/bind-content-database-source-field.ts +256 -0
- package/corpus/templates/content/actions/change-content-database-source-role.ts +420 -0
- package/corpus/templates/content/actions/create-document.ts +8 -0
- package/corpus/templates/content/actions/execute-builder-source-batch.ts +282 -0
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +220 -8
- package/corpus/templates/content/actions/get-content-database.ts +10 -2
- package/corpus/templates/content/actions/prepare-builder-source-execution.ts +19 -2
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +47 -15
- package/corpus/templates/content/actions/refresh-content-database-source.ts +11 -4
- package/corpus/templates/content/actions/review-content-database-source-change-set.ts +6 -2
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +32 -12
- package/corpus/templates/content/actions/stage-builder-revision.ts +14 -10
- package/corpus/templates/content/actions/validate-builder-source-execution.ts +25 -3
- package/corpus/templates/content/app/components/editor/DocumentDatabase.tsx +1520 -553
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +150 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +647 -66
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +543 -204
- package/corpus/templates/content/app/hooks/use-content-database.ts +62 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +64 -0
- package/corpus/templates/content/app/i18n-data.ts +784 -0
- package/corpus/templates/content/changelog/2026-06-29-builder-review-checks-now-preserve-publish-and-unpublish-cho.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-database-sources-can-now-be-added-as-more-rows-or-matched.md +6 -0
- package/corpus/templates/content/changelog/2026-06-29-fixed-loading-the-final-rows-in-larger-databases-without-bre.md +6 -0
- package/corpus/templates/content/shared/api.ts +91 -1
- package/corpus/templates/content/vite.config.ts +14 -7
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +1 -0
- package/dist/cli/create.js.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts +10 -1
- package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/client/extensions/EmbeddedExtension.js +34 -3
- package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/file-upload/actions/upload-image.d.ts +2 -2
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +7 -7
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js +11 -0
- package/dist/sharing/registry.js.map +1 -1
- package/dist/templates/default/pnpm-workspace.yaml +1 -0
- package/dist/templates/workspace-root/pnpm-workspace.yaml +1 -0
- package/package.json +1 -1
- package/src/templates/default/pnpm-workspace.yaml +1 -0
- package/src/templates/workspace-root/pnpm-workspace.yaml +1 -0
package/corpus/README.md
CHANGED
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.80.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 72ef787: Add `onReady` and `onUnavailable` callbacks to `EmbeddedExtension`. `onReady` fires once when the embedded iframe first signals content readiness (its first height report, or iframe load as a fallback) — hosts that gate on content paint, such as dashboard report screenshots, can use it to avoid capturing a blank extension. `onUnavailable` fires when the extension can't be loaded for the current viewer (e.g. 403/404 because it isn't shared with them or no longer exists), so hosts can render an explanatory fallback instead of a silently blank panel.
|
|
8
|
+
|
|
9
|
+
## 0.80.6
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- f52eeb1: Pin the scaffolded workspace Nitro tracer dependency so fresh installs keep building reliably.
|
|
14
|
+
- f52eeb1: Ignore shareable resource registrations that originate from test modules during app runtime.
|
|
15
|
+
|
|
3
16
|
## 0.80.5
|
|
4
17
|
|
|
5
18
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.80.
|
|
3
|
+
"version": "0.80.7",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -1054,6 +1054,7 @@ function postProcessStandalone(
|
|
|
1054
1054
|
sections.overrides = {
|
|
1055
1055
|
'"@assistant-ui/store"': '">=0.2.9 <0.2.14"',
|
|
1056
1056
|
'"@assistant-ui/tap"': '"^0.5.14"',
|
|
1057
|
+
nf3: '"0.3.17"',
|
|
1057
1058
|
};
|
|
1058
1059
|
}
|
|
1059
1060
|
let updated = mergeWorkspaceYamlSections(existing, sections);
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "./delete-extension.js";
|
|
30
30
|
import {
|
|
31
31
|
extensionLoadError,
|
|
32
|
+
extensionLoadErrorStatus,
|
|
32
33
|
shouldRetryExtensionLoad,
|
|
33
34
|
} from "./extension-load-error.js";
|
|
34
35
|
import {
|
|
@@ -74,6 +75,15 @@ export interface EmbeddedExtensionProps {
|
|
|
74
75
|
className?: string;
|
|
75
76
|
/** Initial iframe height before content reports a real height. */
|
|
76
77
|
initialHeight?: number;
|
|
78
|
+
/** Fires once when the embedded iframe first signals content readiness — its
|
|
79
|
+
* first height report, or iframe load as a fallback. Hosts that gate on
|
|
80
|
+
* content paint (e.g. dashboard report screenshots) use this. */
|
|
81
|
+
onReady?: () => void;
|
|
82
|
+
/** Fires when the extension can't be loaded for this viewer (e.g. 403/404 —
|
|
83
|
+
* the extension isn't shared with them or no longer exists). Hosts can use
|
|
84
|
+
* this to render an explanatory fallback instead of a blank panel. By default
|
|
85
|
+
* the component renders nothing on failure (slot-style silent skip). */
|
|
86
|
+
onUnavailable?: (status?: number) => void;
|
|
77
87
|
}
|
|
78
88
|
|
|
79
89
|
/**
|
|
@@ -87,8 +97,20 @@ export function EmbeddedExtension({
|
|
|
87
97
|
context,
|
|
88
98
|
className,
|
|
89
99
|
initialHeight = 80,
|
|
100
|
+
onReady,
|
|
101
|
+
onUnavailable,
|
|
90
102
|
}: EmbeddedExtensionProps) {
|
|
91
103
|
const iframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
104
|
+
// Latch the readiness signal so onReady fires at most once per iframe
|
|
105
|
+
// instance. Reset when the iframe is recreated (extensionId/updatedAt change).
|
|
106
|
+
const onReadyRef = useRef(onReady);
|
|
107
|
+
onReadyRef.current = onReady;
|
|
108
|
+
const readyFiredRef = useRef(false);
|
|
109
|
+
const fireReady = () => {
|
|
110
|
+
if (readyFiredRef.current) return;
|
|
111
|
+
readyFiredRef.current = true;
|
|
112
|
+
onReadyRef.current?.();
|
|
113
|
+
};
|
|
92
114
|
const [height, setHeight] = useState<number>(initialHeight);
|
|
93
115
|
const [isDark, setIsDark] = useState(false);
|
|
94
116
|
// (audit H4) Mirror ExtensionViewer's role-aware gating; deny-by-default until
|
|
@@ -120,6 +142,8 @@ export function EmbeddedExtension({
|
|
|
120
142
|
data: extension,
|
|
121
143
|
isFetching,
|
|
122
144
|
isLoading,
|
|
145
|
+
isError,
|
|
146
|
+
error,
|
|
123
147
|
} = useQuery<Extension>({
|
|
124
148
|
queryKey: ["extension", extensionId],
|
|
125
149
|
queryFn: async () => {
|
|
@@ -141,6 +165,21 @@ export function EmbeddedExtension({
|
|
|
141
165
|
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 4000),
|
|
142
166
|
});
|
|
143
167
|
|
|
168
|
+
// Notify the host once when the extension can't be loaded for this viewer so
|
|
169
|
+
// it can show a fallback instead of a blank panel.
|
|
170
|
+
const onUnavailableRef = useRef(onUnavailable);
|
|
171
|
+
onUnavailableRef.current = onUnavailable;
|
|
172
|
+
const unavailableFiredRef = useRef(false);
|
|
173
|
+
useEffect(() => {
|
|
174
|
+
unavailableFiredRef.current = false;
|
|
175
|
+
}, [extensionId]);
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
if (isError && !isFetching && !unavailableFiredRef.current) {
|
|
178
|
+
unavailableFiredRef.current = true;
|
|
179
|
+
onUnavailableRef.current?.(extensionLoadErrorStatus(error));
|
|
180
|
+
}
|
|
181
|
+
}, [isError, isFetching, error]);
|
|
182
|
+
|
|
144
183
|
// Initial dark state is baked into the URL on first load only; subsequent
|
|
145
184
|
// theme toggles update the iframe's <html class="dark"> via postMessage so
|
|
146
185
|
// the user's interaction state inside the extension survives the toggle.
|
|
@@ -158,6 +197,7 @@ export function EmbeddedExtension({
|
|
|
158
197
|
useEffect(() => {
|
|
159
198
|
bridgeContextRef.current = { role: "viewer", isAuthor: false };
|
|
160
199
|
bindingLatchedRef.current = false;
|
|
200
|
+
readyFiredRef.current = false;
|
|
161
201
|
}, [extensionId, extension?.updatedAt]);
|
|
162
202
|
|
|
163
203
|
useEffect(() => {
|
|
@@ -215,6 +255,8 @@ export function EmbeddedExtension({
|
|
|
215
255
|
const h = Number(message.height);
|
|
216
256
|
if (Number.isFinite(h) && h > 0) {
|
|
217
257
|
setHeight(Math.ceil(h));
|
|
258
|
+
// First laid-out height means the content has painted.
|
|
259
|
+
fireReady();
|
|
218
260
|
}
|
|
219
261
|
return;
|
|
220
262
|
}
|
|
@@ -330,6 +372,9 @@ export function EmbeddedExtension({
|
|
|
330
372
|
{ type: "agent-native-slot-context", context: context ?? {} },
|
|
331
373
|
"*",
|
|
332
374
|
);
|
|
375
|
+
// Fallback readiness signal in case the extension never reports a
|
|
376
|
+
// height (e.g. fixed-height content that skips auto-resize).
|
|
377
|
+
fireReady();
|
|
333
378
|
}}
|
|
334
379
|
/>
|
|
335
380
|
<EmbeddedToolMenu
|
|
@@ -101,6 +101,22 @@ const REGISTRY_KEY = "__agentNativeShareableResources__";
|
|
|
101
101
|
type RegistryStore = Map<string, ShareableResourceRegistration>;
|
|
102
102
|
const globalRegistry: { [K in typeof REGISTRY_KEY]?: RegistryStore } =
|
|
103
103
|
globalThis as any;
|
|
104
|
+
|
|
105
|
+
function isTestRuntime(): boolean {
|
|
106
|
+
return (
|
|
107
|
+
process.env.NODE_ENV === "test" ||
|
|
108
|
+
process.env.VITEST === "true" ||
|
|
109
|
+
process.env.VITEST === "1"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function registrationCameFromTestFile(): boolean {
|
|
114
|
+
const stack = new Error().stack ?? "";
|
|
115
|
+
return /[./\\](?:[^/\\]+[.-])(?:test|spec)\.[cm]?[jt]sx?(?::\d+)?(?::\d+)?/.test(
|
|
116
|
+
stack,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
104
120
|
function getRegistry(): RegistryStore {
|
|
105
121
|
let r = globalRegistry[REGISTRY_KEY];
|
|
106
122
|
if (!r) {
|
|
@@ -113,6 +129,7 @@ function getRegistry(): RegistryStore {
|
|
|
113
129
|
export function registerShareableResource(
|
|
114
130
|
entry: ShareableResourceRegistration,
|
|
115
131
|
): void {
|
|
132
|
+
if (!isTestRuntime() && registrationCameFromTestFile()) return;
|
|
116
133
|
getRegistry().set(entry.type, entry);
|
|
117
134
|
}
|
|
118
135
|
|
|
@@ -56,7 +56,7 @@ When the user asks for a dashboard:
|
|
|
56
56
|
2. If a metric definition, date range, or grain is ambiguous and the choice would change the panel's numbers, use the `ask-question` clarifying tool once before building. Skip it when the dictionary or the user already settled it.
|
|
57
57
|
3. If a metric is not documented, do not guess column names. Ask for the table/columns or introspect the provider schema, then propose a dictionary entry with `save-data-dictionary-entry`.
|
|
58
58
|
4. Build a complete `SqlDashboardConfig` with `name` and `panels`. Optionally set top-level `columns` (1–6, default 2) to control how many grid columns the panels before any section use.
|
|
59
|
-
5. Every panel needs `id`, `title`, `source`, `chartType`, `width`, and `sql`. `width` is the number of grid columns the panel spans (1..6, clamped to the active section's column count). Section panels skip `source` and `sql` and may set their own `columns` (1–6) to override the dashboard default for the panels following the section.
|
|
59
|
+
5. Every panel needs `id`, `title`, `source`, `chartType`, `width`, and `sql`. `width` is the number of grid columns the panel spans (1..6, clamped to the active section's column count). Section panels skip `source` and `sql` and may set their own `columns` (1–6) to override the dashboard default for the panels following the section. Extension panels (`chartType: "extension"`) also skip `source` and `sql`; instead they require `config.extensionId` (see "Embedding An Extension As A Panel").
|
|
60
60
|
6. Persist with `update-dashboard`, not raw SQL or settings writes.
|
|
61
61
|
7. Navigate to it with `pnpm action navigate --view=adhoc --dashboardId=<id>`.
|
|
62
62
|
|
|
@@ -85,6 +85,37 @@ production mode, call `create-extension` automatically and then tell the user
|
|
|
85
85
|
that the request needed a bespoke surface, so you built it as an extension
|
|
86
86
|
rather than forcing it into a native dashboard config.
|
|
87
87
|
|
|
88
|
+
## Embedding An Extension As A Panel
|
|
89
|
+
|
|
90
|
+
Use `chartType: "extension"` to embed an existing extension as a dashboard
|
|
91
|
+
panel. This is different from the section above: there you replace the whole
|
|
92
|
+
dashboard with an extension; here you drop a single extension widget into one
|
|
93
|
+
panel slot alongside normal SQL charts. The panel renders the extension's
|
|
94
|
+
sandboxed iframe instead of running a query, so it skips `source` and `sql` and
|
|
95
|
+
instead requires `config.extensionId` (the id of an extension that already
|
|
96
|
+
exists — create it first with `create-extension`). Validation rejects an
|
|
97
|
+
extension panel without a non-empty `config.extensionId`.
|
|
98
|
+
|
|
99
|
+
```jsonc
|
|
100
|
+
{
|
|
101
|
+
"id": "pipeline-widget",
|
|
102
|
+
"title": "Pipeline Widget",
|
|
103
|
+
"chartType": "extension",
|
|
104
|
+
"width": 3,
|
|
105
|
+
"config": { "extensionId": "<existing-extension-id>" },
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Notes:
|
|
110
|
+
|
|
111
|
+
- The panel renders full-bleed (no card chrome/title) and does not receive the
|
|
112
|
+
dashboard's filters/variables/date range — it's a standalone widget for now.
|
|
113
|
+
- Access is scoped per viewer: embedding does NOT grant access to the extension
|
|
114
|
+
(same model as ExtensionSlots). If you share a dashboard more broadly than the
|
|
115
|
+
embedded extension, viewers without access to that extension see an
|
|
116
|
+
"extension unavailable" message instead of the content. Share the extension to
|
|
117
|
+
the same audience as the dashboard so all viewers can see it.
|
|
118
|
+
|
|
88
119
|
## Config Shape
|
|
89
120
|
|
|
90
121
|
```jsonc
|
|
@@ -207,7 +238,7 @@ type PanelPatch = {
|
|
|
207
238
|
title?: string;
|
|
208
239
|
sql?: string;
|
|
209
240
|
source?: "bigquery" | "ga4" | "amplitude" | "first-party" | "demo" | "prometheus";
|
|
210
|
-
chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout";
|
|
241
|
+
chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout" | "extension";
|
|
211
242
|
width?: number;
|
|
212
243
|
columns?: number;
|
|
213
244
|
tab?: string;
|
|
@@ -219,8 +250,9 @@ type PanelInput = PanelPatch & {
|
|
|
219
250
|
id: string;
|
|
220
251
|
title: string;
|
|
221
252
|
chartType: NonNullable<PanelPatch["chartType"]>;
|
|
222
|
-
source?: PanelPatch["source"]; // required for non-section panels
|
|
223
|
-
sql?: string; // required for non-section panels
|
|
253
|
+
source?: PanelPatch["source"]; // required for non-section / non-extension panels
|
|
254
|
+
sql?: string; // required for non-section / non-extension panels
|
|
255
|
+
// For chartType "extension": config.extensionId is required (the extension to embed).
|
|
224
256
|
};
|
|
225
257
|
|
|
226
258
|
type PanelFilter = {
|
|
@@ -450,6 +482,12 @@ pnpm action set-resource-visibility --resourceType dashboard --resourceId weekly
|
|
|
450
482
|
|
|
451
483
|
Writes require editor access; deletes require admin access. Owners always satisfy access checks.
|
|
452
484
|
|
|
485
|
+
If a dashboard embeds an extension panel (`chartType: "extension"`), sharing the
|
|
486
|
+
dashboard does not share the extension. Share the referenced extension to the
|
|
487
|
+
same audience (`share-resource --resourceType extension ...`) so all dashboard
|
|
488
|
+
viewers can see the embedded content; otherwise they get an "extension
|
|
489
|
+
unavailable" placeholder.
|
|
490
|
+
|
|
453
491
|
## Important Rules
|
|
454
492
|
|
|
455
493
|
- Never fabricate data or create a dashboard from guessed schema. A panel's SQL must hit a real source; do not present figures you did not actually query.
|
|
@@ -36,7 +36,7 @@ type PanelPatch = {
|
|
|
36
36
|
title?: string;
|
|
37
37
|
sql?: string;
|
|
38
38
|
source?: "bigquery" | "ga4" | "amplitude" | "first-party" | "demo" | "prometheus";
|
|
39
|
-
chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout";
|
|
39
|
+
chartType?: "line" | "area" | "bar" | "metric" | "table" | "pie" | "section" | "heatmap" | "callout" | "extension";
|
|
40
40
|
width?: number;
|
|
41
41
|
columns?: number;
|
|
42
42
|
tab?: string;
|
|
@@ -115,6 +115,7 @@ export const DASHBOARD_MUTATION_EXAMPLES = [
|
|
|
115
115
|
'dashboard.insertPanel({"id":"new-kpi","title":"New KPI","source":"first-party","chartType":"metric","width":1,"sql":"SELECT COUNT(*) AS value FROM analytics_events"}).atTop();',
|
|
116
116
|
'dashboard.insertPanel({"id":"new-chart","title":"New Chart","source":"first-party","chartType":"line","width":1,"sql":"SELECT date, COUNT(*) AS value FROM analytics_events GROUP BY date ORDER BY date"}).nextTo("retention-over-time");',
|
|
117
117
|
'dashboard.insertPanel({"id":"row-chart","title":"Row Chart","source":"first-party","chartType":"bar","width":1,"sql":"SELECT name, COUNT(*) AS value FROM analytics_events GROUP BY name"}).atRow(2);',
|
|
118
|
+
'dashboard.insertPanel({"id":"pipeline-widget","title":"Pipeline Widget","chartType":"extension","width":3,"config":{"extensionId":"<existing-extension-id>"}}).atBottom();',
|
|
118
119
|
] as const;
|
|
119
120
|
|
|
120
121
|
export type DashboardMutationOperation =
|
|
@@ -334,12 +334,15 @@ export function validateDashboardConfig(
|
|
|
334
334
|
if (!p || typeof p !== "object") {
|
|
335
335
|
return `panel[${i}] must be an object`;
|
|
336
336
|
}
|
|
337
|
-
// Section panels are pure layout dividers
|
|
338
|
-
// Width stays required for
|
|
337
|
+
// Section panels are pure layout dividers and extension panels render their
|
|
338
|
+
// own iframe, so both make source and sql optional. Width stays required for
|
|
339
|
+
// backward-compatible dashboard payloads.
|
|
339
340
|
const isSection = p.chartType === "section";
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
|
|
341
|
+
const isExtension = p.chartType === "extension";
|
|
342
|
+
const required =
|
|
343
|
+
isSection || isExtension
|
|
344
|
+
? (["id", "title", "chartType", "width"] as const)
|
|
345
|
+
: (["id", "title", "sql", "source", "chartType", "width"] as const);
|
|
343
346
|
for (const field of required) {
|
|
344
347
|
const v = p[field];
|
|
345
348
|
if (field === "width") {
|
|
@@ -352,9 +355,19 @@ export function validateDashboardConfig(
|
|
|
352
355
|
return `panel[${i}].${field} is required (non-empty string)`;
|
|
353
356
|
}
|
|
354
357
|
}
|
|
355
|
-
if (!isSection && !validSources.has(p.source as string)) {
|
|
358
|
+
if (!isSection && !isExtension && !validSources.has(p.source as string)) {
|
|
356
359
|
return `panel[${i}].source must be 'bigquery', 'ga4', 'amplitude', 'first-party', 'demo', or 'prometheus' (got '${p.source}'). source selects the backend — put the PromQL/SQL/table name in sql, not here.`;
|
|
357
360
|
}
|
|
361
|
+
if (isExtension) {
|
|
362
|
+
const cfg = p.config as Record<string, unknown> | undefined;
|
|
363
|
+
const extensionId =
|
|
364
|
+
cfg && typeof cfg.extensionId === "string"
|
|
365
|
+
? cfg.extensionId.trim()
|
|
366
|
+
: "";
|
|
367
|
+
if (!extensionId) {
|
|
368
|
+
return `panel[${i}].config.extensionId is required for extension panels (the id of the extension to render inline)`;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
358
371
|
if (
|
|
359
372
|
isSection &&
|
|
360
373
|
p.columns !== undefined &&
|
|
@@ -382,9 +395,10 @@ export async function validatePanelSql(
|
|
|
382
395
|
const vars = buildDryRunVars(config);
|
|
383
396
|
for (let i = 0; i < panels.length; i++) {
|
|
384
397
|
const p = panels[i] as Record<string, unknown>;
|
|
385
|
-
// Sections are layout-only
|
|
386
|
-
// query panels still
|
|
387
|
-
|
|
398
|
+
// Sections are layout-only and extensions render their own iframe — neither
|
|
399
|
+
// has SQL to dry-run. heatmap, callout, and other query panels still
|
|
400
|
+
// validate normally below.
|
|
401
|
+
if (p.chartType === "section" || p.chartType === "extension") continue;
|
|
388
402
|
if (p.source === "amplitude") {
|
|
389
403
|
const raw = typeof p.sql === "string" ? p.sql : "";
|
|
390
404
|
if (raw.trim()) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client";
|
|
2
|
+
import { EmbeddedExtension } from "@agent-native/core/client/extensions";
|
|
2
3
|
import {
|
|
3
4
|
IconArrowsSort,
|
|
4
5
|
IconSortAscending,
|
|
@@ -868,7 +869,10 @@ export function SqlChart({
|
|
|
868
869
|
const t = useT();
|
|
869
870
|
// Hooks must be called unconditionally before any early return.
|
|
870
871
|
const isSection = panel.chartType === "section";
|
|
871
|
-
const
|
|
872
|
+
const isExtension = panel.chartType === "extension";
|
|
873
|
+
// Sections are pure layout and extensions render their own iframe — neither
|
|
874
|
+
// runs the SQL pipeline.
|
|
875
|
+
const shouldQuery = !isSection && !isExtension && loadData;
|
|
872
876
|
const sql = serializePanelSql(resolvedSql ?? panel.sql);
|
|
873
877
|
const {
|
|
874
878
|
data: result,
|
|
@@ -918,6 +922,24 @@ export function SqlChart({
|
|
|
918
922
|
</div>
|
|
919
923
|
);
|
|
920
924
|
}
|
|
925
|
+
|
|
926
|
+
// Extension panels render a sandboxed extension iframe instead of querying a
|
|
927
|
+
// data source. The extension id lives in config.extensionId.
|
|
928
|
+
if (isExtension) {
|
|
929
|
+
const extensionId = panel.config?.extensionId;
|
|
930
|
+
if (!extensionId) {
|
|
931
|
+
return (
|
|
932
|
+
<div className="flex flex-1 items-center justify-center px-4 py-8 min-h-[120px]">
|
|
933
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
934
|
+
{t("sqlDashboard.extensionMissingId")}
|
|
935
|
+
</p>
|
|
936
|
+
</div>
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
return (
|
|
940
|
+
<DashboardExtensionPanel extensionId={extensionId} panelId={panel.id} />
|
|
941
|
+
);
|
|
942
|
+
}
|
|
921
943
|
const colors = panel.config?.colors || DEFAULT_COLORS;
|
|
922
944
|
const yFormatter = panel.config?.yFormatter;
|
|
923
945
|
|
|
@@ -1041,6 +1063,55 @@ export function SqlChart({
|
|
|
1041
1063
|
);
|
|
1042
1064
|
}
|
|
1043
1065
|
|
|
1066
|
+
function DashboardExtensionPanel({
|
|
1067
|
+
extensionId,
|
|
1068
|
+
panelId,
|
|
1069
|
+
}: {
|
|
1070
|
+
extensionId: string;
|
|
1071
|
+
panelId: string;
|
|
1072
|
+
}) {
|
|
1073
|
+
const t = useT();
|
|
1074
|
+
// Hold the report-readiness marker until the extension iframe paints so
|
|
1075
|
+
// dashboard report screenshots don't capture a blank extension panel.
|
|
1076
|
+
const [ready, setReady] = useState(false);
|
|
1077
|
+
const [unavailable, setUnavailable] = useState(false);
|
|
1078
|
+
|
|
1079
|
+
// Embedding never grants access to the extension itself (same model as
|
|
1080
|
+
// ExtensionSlots). A viewer with dashboard-only access who can't see the
|
|
1081
|
+
// referenced extension gets a clear message instead of a blank panel.
|
|
1082
|
+
if (unavailable) {
|
|
1083
|
+
return (
|
|
1084
|
+
<div className="flex flex-1 items-center justify-center px-4 py-8 min-h-[120px]">
|
|
1085
|
+
<p className="text-sm text-muted-foreground text-center">
|
|
1086
|
+
{t("sqlDashboard.extensionUnavailable")}
|
|
1087
|
+
</p>
|
|
1088
|
+
</div>
|
|
1089
|
+
);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return (
|
|
1093
|
+
<div
|
|
1094
|
+
className="w-full"
|
|
1095
|
+
data-dashboard-report-loading={ready ? undefined : "true"}
|
|
1096
|
+
>
|
|
1097
|
+
<EmbeddedExtension
|
|
1098
|
+
extensionId={extensionId}
|
|
1099
|
+
slotId={`dashboard-panel-${panelId}`}
|
|
1100
|
+
className="w-full"
|
|
1101
|
+
// Intentional for v1: extension panels are standalone widgets and do not
|
|
1102
|
+
// receive the dashboard's filters/variables/date range as `context`.
|
|
1103
|
+
initialHeight={180}
|
|
1104
|
+
onReady={() => setReady(true)}
|
|
1105
|
+
onUnavailable={() => {
|
|
1106
|
+
// Clear the report-loading gate so report capture doesn't hang.
|
|
1107
|
+
setReady(true);
|
|
1108
|
+
setUnavailable(true);
|
|
1109
|
+
}}
|
|
1110
|
+
/>
|
|
1111
|
+
</div>
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1044
1115
|
function MetricRenderer({
|
|
1045
1116
|
rows,
|
|
1046
1117
|
panel,
|
|
@@ -392,6 +392,9 @@ const enUS = {
|
|
|
392
392
|
off: "Off",
|
|
393
393
|
since: "Since",
|
|
394
394
|
sectionOptions: "Section options",
|
|
395
|
+
extensionMissingId: "This extension panel has no extension selected.",
|
|
396
|
+
extensionUnavailable:
|
|
397
|
+
"This extension isn't shared with you, or it no longer exists.",
|
|
395
398
|
panelOptions: "Panel options",
|
|
396
399
|
fullScreen: "Full screen",
|
|
397
400
|
refresh: "Refresh",
|
|
@@ -5249,6 +5252,8 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5249
5252
|
saveView: "保存视图",
|
|
5250
5253
|
savedViews: "已保存的视图",
|
|
5251
5254
|
sectionOptions: "部分选项",
|
|
5255
|
+
extensionMissingId: "此扩展面板未选择任何扩展。",
|
|
5256
|
+
extensionUnavailable: "此扩展未与您共享,或已不存在。",
|
|
5252
5257
|
sharedWithOrg: "与组织共享",
|
|
5253
5258
|
unhideFailed: "无法取消隐藏仪表板",
|
|
5254
5259
|
untitledDashboard: "无标题仪表板",
|
|
@@ -5424,6 +5429,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5424
5429
|
saveView: "Guardar vista",
|
|
5425
5430
|
savedViews: "Vistas guardadas",
|
|
5426
5431
|
sectionOptions: "Opciones de sección",
|
|
5432
|
+
extensionMissingId:
|
|
5433
|
+
"Este panel de extensión no tiene ninguna extensión seleccionada.",
|
|
5434
|
+
extensionUnavailable:
|
|
5435
|
+
"Esta extensión no está compartida contigo o ya no existe.",
|
|
5427
5436
|
sharedWithOrg: "Compartido con la organización",
|
|
5428
5437
|
unhideFailed: "No se pudo mostrar el panel",
|
|
5429
5438
|
untitledDashboard: "Panel de control sin título",
|
|
@@ -5601,6 +5610,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5601
5610
|
saveView: "Enregistrer la vue",
|
|
5602
5611
|
savedViews: "Vues enregistrées",
|
|
5603
5612
|
sectionOptions: "Options de sections",
|
|
5613
|
+
extensionMissingId:
|
|
5614
|
+
"Ce panneau d'extension n'a aucune extension sélectionnée.",
|
|
5615
|
+
extensionUnavailable:
|
|
5616
|
+
"Cette extension n'est pas partagée avec vous ou n'existe plus.",
|
|
5604
5617
|
sharedWithOrg: "Partagé avec l'organisation",
|
|
5605
5618
|
unhideFailed: "Impossible d'afficher le tableau de bord",
|
|
5606
5619
|
untitledDashboard: "Tableau de bord sans titre",
|
|
@@ -5779,6 +5792,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5779
5792
|
saveView: "Ansicht speichern",
|
|
5780
5793
|
savedViews: "Gespeicherte Ansichten",
|
|
5781
5794
|
sectionOptions: "Abschnittsoptionen",
|
|
5795
|
+
extensionMissingId:
|
|
5796
|
+
"Für dieses Erweiterungs-Panel ist keine Erweiterung ausgewählt.",
|
|
5797
|
+
extensionUnavailable:
|
|
5798
|
+
"Diese Erweiterung ist nicht für Sie freigegeben oder existiert nicht mehr.",
|
|
5782
5799
|
sharedWithOrg: "Mit Org geteilt",
|
|
5783
5800
|
unhideFailed: "Das Dashboard konnte nicht eingeblendet werden",
|
|
5784
5801
|
untitledDashboard: "Unbenanntes Dashboard",
|
|
@@ -5952,6 +5969,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
5952
5969
|
saveView: "ビューを保存",
|
|
5953
5970
|
savedViews: "保存されたビュー",
|
|
5954
5971
|
sectionOptions: "セクションのオプション",
|
|
5972
|
+
extensionMissingId:
|
|
5973
|
+
"この拡張機能パネルには拡張機能が選択されていません。",
|
|
5974
|
+
extensionUnavailable:
|
|
5975
|
+
"この拡張機能はあなたと共有されていないか、存在しません。",
|
|
5955
5976
|
sharedWithOrg: "組織と共有",
|
|
5956
5977
|
unhideFailed: "ダッシュボードを再表示できませんでした",
|
|
5957
5978
|
untitledDashboard: "無題のダッシュボード",
|
|
@@ -6124,6 +6145,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
6124
6145
|
saveView: "보기 저장",
|
|
6125
6146
|
savedViews: "저장된 보기",
|
|
6126
6147
|
sectionOptions: "섹션 옵션",
|
|
6148
|
+
extensionMissingId:
|
|
6149
|
+
"이 확장 프로그램 패널에 선택된 확장 프로그램이 없습니다.",
|
|
6150
|
+
extensionUnavailable:
|
|
6151
|
+
"이 확장 프로그램이 공유되지 않았거나 더 이상 존재하지 않습니다.",
|
|
6127
6152
|
sharedWithOrg: "조직과 공유됨",
|
|
6128
6153
|
unhideFailed: "대시보드를 숨기기 해제할 수 없습니다.",
|
|
6129
6154
|
untitledDashboard: "제목 없는 대시보드",
|
|
@@ -6300,6 +6325,10 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
6300
6325
|
saveView: "Salvar visualização",
|
|
6301
6326
|
savedViews: "Visualizações salvas",
|
|
6302
6327
|
sectionOptions: "Opções de seção",
|
|
6328
|
+
extensionMissingId:
|
|
6329
|
+
"Este painel de extensão não tem nenhuma extensão selecionada.",
|
|
6330
|
+
extensionUnavailable:
|
|
6331
|
+
"Esta extensão não foi compartilhada com você ou não existe mais.",
|
|
6303
6332
|
sharedWithOrg: "Compartilhado com a organização",
|
|
6304
6333
|
unhideFailed: "Não foi possível exibir o painel",
|
|
6305
6334
|
untitledDashboard: "Painel sem título",
|
|
@@ -6472,6 +6501,9 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
6472
6501
|
saveView: "दृश्य सहेजें",
|
|
6473
6502
|
savedViews: "सहेजे गए दृश्य",
|
|
6474
6503
|
sectionOptions: "अनुभाग विकल्प",
|
|
6504
|
+
extensionMissingId: "इस एक्सटेंशन पैनल में कोई एक्सटेंशन चयनित नहीं है।",
|
|
6505
|
+
extensionUnavailable:
|
|
6506
|
+
"यह एक्सटेंशन आपके साथ साझा नहीं किया गया है, या अब मौजूद नहीं है।",
|
|
6475
6507
|
sharedWithOrg: "संगठन के साथ साझा किया गया",
|
|
6476
6508
|
unhideFailed: "डैशबोर्ड को उजागर नहीं किया जा सका",
|
|
6477
6509
|
untitledDashboard: "शीर्षक रहित डैशबोर्ड",
|
|
@@ -6644,6 +6676,8 @@ const translatedAnalyticsDebtTranslations = {
|
|
|
6644
6676
|
saveView: "حفظ العرض",
|
|
6645
6677
|
savedViews: "المشاهدات المحفوظة",
|
|
6646
6678
|
sectionOptions: "خيارات القسم",
|
|
6679
|
+
extensionMissingId: "لم يتم تحديد أي إضافة في لوحة الإضافة هذه.",
|
|
6680
|
+
extensionUnavailable: "هذه الإضافة غير مشاركة معك، أو لم تعد موجودة.",
|
|
6647
6681
|
sharedWithOrg: "تمت المشاركة مع منظمة",
|
|
6648
6682
|
unhideFailed: "تعذر إظهار لوحة البيانات",
|
|
6649
6683
|
untitledDashboard: "لوحة تحكم بلا عنوان",
|