@agent-native/core 0.76.9 → 0.76.10
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 +33 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/scripts/generate-og-font-data.mjs +9 -5
- package/corpus/core/src/client/AgentPanel.tsx +70 -1
- package/corpus/core/src/client/AssistantChat.tsx +1 -3
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +0 -62
- package/corpus/core/src/client/composer/TiptapComposer.tsx +6 -13
- package/corpus/core/src/server/og-fonts-data.ts +7 -4
- package/corpus/core/src/server/og-fonts.ts +10 -3
- package/corpus/core/src/server/social-og-image.ts +45 -6
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +199 -19
- package/corpus/templates/analytics/app/pages/adhoc/AdhocRouter.tsx +1 -21
- package/corpus/templates/analytics/app/pages/adhoc/DashboardSkeleton.tsx +35 -0
- package/corpus/templates/analytics/app/pages/adhoc/explorer-dashboard/index.tsx +3 -23
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/index.tsx +3 -26
- package/corpus/templates/analytics/changelog/2026-06-24-daily-dashboard-charts-now-use-local-day-buckets-and-show-th.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-dashboard-loading-placeholders-now-span-the-available-page-w.md +6 -0
- package/corpus/templates/analytics/changelog/2026-06-24-first-party-dashboard-dates-now-align-to-pacific-time-for-da.md +6 -0
- package/corpus/templates/analytics/server/lib/first-party-metric-catalog.ts +19 -7
- package/dist/client/AgentPanel.d.ts +5 -1
- package/dist/client/AgentPanel.d.ts.map +1 -1
- package/dist/client/AgentPanel.js +23 -4
- package/dist/client/AgentPanel.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +1 -3
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +1 -17
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/composer/TiptapComposer.d.ts +4 -0
- package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/client/composer/TiptapComposer.js +5 -9
- package/dist/client/composer/TiptapComposer.js.map +1 -1
- package/dist/server/og-fonts-data.d.ts +1 -0
- package/dist/server/og-fonts-data.d.ts.map +1 -1
- package/dist/server/og-fonts-data.js +5 -4
- package/dist/server/og-fonts-data.js.map +1 -1
- package/dist/server/og-fonts.d.ts +1 -0
- package/dist/server/og-fonts.d.ts.map +1 -1
- package/dist/server/og-fonts.js +10 -4
- package/dist/server/og-fonts.js.map +1 -1
- package/dist/server/social-og-image.d.ts.map +1 -1
- package/dist/server/social-og-image.js +33 -6
- package/dist/server/social-og-image.js.map +1 -1
- package/package.json +1 -1
|
@@ -5,14 +5,16 @@ import path from "node:path";
|
|
|
5
5
|
import {
|
|
6
6
|
LIBERATION_SANS_BOLD_BASE64,
|
|
7
7
|
LIBERATION_SANS_REGULAR_BASE64,
|
|
8
|
+
NOTO_NASKH_ARABIC_BASE64,
|
|
8
9
|
} from "./og-fonts-data.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Liberation Sans is the metric-compatible libre replacement for
|
|
12
|
-
* Arial/Helvetica that the OG image SVG asks for.
|
|
13
|
+
* Arial/Helvetica that the OG image SVG asks for. Noto Naskh Arabic fills the
|
|
14
|
+
* Arabic script gap for localized docs titles. They ship embedded as base64
|
|
13
15
|
* (see {@link ./og-fonts-data.ts}) so the renderer never depends on the host's
|
|
14
|
-
* system fonts — Linux serverless runtimes (Netlify/Lambda) have neither Arial
|
|
15
|
-
*
|
|
16
|
+
* system fonts — Linux serverless runtimes (Netlify/Lambda) have neither Arial,
|
|
17
|
+
* Inter, nor Arabic fallback fonts.
|
|
16
18
|
*/
|
|
17
19
|
const OG_FONT_FILES = [
|
|
18
20
|
{
|
|
@@ -20,9 +22,14 @@ const OG_FONT_FILES = [
|
|
|
20
22
|
base64: LIBERATION_SANS_REGULAR_BASE64,
|
|
21
23
|
},
|
|
22
24
|
{ filename: "LiberationSans-Bold.ttf", base64: LIBERATION_SANS_BOLD_BASE64 },
|
|
25
|
+
{
|
|
26
|
+
filename: "NotoNaskhArabic-Variable.ttf",
|
|
27
|
+
base64: NOTO_NASKH_ARABIC_BASE64,
|
|
28
|
+
},
|
|
23
29
|
] as const;
|
|
24
30
|
|
|
25
31
|
export const OG_FONT_FAMILY = "Liberation Sans";
|
|
32
|
+
export const OG_ARABIC_FONT_FAMILY = "Noto Naskh Arabic";
|
|
26
33
|
|
|
27
34
|
let cachedFontFiles: string[] | null | undefined;
|
|
28
35
|
|
|
@@ -11,7 +11,11 @@ import {
|
|
|
11
11
|
resolveBuiltInAuthMarketingByName,
|
|
12
12
|
} from "./auth-marketing.js";
|
|
13
13
|
import { getAppName } from "./app-name.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
OG_ARABIC_FONT_FAMILY,
|
|
16
|
+
OG_FONT_FAMILY,
|
|
17
|
+
resolveOgFontFiles,
|
|
18
|
+
} from "./og-fonts.js";
|
|
15
19
|
|
|
16
20
|
export interface AgentNativeOgImageInput {
|
|
17
21
|
appName?: string | null;
|
|
@@ -32,7 +36,8 @@ const BRAND_BLUE = "#00B5FF";
|
|
|
32
36
|
const BRAND_MINT = "#48FFE4";
|
|
33
37
|
const BG = "#000000";
|
|
34
38
|
const FG = "#f5f5f5";
|
|
35
|
-
const
|
|
39
|
+
const DEFAULT_FONT_FAMILY = `${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
|
|
40
|
+
const ARABIC_FONT_FAMILY = `${OG_ARABIC_FONT_FAMILY}, ${OG_FONT_FAMILY}, Arial, Helvetica, system-ui, sans-serif`;
|
|
36
41
|
const DEFAULT_ACCENT_TEXT = "100% free and open source";
|
|
37
42
|
|
|
38
43
|
const LOGO_MARK = `
|
|
@@ -101,6 +106,16 @@ function estimateTextWidth(value: string, fontSize: number): number {
|
|
|
101
106
|
return units * fontSize;
|
|
102
107
|
}
|
|
103
108
|
|
|
109
|
+
function containsArabicText(value: string): boolean {
|
|
110
|
+
return /[\u0600-\u06ff\u0750-\u077f\u0870-\u089f\ufb50-\ufdff\ufe70-\ufeff]/u.test(
|
|
111
|
+
value,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fontFamilyForText(value: string): string {
|
|
116
|
+
return containsArabicText(value) ? ARABIC_FONT_FAMILY : DEFAULT_FONT_FAMILY;
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
function trimTextToWidth(
|
|
105
120
|
value: string,
|
|
106
121
|
fontSize: number,
|
|
@@ -205,6 +220,8 @@ function textBlock({
|
|
|
205
220
|
weight,
|
|
206
221
|
fill,
|
|
207
222
|
anchor = "start",
|
|
223
|
+
direction,
|
|
224
|
+
fontFamily = DEFAULT_FONT_FAMILY,
|
|
208
225
|
}: {
|
|
209
226
|
lines: string[];
|
|
210
227
|
x: number;
|
|
@@ -213,9 +230,14 @@ function textBlock({
|
|
|
213
230
|
lineHeight: number;
|
|
214
231
|
weight: number;
|
|
215
232
|
fill: string;
|
|
216
|
-
anchor?: "start" | "middle";
|
|
233
|
+
anchor?: "start" | "middle" | "end";
|
|
234
|
+
direction?: "ltr" | "rtl";
|
|
235
|
+
fontFamily?: string;
|
|
217
236
|
}): string {
|
|
218
|
-
|
|
237
|
+
const directionAttrs = direction
|
|
238
|
+
? ` direction="${direction}" unicode-bidi="plaintext"`
|
|
239
|
+
: "";
|
|
240
|
+
return `<text x="${x}" y="${y}" text-anchor="${anchor}"${directionAttrs} font-family="${fontFamily}" font-size="${fontSize}" font-weight="${weight}" fill="${fill}">${lines
|
|
219
241
|
.map(
|
|
220
242
|
(line, index) =>
|
|
221
243
|
`<tspan x="${x}" dy="${index === 0 ? 0 : lineHeight}">${escapeSvg(line)}</tspan>`,
|
|
@@ -289,6 +311,10 @@ export function renderAgentNativeOgImageSvg(
|
|
|
289
311
|
const title = cleanText(input.title) || titleFromAppName(appName);
|
|
290
312
|
const accentText = cleanText(input.accentText) || DEFAULT_ACCENT_TEXT;
|
|
291
313
|
const titleLayout = getTitleLayout(title);
|
|
314
|
+
const titleIsRtl = containsArabicText(title);
|
|
315
|
+
const textX = titleIsRtl ? WIDTH - 80 : 80;
|
|
316
|
+
const accentX = titleIsRtl ? WIDTH - 84 : 84;
|
|
317
|
+
const textAnchor = titleIsRtl ? "end" : "start";
|
|
292
318
|
const titleY = titleLayout.lines.length > 1 ? 288 : 330;
|
|
293
319
|
const accentY =
|
|
294
320
|
titleY + titleLayout.lineHeight * (titleLayout.lines.length - 1) + 70;
|
|
@@ -312,7 +338,7 @@ export function renderAgentNativeOgImageSvg(
|
|
|
312
338
|
<g>
|
|
313
339
|
${textBlock({
|
|
314
340
|
lines: titleLayout.lines,
|
|
315
|
-
x:
|
|
341
|
+
x: textX,
|
|
316
342
|
y: titleY,
|
|
317
343
|
fontSize: titleLayout.fontSize,
|
|
318
344
|
lineHeight: titleLayout.lineHeight,
|
|
@@ -321,8 +347,21 @@ export function renderAgentNativeOgImageSvg(
|
|
|
321
347
|
// bundle, which is the intended look for the display title.
|
|
322
348
|
weight: 800,
|
|
323
349
|
fill: FG,
|
|
350
|
+
anchor: textAnchor,
|
|
351
|
+
direction: titleIsRtl ? "rtl" : undefined,
|
|
352
|
+
fontFamily: fontFamilyForText(title),
|
|
353
|
+
})}
|
|
354
|
+
${textBlock({
|
|
355
|
+
lines: [accentText],
|
|
356
|
+
x: accentX,
|
|
357
|
+
y: accentY,
|
|
358
|
+
fontSize: 34,
|
|
359
|
+
lineHeight: 40,
|
|
360
|
+
weight: 800,
|
|
361
|
+
fill: BRAND_BLUE,
|
|
362
|
+
anchor: textAnchor,
|
|
363
|
+
fontFamily: fontFamilyForText(accentText),
|
|
324
364
|
})}
|
|
325
|
-
<text x="84" y="${accentY}" font-family="${FONT_FAMILY}" font-size="34" font-weight="800" fill="${BRAND_BLUE}">${escapeSvg(accentText)}</text>
|
|
326
365
|
</g>
|
|
327
366
|
</svg>`;
|
|
328
367
|
}
|
|
@@ -97,6 +97,10 @@ const CHART_LEGEND_PROPS = {
|
|
|
97
97
|
wrapperStyle: CHART_LEGEND_WRAPPER_STYLE,
|
|
98
98
|
} as const;
|
|
99
99
|
|
|
100
|
+
const PARTIAL_DAY_TIME_ZONE = "America/Los_Angeles";
|
|
101
|
+
const PARTIAL_DAY_DASH = "3 5";
|
|
102
|
+
const PARTIAL_DAY_KEY_PREFIX = "__sql_chart_partial_day";
|
|
103
|
+
|
|
100
104
|
function formatYValue(
|
|
101
105
|
value: number,
|
|
102
106
|
formatter?: "number" | "currency" | "percent",
|
|
@@ -167,6 +171,122 @@ function truncateLabel(value: string, max = 48): string {
|
|
|
167
171
|
return `${value.slice(0, max - 3)}...`;
|
|
168
172
|
}
|
|
169
173
|
|
|
174
|
+
function parseCalendarDate(value: string): Date | null {
|
|
175
|
+
const normalized = /^\d{8}$/.test(value)
|
|
176
|
+
? `${value.slice(0, 4)}-${value.slice(4, 6)}-${value.slice(6, 8)}`
|
|
177
|
+
: value;
|
|
178
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(normalized);
|
|
179
|
+
if (dateOnly) {
|
|
180
|
+
return new Date(
|
|
181
|
+
Number(dateOnly[1]),
|
|
182
|
+
Number(dateOnly[2]) - 1,
|
|
183
|
+
Number(dateOnly[3]),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
const parsed = new Date(normalized);
|
|
187
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function toSqlChartDateKey(value: unknown): string | null {
|
|
191
|
+
if (typeof value === "string") {
|
|
192
|
+
const trimmed = value.trim();
|
|
193
|
+
const ymd = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
|
|
194
|
+
if (ymd) return `${ymd[1]}-${ymd[2]}-${ymd[3]}`;
|
|
195
|
+
const compact = /^(\d{4})(\d{2})(\d{2})$/.exec(trimmed);
|
|
196
|
+
if (compact) return `${compact[1]}-${compact[2]}-${compact[3]}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const parsed = new Date(String(value ?? ""));
|
|
200
|
+
return Number.isNaN(parsed.getTime()) ? null : sqlChartLocalDateKey(parsed);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function sqlChartLocalDateKey(
|
|
204
|
+
date = new Date(),
|
|
205
|
+
timeZone = PARTIAL_DAY_TIME_ZONE,
|
|
206
|
+
): string {
|
|
207
|
+
try {
|
|
208
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
209
|
+
timeZone,
|
|
210
|
+
year: "numeric",
|
|
211
|
+
month: "2-digit",
|
|
212
|
+
day: "2-digit",
|
|
213
|
+
}).formatToParts(date);
|
|
214
|
+
const get = (type: string) =>
|
|
215
|
+
parts.find((part) => part.type === type)?.value ?? "";
|
|
216
|
+
const year = get("year");
|
|
217
|
+
const month = get("month");
|
|
218
|
+
const day = get("day");
|
|
219
|
+
if (year && month && day) return `${year}-${month}-${day}`;
|
|
220
|
+
} catch {}
|
|
221
|
+
return date.toISOString().slice(0, 10);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export interface SplitTimeSeries {
|
|
225
|
+
key: string;
|
|
226
|
+
solidKey: string;
|
|
227
|
+
partialKey: string | null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function splitCurrentDayTimeSeriesRows(
|
|
231
|
+
rows: Record<string, unknown>[],
|
|
232
|
+
xKey: string,
|
|
233
|
+
yKeys: string[],
|
|
234
|
+
todayKey = sqlChartLocalDateKey(),
|
|
235
|
+
): { rows: Record<string, unknown>[]; series: SplitTimeSeries[] } {
|
|
236
|
+
const todayIndexes = rows
|
|
237
|
+
.map((row, index) => ({ index, dateKey: toSqlChartDateKey(row[xKey]) }))
|
|
238
|
+
.filter((entry) => entry.dateKey === todayKey)
|
|
239
|
+
.map((entry) => entry.index);
|
|
240
|
+
const firstTodayIndex = todayIndexes[0] ?? -1;
|
|
241
|
+
|
|
242
|
+
if (firstTodayIndex <= 0 || yKeys.length === 0) {
|
|
243
|
+
return {
|
|
244
|
+
rows,
|
|
245
|
+
series: yKeys.map((key) => ({ key, solidKey: key, partialKey: null })),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const todaySet = new Set(todayIndexes);
|
|
250
|
+
const previousIndex = firstTodayIndex - 1;
|
|
251
|
+
const series = yKeys.map((key, index) => ({
|
|
252
|
+
key,
|
|
253
|
+
solidKey: `${PARTIAL_DAY_KEY_PREFIX}_${index}_solid`,
|
|
254
|
+
partialKey: `${PARTIAL_DAY_KEY_PREFIX}_${index}_partial`,
|
|
255
|
+
}));
|
|
256
|
+
const splitRows = rows.map((row, index) => {
|
|
257
|
+
const next = { ...row };
|
|
258
|
+
const isToday = todaySet.has(index);
|
|
259
|
+
const isPartialSegmentStart = index === previousIndex;
|
|
260
|
+
for (const item of series) {
|
|
261
|
+
next[item.solidKey] = isToday ? null : row[item.key];
|
|
262
|
+
next[item.partialKey] =
|
|
263
|
+
isToday || isPartialSegmentStart ? row[item.key] : null;
|
|
264
|
+
}
|
|
265
|
+
return next;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
return { rows: splitRows, series };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function shouldSplitCurrentDayTimeSeries(
|
|
272
|
+
panel: Pick<SqlPanel, "source">,
|
|
273
|
+
xKey: string,
|
|
274
|
+
): boolean {
|
|
275
|
+
if (panel.source === "prometheus") return false;
|
|
276
|
+
|
|
277
|
+
const normalizedKey = xKey.trim().toLowerCase();
|
|
278
|
+
if (normalizedKey === "timestamp" || normalizedKey.endsWith("_timestamp")) {
|
|
279
|
+
return false;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
normalizedKey === "date" ||
|
|
284
|
+
normalizedKey === "day" ||
|
|
285
|
+
normalizedKey.endsWith("_date") ||
|
|
286
|
+
normalizedKey.endsWith("_day")
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
170
290
|
function formatSeriesLabel(value: string): string {
|
|
171
291
|
const { metric, labels } = parsePrometheusSeriesLabel(value);
|
|
172
292
|
const target =
|
|
@@ -232,11 +352,8 @@ function formatSeriesLabelForPanel(panel: SqlPanel, value: string): string {
|
|
|
232
352
|
function formatXLabel(value: string, panel: SqlPanel): string {
|
|
233
353
|
try {
|
|
234
354
|
const s = String(value);
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
: s;
|
|
238
|
-
const d = new Date(normalized);
|
|
239
|
-
if (!isNaN(d.getTime()) && s.length >= 8) {
|
|
355
|
+
const d = parseCalendarDate(s);
|
|
356
|
+
if (d && s.length >= 8) {
|
|
240
357
|
return d.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
241
358
|
}
|
|
242
359
|
} catch {}
|
|
@@ -252,12 +369,21 @@ function numericTooltipValue(value: unknown): number {
|
|
|
252
369
|
return Number.isFinite(numeric) ? numeric : Number.NEGATIVE_INFINITY;
|
|
253
370
|
}
|
|
254
371
|
|
|
372
|
+
function tooltipItemName(item: {
|
|
373
|
+
dataKey?: string | number;
|
|
374
|
+
name?: string | number;
|
|
375
|
+
}): string {
|
|
376
|
+
return String(item.name ?? item.dataKey ?? "");
|
|
377
|
+
}
|
|
378
|
+
|
|
255
379
|
export function sortTooltipPayloadItems<
|
|
256
380
|
T extends {
|
|
381
|
+
dataKey?: string | number;
|
|
382
|
+
name?: string | number;
|
|
257
383
|
value?: unknown;
|
|
258
384
|
},
|
|
259
385
|
>(items: T[]): T[] {
|
|
260
|
-
|
|
386
|
+
const sorted = items
|
|
261
387
|
.map((item, index) => ({ item, index }))
|
|
262
388
|
.sort((a, b) => {
|
|
263
389
|
const diff =
|
|
@@ -265,6 +391,13 @@ export function sortTooltipPayloadItems<
|
|
|
265
391
|
return diff !== 0 ? diff : a.index - b.index;
|
|
266
392
|
})
|
|
267
393
|
.map(({ item }) => item);
|
|
394
|
+
const seen = new Set<string>();
|
|
395
|
+
return sorted.filter((item) => {
|
|
396
|
+
const name = tooltipItemName(item);
|
|
397
|
+
if (seen.has(name)) return false;
|
|
398
|
+
seen.add(name);
|
|
399
|
+
return true;
|
|
400
|
+
});
|
|
268
401
|
}
|
|
269
402
|
|
|
270
403
|
function useSeriesVisibility(keys: string[]): {
|
|
@@ -1390,6 +1523,21 @@ function TimeSeriesRenderer({
|
|
|
1390
1523
|
const seriesNameFormatter = (name: string) =>
|
|
1391
1524
|
formatSeriesLabelForPanel(panel, name);
|
|
1392
1525
|
const { hiddenKeys, visibleKeys, toggleSeries } = useSeriesVisibility(yKeys);
|
|
1526
|
+
const splitPartialDay = shouldSplitCurrentDayTimeSeries(panel, xKey);
|
|
1527
|
+
const { rows: chartRows, series } = useMemo(
|
|
1528
|
+
() =>
|
|
1529
|
+
splitPartialDay
|
|
1530
|
+
? splitCurrentDayTimeSeriesRows(rows, xKey, yKeys)
|
|
1531
|
+
: {
|
|
1532
|
+
rows,
|
|
1533
|
+
series: yKeys.map((key) => ({
|
|
1534
|
+
key,
|
|
1535
|
+
solidKey: key,
|
|
1536
|
+
partialKey: null,
|
|
1537
|
+
})),
|
|
1538
|
+
},
|
|
1539
|
+
[rows, xKey, yKeys, splitPartialDay],
|
|
1540
|
+
);
|
|
1393
1541
|
|
|
1394
1542
|
if (chartType === "line") {
|
|
1395
1543
|
return (
|
|
@@ -1402,7 +1550,7 @@ function TimeSeriesRenderer({
|
|
|
1402
1550
|
showCustomLegend
|
|
1403
1551
|
>
|
|
1404
1552
|
<ResponsiveContainer width="100%" height="100%">
|
|
1405
|
-
<LineChart data={
|
|
1553
|
+
<LineChart data={chartRows}>
|
|
1406
1554
|
<XAxis
|
|
1407
1555
|
dataKey={xKey}
|
|
1408
1556
|
stroke="hsl(var(--muted-foreground))"
|
|
@@ -1435,18 +1583,33 @@ function TimeSeriesRenderer({
|
|
|
1435
1583
|
}
|
|
1436
1584
|
itemSorter={(item) => -(Number(item.value) || 0)}
|
|
1437
1585
|
/>
|
|
1438
|
-
{
|
|
1586
|
+
{series.map((item, i) => (
|
|
1439
1587
|
<Line
|
|
1440
|
-
key={
|
|
1588
|
+
key={item.solidKey}
|
|
1441
1589
|
type="monotone"
|
|
1442
|
-
dataKey={
|
|
1443
|
-
name={seriesNameFormatter(key)}
|
|
1590
|
+
dataKey={item.solidKey}
|
|
1591
|
+
name={seriesNameFormatter(item.key)}
|
|
1444
1592
|
stroke={colors[i % colors.length]}
|
|
1445
1593
|
strokeWidth={2}
|
|
1446
1594
|
dot={false}
|
|
1447
|
-
hide={hiddenKeys.has(key)}
|
|
1595
|
+
hide={hiddenKeys.has(item.key)}
|
|
1448
1596
|
/>
|
|
1449
1597
|
))}
|
|
1598
|
+
{series.map((item, i) =>
|
|
1599
|
+
item.partialKey ? (
|
|
1600
|
+
<Line
|
|
1601
|
+
key={item.partialKey}
|
|
1602
|
+
type="monotone"
|
|
1603
|
+
dataKey={item.partialKey}
|
|
1604
|
+
name={seriesNameFormatter(item.key)}
|
|
1605
|
+
stroke={colors[i % colors.length]}
|
|
1606
|
+
strokeWidth={2}
|
|
1607
|
+
strokeDasharray={PARTIAL_DAY_DASH}
|
|
1608
|
+
dot={false}
|
|
1609
|
+
hide={hiddenKeys.has(item.key)}
|
|
1610
|
+
/>
|
|
1611
|
+
) : null,
|
|
1612
|
+
)}
|
|
1450
1613
|
</LineChart>
|
|
1451
1614
|
</ResponsiveContainer>
|
|
1452
1615
|
</ChartFrame>
|
|
@@ -1468,7 +1631,7 @@ function TimeSeriesRenderer({
|
|
|
1468
1631
|
showCustomLegend
|
|
1469
1632
|
>
|
|
1470
1633
|
<ResponsiveContainer width="100%" height="100%">
|
|
1471
|
-
<AreaChart data={
|
|
1634
|
+
<AreaChart data={chartRows}>
|
|
1472
1635
|
{showFill && (
|
|
1473
1636
|
<defs>
|
|
1474
1637
|
{yKeys.map((key, i) => (
|
|
@@ -1526,20 +1689,37 @@ function TimeSeriesRenderer({
|
|
|
1526
1689
|
}
|
|
1527
1690
|
itemSorter={(item) => -(Number(item.value) || 0)}
|
|
1528
1691
|
/>
|
|
1529
|
-
{
|
|
1692
|
+
{series.map((item, i) => (
|
|
1530
1693
|
<Area
|
|
1531
|
-
key={
|
|
1694
|
+
key={item.solidKey}
|
|
1532
1695
|
type="monotone"
|
|
1533
|
-
dataKey={
|
|
1534
|
-
name={seriesNameFormatter(key)}
|
|
1696
|
+
dataKey={item.solidKey}
|
|
1697
|
+
name={seriesNameFormatter(item.key)}
|
|
1535
1698
|
stroke={colors[i % colors.length]}
|
|
1536
1699
|
strokeWidth={2}
|
|
1537
1700
|
fillOpacity={showFill ? 1 : 0}
|
|
1538
|
-
fill={showFill ? `url(#sql-gradient-${key})` : "none"}
|
|
1701
|
+
fill={showFill ? `url(#sql-gradient-${item.key})` : "none"}
|
|
1539
1702
|
stackId={stacked ? "stack" : undefined}
|
|
1540
|
-
hide={hiddenKeys.has(key)}
|
|
1703
|
+
hide={hiddenKeys.has(item.key)}
|
|
1541
1704
|
/>
|
|
1542
1705
|
))}
|
|
1706
|
+
{series.map((item, i) =>
|
|
1707
|
+
item.partialKey ? (
|
|
1708
|
+
<Area
|
|
1709
|
+
key={item.partialKey}
|
|
1710
|
+
type="monotone"
|
|
1711
|
+
dataKey={item.partialKey}
|
|
1712
|
+
name={seriesNameFormatter(item.key)}
|
|
1713
|
+
stroke={colors[i % colors.length]}
|
|
1714
|
+
strokeWidth={2}
|
|
1715
|
+
strokeDasharray={PARTIAL_DAY_DASH}
|
|
1716
|
+
fill="none"
|
|
1717
|
+
fillOpacity={0}
|
|
1718
|
+
stackId={stacked ? "partial-stack" : undefined}
|
|
1719
|
+
hide={hiddenKeys.has(item.key)}
|
|
1720
|
+
/>
|
|
1721
|
+
) : null,
|
|
1722
|
+
)}
|
|
1543
1723
|
</AreaChart>
|
|
1544
1724
|
</ResponsiveContainer>
|
|
1545
1725
|
</ChartFrame>
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Suspense, lazy, useEffect } from "react";
|
|
2
2
|
import { useParams } from "react-router";
|
|
3
|
-
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
4
|
-
import { Skeleton } from "@/components/ui/skeleton";
|
|
5
3
|
import { dashboardComponents } from "./registry";
|
|
6
4
|
import { incrementItemView } from "@/lib/item-popularity";
|
|
5
|
+
import { DashboardSkeleton } from "./DashboardSkeleton";
|
|
7
6
|
|
|
8
7
|
const SqlDashboardPage = lazy(() => import("./sql-dashboard"));
|
|
9
8
|
|
|
@@ -11,25 +10,6 @@ const SqlDashboardPage = lazy(() => import("./sql-dashboard"));
|
|
|
11
10
|
// Suspense → dashboard config load. Matches the real SqlChartCard shape (Card
|
|
12
11
|
// chrome + title row + chart-body skeleton) so the user sees one continuous
|
|
13
12
|
// skeleton state rather than four different ones morphing into each other.
|
|
14
|
-
function DashboardSkeleton() {
|
|
15
|
-
return (
|
|
16
|
-
<div className="space-y-4">
|
|
17
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
18
|
-
{[0, 1].map((i) => (
|
|
19
|
-
<Card key={i} className="flex flex-col overflow-visible">
|
|
20
|
-
<CardHeader className="pb-2 shrink-0">
|
|
21
|
-
<Skeleton className="h-4 w-32" />
|
|
22
|
-
</CardHeader>
|
|
23
|
-
<CardContent className="flex flex-1 flex-col pt-0">
|
|
24
|
-
<Skeleton className="w-full flex-1 min-h-[250px]" />
|
|
25
|
-
</CardContent>
|
|
26
|
-
</Card>
|
|
27
|
-
))}
|
|
28
|
-
</div>
|
|
29
|
-
</div>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
13
|
function SqlDashboardLoader() {
|
|
34
14
|
return (
|
|
35
15
|
<Suspense fallback={<DashboardSkeleton />}>
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
|
3
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
4
|
+
|
|
5
|
+
interface DashboardSkeletonProps {
|
|
6
|
+
columns?: number;
|
|
7
|
+
count?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DashboardSkeleton({
|
|
11
|
+
columns = 2,
|
|
12
|
+
count = 2,
|
|
13
|
+
}: DashboardSkeletonProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="dashboard-grid-container space-y-4">
|
|
16
|
+
<div
|
|
17
|
+
className="dashboard-grid"
|
|
18
|
+
style={{ "--dash-cols": columns } as CSSProperties}
|
|
19
|
+
>
|
|
20
|
+
{Array.from({ length: count }, (_, i) => (
|
|
21
|
+
<div key={i} className="dashboard-grid-cell h-full">
|
|
22
|
+
<Card className="flex h-full flex-col overflow-visible">
|
|
23
|
+
<CardHeader className="pb-2 shrink-0">
|
|
24
|
+
<Skeleton className="h-4 w-32" />
|
|
25
|
+
</CardHeader>
|
|
26
|
+
<CardContent className="flex flex-1 flex-col pt-0">
|
|
27
|
+
<Skeleton className="w-full flex-1 min-h-[250px]" />
|
|
28
|
+
</CardContent>
|
|
29
|
+
</Card>
|
|
30
|
+
</div>
|
|
31
|
+
))}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -3,7 +3,7 @@ import { useSearchParams, useNavigate } from "react-router";
|
|
|
3
3
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
4
|
import { Button } from "@/components/ui/button";
|
|
5
5
|
import { Input } from "@/components/ui/input";
|
|
6
|
-
import { Card, CardContent
|
|
6
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
7
7
|
import {
|
|
8
8
|
Dialog,
|
|
9
9
|
DialogContent,
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
AlertDialogHeader,
|
|
22
22
|
AlertDialogTitle,
|
|
23
23
|
} from "@/components/ui/alert-dialog";
|
|
24
|
-
import { Skeleton } from "@/components/ui/skeleton";
|
|
25
24
|
import {
|
|
26
25
|
IconPlus,
|
|
27
26
|
IconTrash,
|
|
@@ -82,6 +81,7 @@ import {
|
|
|
82
81
|
TooltipContent,
|
|
83
82
|
TooltipTrigger,
|
|
84
83
|
} from "@/components/ui/tooltip";
|
|
84
|
+
import { DashboardSkeleton } from "../DashboardSkeleton";
|
|
85
85
|
|
|
86
86
|
export interface DashboardChart {
|
|
87
87
|
id: string;
|
|
@@ -485,27 +485,7 @@ export default function ExplorerDashboardPage() {
|
|
|
485
485
|
}
|
|
486
486
|
|
|
487
487
|
if (!loaded) {
|
|
488
|
-
|
|
489
|
-
// chart-body skeleton) so the transition from "dashboard config loading"
|
|
490
|
-
// to "queries loading" doesn't morph skeleton shape — the title text just
|
|
491
|
-
// fills in. Otherwise the bare h-64 rectangles jump into Card-chromed
|
|
492
|
-
// panels and the page visibly shifts.
|
|
493
|
-
return (
|
|
494
|
-
<div className="space-y-4">
|
|
495
|
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
496
|
-
{[0, 1].map((i) => (
|
|
497
|
-
<Card key={i} className="flex flex-col overflow-visible">
|
|
498
|
-
<CardHeader className="pb-2 shrink-0">
|
|
499
|
-
<Skeleton className="h-4 w-32" />
|
|
500
|
-
</CardHeader>
|
|
501
|
-
<CardContent className="flex flex-1 flex-col pt-0">
|
|
502
|
-
<Skeleton className="w-full flex-1 min-h-[250px]" />
|
|
503
|
-
</CardContent>
|
|
504
|
-
</Card>
|
|
505
|
-
))}
|
|
506
|
-
</div>
|
|
507
|
-
</div>
|
|
508
|
-
);
|
|
488
|
+
return <DashboardSkeleton />;
|
|
509
489
|
}
|
|
510
490
|
|
|
511
491
|
if (!dashboard) return null;
|
|
@@ -12,8 +12,7 @@ import { toast } from "sonner";
|
|
|
12
12
|
import { Button } from "@/components/ui/button";
|
|
13
13
|
import { Input } from "@/components/ui/input";
|
|
14
14
|
import { Textarea } from "@/components/ui/textarea";
|
|
15
|
-
import { Card, CardContent
|
|
16
|
-
import { Skeleton } from "@/components/ui/skeleton";
|
|
15
|
+
import { Card, CardContent } from "@/components/ui/card";
|
|
17
16
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
18
17
|
import {
|
|
19
18
|
AlertDialog,
|
|
@@ -90,6 +89,7 @@ import {
|
|
|
90
89
|
sqlDashboardPrefetchKey,
|
|
91
90
|
type PrefetchSnapshot,
|
|
92
91
|
} from "@/lib/prefetch-keys";
|
|
92
|
+
import { DashboardSkeleton } from "../DashboardSkeleton";
|
|
93
93
|
import {
|
|
94
94
|
resourceCanEdit,
|
|
95
95
|
resourceCanManage,
|
|
@@ -1119,30 +1119,7 @@ export default function SqlDashboardPage() {
|
|
|
1119
1119
|
}
|
|
1120
1120
|
|
|
1121
1121
|
if (!loaded) {
|
|
1122
|
-
|
|
1123
|
-
// chart-body skeleton) so the transition from "dashboard config loading" to
|
|
1124
|
-
// "queries loading" doesn't morph the skeleton's shape — only the title
|
|
1125
|
-
// text fills in. Otherwise the bare h-64 rectangles jump into Card-chromed
|
|
1126
|
-
// panels and the page visibly shifts.
|
|
1127
|
-
return (
|
|
1128
|
-
<div className="dashboard-grid-container space-y-4">
|
|
1129
|
-
<div
|
|
1130
|
-
className="dashboard-grid"
|
|
1131
|
-
style={{ "--dash-cols": 2 } as React.CSSProperties}
|
|
1132
|
-
>
|
|
1133
|
-
{[0, 1].map((i) => (
|
|
1134
|
-
<Card key={i} className="flex flex-col overflow-visible">
|
|
1135
|
-
<CardHeader className="pb-2 shrink-0">
|
|
1136
|
-
<Skeleton className="h-4 w-32" />
|
|
1137
|
-
</CardHeader>
|
|
1138
|
-
<CardContent className="flex flex-1 flex-col pt-0">
|
|
1139
|
-
<Skeleton className="w-full flex-1 min-h-[250px]" />
|
|
1140
|
-
</CardContent>
|
|
1141
|
-
</Card>
|
|
1142
|
-
))}
|
|
1143
|
-
</div>
|
|
1144
|
-
</div>
|
|
1145
|
-
);
|
|
1122
|
+
return <DashboardSkeleton />;
|
|
1146
1123
|
}
|
|
1147
1124
|
|
|
1148
1125
|
if (!dashboard) return <BlankDashboard />;
|