@agent-native/core 0.70.2 → 0.71.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 +2 -2
- package/corpus/core/CHANGELOG.md +19 -0
- package/corpus/core/docs/content/multi-app-workspace.md +2 -2
- package/corpus/core/docs/design/durable-agent-runs.md +458 -4
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/action.ts +21 -10
- package/corpus/core/src/agent/durable-background.ts +192 -0
- package/corpus/core/src/agent/production-agent.ts +330 -13
- package/corpus/core/src/agent/run-manager.ts +66 -3
- package/corpus/core/src/agent/run-store.ts +129 -23
- package/corpus/core/src/agent/types.ts +20 -0
- package/corpus/core/src/client/AgentPanel.tsx +12 -9
- package/corpus/core/src/client/blocks/library/FileTreeBlock.tsx +72 -14
- package/corpus/core/src/deploy/build.ts +96 -0
- package/corpus/core/src/deploy/workspace-deploy.ts +121 -0
- package/corpus/core/src/mcp/build-server.ts +22 -13
- package/corpus/core/src/server/agent-chat-plugin.ts +133 -66
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +19 -0
- package/corpus/templates/analytics/AGENTS.md +8 -0
- package/corpus/templates/analytics/actions/compose-dashboard.ts +317 -0
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +33 -6
- package/corpus/templates/analytics/changelog/2026-06-23-build-large-first-party-analytics-dashboards-in-one-fast-cal.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +574 -0
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +8 -2
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -9
- package/corpus/templates/clips/changelog/2026-06-23-dragging-the-desktop-camera-bubble-now-glides-to-a-stop-at-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-06-23-screen-recordings-now-capture-at-a-crisp-1080p-bitrate-inste.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -0
- package/corpus/templates/clips/chrome-extension/scripts/dev.ts +102 -0
- package/corpus/templates/clips/chrome-extension/src/background.ts +164 -32
- package/corpus/templates/clips/chrome-extension/src/content-script.ts +163 -13
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +198 -35
- package/corpus/templates/clips/chrome-extension/src/overlay.css +73 -23
- package/corpus/templates/clips/chrome-extension/src/overlay.ts +82 -13
- package/corpus/templates/clips/chrome-extension/src/popup.html +62 -5
- package/corpus/templates/clips/chrome-extension/src/popup.ts +290 -1
- package/corpus/templates/clips/chrome-extension/src/styles.css +34 -0
- package/corpus/templates/clips/desktop/src/lib/audio-cue.ts +21 -16
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +17 -20
- package/corpus/templates/clips/desktop/src/overlays/bubble.tsx +73 -23
- package/corpus/templates/clips/desktop/src/overlays/countdown.tsx +0 -6
- package/corpus/templates/clips/desktop/src/overlays/toolbar.tsx +56 -46
- package/corpus/templates/clips/desktop/src/styles.css +33 -5
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +113 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +2 -2
- package/corpus/templates/clips/server/routes/api/video/[recordingId].get.ts +9 -1
- package/corpus/templates/clips/shared/upload-limits.ts +5 -2
- package/corpus/templates/plan/app/pages/PlansPage.tsx +11 -7
- package/corpus/templates/plan/changelog/2026-06-23-plan-loading-skeleton-no-longer-cuts-off-the-canvas-preview-.md +6 -0
- package/dist/action.js +21 -10
- package/dist/action.js.map +1 -1
- package/dist/agent/durable-background.d.ts +60 -0
- package/dist/agent/durable-background.d.ts.map +1 -0
- package/dist/agent/durable-background.js +144 -0
- package/dist/agent/durable-background.js.map +1 -0
- package/dist/agent/production-agent.d.ts +20 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +292 -14
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/agent/run-manager.d.ts +48 -0
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +45 -3
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.d.ts +30 -1
- package/dist/agent/run-store.d.ts.map +1 -1
- package/dist/agent/run-store.js +124 -24
- package/dist/agent/run-store.js.map +1 -1
- package/dist/agent/types.d.ts +20 -0
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/agent/types.js.map +1 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +2 -2
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.d.ts.map +1 -1
- package/dist/client/blocks/library/FileTreeBlock.js +37 -4
- package/dist/client/blocks/library/FileTreeBlock.js.map +1 -1
- package/dist/deploy/build.d.ts +29 -0
- package/dist/deploy/build.d.ts.map +1 -1
- package/dist/deploy/build.js +85 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/deploy/workspace-deploy.d.ts.map +1 -1
- package/dist/deploy/workspace-deploy.js +108 -0
- package/dist/deploy/workspace-deploy.js.map +1 -1
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +23 -9
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +68 -13
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/docs/content/multi-app-workspace.md +2 -2
- package/docs/design/durable-agent-runs.md +458 -4
- package/package.json +1 -1
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { defineAction, embedApp } from "@agent-native/core";
|
|
2
|
+
import {
|
|
3
|
+
getRequestUserEmail,
|
|
4
|
+
getRequestOrgId,
|
|
5
|
+
buildDeepLink,
|
|
6
|
+
} from "@agent-native/core/server";
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import { getDashboard, upsertDashboard } from "../server/lib/dashboards-store";
|
|
9
|
+
import {
|
|
10
|
+
buildPanel,
|
|
11
|
+
listMetricKeys,
|
|
12
|
+
type ComposedPanel,
|
|
13
|
+
type MetricWindow,
|
|
14
|
+
} from "../server/lib/first-party-metric-catalog";
|
|
15
|
+
import { validateFirstPartyAnalyticsSql } from "../server/lib/first-party-analytics.js";
|
|
16
|
+
import {
|
|
17
|
+
hasCollabState,
|
|
18
|
+
applyText,
|
|
19
|
+
seedFromText,
|
|
20
|
+
} from "@agent-native/core/collab";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Push the saved config through the collab layer so open dashboard editors get
|
|
24
|
+
* the change in real time (mirrors update-dashboard / install-dashboard-template).
|
|
25
|
+
*/
|
|
26
|
+
async function syncToCollab(
|
|
27
|
+
dashboardId: string,
|
|
28
|
+
config: Record<string, unknown>,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const docId = `dash-${dashboardId}`;
|
|
31
|
+
const configStr = JSON.stringify(config);
|
|
32
|
+
try {
|
|
33
|
+
if (await hasCollabState(docId)) {
|
|
34
|
+
await applyText(docId, configStr, "content", "agent");
|
|
35
|
+
} else {
|
|
36
|
+
await seedFromText(docId, configStr);
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
// Collab sync is best-effort — the SQL write is the source of truth.
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const WINDOWS = new Set<MetricWindow>(["30d", "90d", "all"]);
|
|
44
|
+
|
|
45
|
+
function normalizeWindow(raw: unknown): MetricWindow | undefined {
|
|
46
|
+
if (typeof raw !== "string") return undefined;
|
|
47
|
+
const v = raw.trim() as MetricWindow;
|
|
48
|
+
return WINDOWS.has(v) ? v : undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** One requested metric, normalized to a key plus optional overrides. */
|
|
52
|
+
interface NormalizedRequest {
|
|
53
|
+
metric: string;
|
|
54
|
+
id?: string;
|
|
55
|
+
title?: string;
|
|
56
|
+
chartType?: string;
|
|
57
|
+
width?: number;
|
|
58
|
+
window?: MetricWindow;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function normalizeRequest(raw: unknown): NormalizedRequest | null {
|
|
62
|
+
if (typeof raw === "string") {
|
|
63
|
+
const metric = raw.trim();
|
|
64
|
+
return metric ? { metric } : null;
|
|
65
|
+
}
|
|
66
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
67
|
+
const obj = raw as Record<string, unknown>;
|
|
68
|
+
const metric = typeof obj.metric === "string" ? obj.metric.trim() : "";
|
|
69
|
+
if (!metric) return null;
|
|
70
|
+
const width =
|
|
71
|
+
typeof obj.width === "number" && Number.isFinite(obj.width)
|
|
72
|
+
? obj.width
|
|
73
|
+
: undefined;
|
|
74
|
+
return {
|
|
75
|
+
metric,
|
|
76
|
+
id: typeof obj.id === "string" ? obj.id : undefined,
|
|
77
|
+
title: typeof obj.title === "string" ? obj.title : undefined,
|
|
78
|
+
chartType: typeof obj.chartType === "string" ? obj.chartType : undefined,
|
|
79
|
+
width,
|
|
80
|
+
window: normalizeWindow(obj.window),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const metricSchema = z.union([
|
|
87
|
+
z.string(),
|
|
88
|
+
z.object({
|
|
89
|
+
metric: z.string(),
|
|
90
|
+
id: z.string().optional(),
|
|
91
|
+
title: z.string().optional(),
|
|
92
|
+
chartType: z.string().optional(),
|
|
93
|
+
width: z.number().optional(),
|
|
94
|
+
window: z.string().optional(),
|
|
95
|
+
}),
|
|
96
|
+
]);
|
|
97
|
+
|
|
98
|
+
const METRIC_KEYS = listMetricKeys();
|
|
99
|
+
|
|
100
|
+
export default defineAction({
|
|
101
|
+
description:
|
|
102
|
+
"Build a large first-party analytics dashboard in ONE fast call: name the metrics you want and the SERVER generates the validated SQL + chart config for every panel. " +
|
|
103
|
+
"Do NOT hand-author large `update-dashboard` configs panel-by-panel — producing/streaming a big multi-panel config inside the ~40s run budget fails and thrashes. " +
|
|
104
|
+
"Each metric expands into a complete first-party panel from the shipped, already-validated metric catalog. Unknown metric keys are skipped and reported (not fatal); each panel's SQL is validated independently (valid panels are saved, invalid ones reported), and the dashboard is assembled and saved in a single atomic store write. " +
|
|
105
|
+
"If the dashboard already exists and `overwrite` is false (default), the new panels are APPENDED (panels whose id is already present are skipped); with `overwrite: true` the config is replaced. " +
|
|
106
|
+
"Returns { dashboardId, panelCount, createdMetrics, unknownMetrics, invalidMetrics, urlPath, deepLink, message } — use panelCount as proof-of-done. " +
|
|
107
|
+
`Available metric keys: ${METRIC_KEYS.join(", ")}. ` +
|
|
108
|
+
"Each metric accepts an optional per-metric `window` of '30d' | '90d' | 'all' (only affects windowed virality/time metrics).",
|
|
109
|
+
schema: z.object({
|
|
110
|
+
dashboardId: z
|
|
111
|
+
.string()
|
|
112
|
+
.describe(
|
|
113
|
+
"Dashboard id (without the `sql-dashboard-` prefix), e.g. 'first-party-overview'.",
|
|
114
|
+
),
|
|
115
|
+
title: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe(
|
|
119
|
+
"Dashboard name. Used on create; on append it is applied only if the existing dashboard has no name.",
|
|
120
|
+
),
|
|
121
|
+
metrics: z
|
|
122
|
+
.preprocess(
|
|
123
|
+
(v) => (typeof v === "string" ? JSON.parse(v) : v),
|
|
124
|
+
z.array(metricSchema),
|
|
125
|
+
)
|
|
126
|
+
.describe(
|
|
127
|
+
"Metric keys to include, in order. Each item is either a key string or { metric, title?, chartType?, width?, window? }. " +
|
|
128
|
+
`Valid keys: ${METRIC_KEYS.join(", ")}.`,
|
|
129
|
+
),
|
|
130
|
+
overwrite: z
|
|
131
|
+
.boolean()
|
|
132
|
+
.optional()
|
|
133
|
+
.describe(
|
|
134
|
+
"If true, replace the whole dashboard config. If false (default) and the dashboard exists, APPEND the new panels (skipping ids already present).",
|
|
135
|
+
),
|
|
136
|
+
}),
|
|
137
|
+
mcpApp: {
|
|
138
|
+
compactCatalog: true,
|
|
139
|
+
resource: embedApp({
|
|
140
|
+
title: "Dashboard preview",
|
|
141
|
+
description: "Open the composed dashboard in the real Analytics UI.",
|
|
142
|
+
iframeTitle: "Agent-Native Analytics",
|
|
143
|
+
openLabel: "Open dashboard",
|
|
144
|
+
height: 680,
|
|
145
|
+
}),
|
|
146
|
+
},
|
|
147
|
+
run: async (args) => {
|
|
148
|
+
const email = getRequestUserEmail();
|
|
149
|
+
if (!email) throw new Error("no authenticated user");
|
|
150
|
+
const ctx = { email, orgId: getRequestOrgId() || null };
|
|
151
|
+
|
|
152
|
+
// The CLI/gateway may hand `metrics` over as a JSON string; the schema
|
|
153
|
+
// preprocess handles that, but normalize defensively here too.
|
|
154
|
+
const rawMetrics: unknown[] = Array.isArray(args.metrics)
|
|
155
|
+
? (args.metrics as unknown[])
|
|
156
|
+
: typeof args.metrics === "string"
|
|
157
|
+
? (JSON.parse(args.metrics) as unknown[])
|
|
158
|
+
: [];
|
|
159
|
+
|
|
160
|
+
const requests: NormalizedRequest[] = [];
|
|
161
|
+
for (const raw of rawMetrics) {
|
|
162
|
+
const normalized = normalizeRequest(raw);
|
|
163
|
+
if (normalized) requests.push(normalized);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const createdMetrics: string[] = [];
|
|
167
|
+
const unknownMetrics: string[] = [];
|
|
168
|
+
const invalidMetrics: Array<{ metric: string; reason: string }> = [];
|
|
169
|
+
const composedPanels: ComposedPanel[] = [];
|
|
170
|
+
|
|
171
|
+
for (const req of requests) {
|
|
172
|
+
const panel = buildPanel(req.metric, {
|
|
173
|
+
id: req.id,
|
|
174
|
+
title: req.title,
|
|
175
|
+
chartType: req.chartType,
|
|
176
|
+
width: req.width,
|
|
177
|
+
window: req.window,
|
|
178
|
+
});
|
|
179
|
+
if (!panel) {
|
|
180
|
+
// Unknown key — report, never throw.
|
|
181
|
+
if (!unknownMetrics.includes(req.metric)) {
|
|
182
|
+
unknownMetrics.push(req.metric);
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// Per-panel graceful validation: a bad panel is dropped + reported, the
|
|
187
|
+
// rest of the dashboard still builds. (Catalog SQL is known-good, so this
|
|
188
|
+
// is a defensive net, e.g. if a future window/override produces bad SQL.)
|
|
189
|
+
try {
|
|
190
|
+
validateFirstPartyAnalyticsSql(panel.sql);
|
|
191
|
+
} catch (e: any) {
|
|
192
|
+
invalidMetrics.push({
|
|
193
|
+
metric: req.metric,
|
|
194
|
+
reason: e?.message ?? String(e),
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
composedPanels.push(panel);
|
|
199
|
+
createdMetrics.push(req.metric);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const existing = await getDashboard(args.dashboardId, ctx);
|
|
203
|
+
const dashboardName =
|
|
204
|
+
args.title?.trim() ||
|
|
205
|
+
(existing && typeof existing.config?.name === "string"
|
|
206
|
+
? (existing.config.name as string)
|
|
207
|
+
: args.dashboardId);
|
|
208
|
+
|
|
209
|
+
let finalConfig: Record<string, unknown>;
|
|
210
|
+
let appendedCount = composedPanels.length;
|
|
211
|
+
let skippedExistingIds: string[] = [];
|
|
212
|
+
|
|
213
|
+
if (existing && !args.overwrite) {
|
|
214
|
+
// Append: preserve existing panels + order, add only new panel ids.
|
|
215
|
+
const existingConfig = existing.config as Record<string, unknown>;
|
|
216
|
+
const existingPanels = Array.isArray(existingConfig.panels)
|
|
217
|
+
? (existingConfig.panels as Array<Record<string, unknown>>)
|
|
218
|
+
: [];
|
|
219
|
+
const existingIds = new Set(
|
|
220
|
+
existingPanels
|
|
221
|
+
.map((p) => (typeof p?.id === "string" ? p.id : null))
|
|
222
|
+
.filter((id): id is string => !!id),
|
|
223
|
+
);
|
|
224
|
+
const toAppend: ComposedPanel[] = [];
|
|
225
|
+
for (const panel of composedPanels) {
|
|
226
|
+
if (existingIds.has(panel.id)) {
|
|
227
|
+
skippedExistingIds.push(panel.id);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
toAppend.push(panel);
|
|
231
|
+
existingIds.add(panel.id);
|
|
232
|
+
}
|
|
233
|
+
appendedCount = toAppend.length;
|
|
234
|
+
finalConfig = {
|
|
235
|
+
...existingConfig,
|
|
236
|
+
name:
|
|
237
|
+
typeof existingConfig.name === "string" && existingConfig.name.trim()
|
|
238
|
+
? existingConfig.name
|
|
239
|
+
: dashboardName,
|
|
240
|
+
panels: [...existingPanels, ...toAppend],
|
|
241
|
+
};
|
|
242
|
+
} else {
|
|
243
|
+
// Create or overwrite: a fresh config with exactly the composed panels.
|
|
244
|
+
finalConfig = {
|
|
245
|
+
name: dashboardName,
|
|
246
|
+
description:
|
|
247
|
+
"First-party analytics dashboard composed from the metric catalog.",
|
|
248
|
+
panels: composedPanels,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const panelCount = Array.isArray(finalConfig.panels)
|
|
253
|
+
? (finalConfig.panels as unknown[]).length
|
|
254
|
+
: 0;
|
|
255
|
+
|
|
256
|
+
// Single atomic save through the same store path update-dashboard uses.
|
|
257
|
+
await upsertDashboard(args.dashboardId, "sql", finalConfig, ctx);
|
|
258
|
+
await syncToCollab(args.dashboardId, finalConfig);
|
|
259
|
+
|
|
260
|
+
const parts: string[] = [];
|
|
261
|
+
if (existing && !args.overwrite) {
|
|
262
|
+
parts.push(`Appended ${appendedCount} panel(s) to "${args.dashboardId}"`);
|
|
263
|
+
if (skippedExistingIds.length > 0) {
|
|
264
|
+
parts.push(`${skippedExistingIds.length} already present`);
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
parts.push(
|
|
268
|
+
`${existing ? "Replaced" : "Created"} "${args.dashboardId}" with ${createdMetrics.length} panel(s)`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (unknownMetrics.length > 0) {
|
|
272
|
+
parts.push(
|
|
273
|
+
`skipped ${unknownMetrics.length} unknown metric(s): ${unknownMetrics.join(", ")}`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
if (invalidMetrics.length > 0) {
|
|
277
|
+
parts.push(
|
|
278
|
+
`skipped ${invalidMetrics.length} invalid metric(s): ${invalidMetrics.map((m) => m.metric).join(", ")}`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
parts.push(`Dashboard now has ${panelCount} panel(s).`);
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
id: args.dashboardId,
|
|
285
|
+
dashboardId: args.dashboardId,
|
|
286
|
+
name: dashboardName,
|
|
287
|
+
panelCount,
|
|
288
|
+
createdMetrics,
|
|
289
|
+
unknownMetrics,
|
|
290
|
+
invalidMetrics,
|
|
291
|
+
skippedExistingIds,
|
|
292
|
+
urlPath: `/dashboards/${args.dashboardId}`,
|
|
293
|
+
deepLink: buildDeepLink({
|
|
294
|
+
app: "analytics",
|
|
295
|
+
view: "adhoc",
|
|
296
|
+
params: { dashboardId: args.dashboardId },
|
|
297
|
+
}),
|
|
298
|
+
message: parts.join("; ") + ".",
|
|
299
|
+
};
|
|
300
|
+
},
|
|
301
|
+
link: ({ result }) => {
|
|
302
|
+
const dashboardId =
|
|
303
|
+
result && typeof result === "object"
|
|
304
|
+
? (result as { dashboardId?: string }).dashboardId
|
|
305
|
+
: undefined;
|
|
306
|
+
if (!dashboardId) return null;
|
|
307
|
+
return {
|
|
308
|
+
url: buildDeepLink({
|
|
309
|
+
app: "analytics",
|
|
310
|
+
view: "adhoc",
|
|
311
|
+
params: { dashboardId },
|
|
312
|
+
}),
|
|
313
|
+
label: "Open composed dashboard in Analytics",
|
|
314
|
+
view: "adhoc",
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
});
|
|
@@ -103,6 +103,34 @@ function formatYValue(
|
|
|
103
103
|
return value.toLocaleString();
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Format a single metric value for display. Coerces Postgres numeric/bigint
|
|
108
|
+
* columns (returned as strings, e.g. a rate of "0.00000000000000000000") to a
|
|
109
|
+
* number so the formatter applies — SQLite returns JS numbers, so this only
|
|
110
|
+
* bites on Postgres/Neon, where the raw high-scale decimal would otherwise be
|
|
111
|
+
* dumped verbatim. A configured `valueLabels` mapping wins; a non-numeric
|
|
112
|
+
* string falls through unformatted.
|
|
113
|
+
*/
|
|
114
|
+
export function formatMetricValue(
|
|
115
|
+
raw: unknown,
|
|
116
|
+
formatter?: "number" | "currency" | "percent",
|
|
117
|
+
valueLabels?: Record<string, string>,
|
|
118
|
+
): string {
|
|
119
|
+
const valueLabel = valueLabels?.[String(raw)];
|
|
120
|
+
if (valueLabel !== undefined) return valueLabel;
|
|
121
|
+
const numericRaw =
|
|
122
|
+
typeof raw === "number"
|
|
123
|
+
? raw
|
|
124
|
+
: typeof raw === "string" &&
|
|
125
|
+
raw.trim() !== "" &&
|
|
126
|
+
Number.isFinite(Number(raw))
|
|
127
|
+
? Number(raw)
|
|
128
|
+
: null;
|
|
129
|
+
return numericRaw !== null
|
|
130
|
+
? formatYValue(numericRaw, formatter)
|
|
131
|
+
: String(raw ?? "-");
|
|
132
|
+
}
|
|
133
|
+
|
|
106
134
|
function parsePrometheusSeriesLabel(label: string): {
|
|
107
135
|
metric: string;
|
|
108
136
|
labels: Record<string, string>;
|
|
@@ -647,12 +675,11 @@ function MetricRenderer({
|
|
|
647
675
|
} else {
|
|
648
676
|
raw = row[valueCol];
|
|
649
677
|
}
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
: String(raw ?? "-"));
|
|
678
|
+
const value = formatMetricValue(
|
|
679
|
+
raw,
|
|
680
|
+
panel.config?.yFormatter,
|
|
681
|
+
panel.config?.valueLabels,
|
|
682
|
+
);
|
|
656
683
|
|
|
657
684
|
return (
|
|
658
685
|
<div className="flex flex-1 flex-col items-center justify-center py-2 text-center">
|