@agent-native/core 0.72.4 → 0.73.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 +28 -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 +25 -12
- package/corpus/core/src/cli/create.ts +1 -1
- package/corpus/core/src/cli/skills.ts +450 -19
- package/corpus/core/src/server/auth.ts +7 -0
- package/corpus/core/src/server/onboarding-html.ts +95 -9
- 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/update-dashboard.ts +3 -4
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +144 -40
- 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-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 +256 -44
- 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 +287 -42
- 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/player/player-controls.tsx +62 -32
- package/corpus/templates/clips/app/lib/capture-install-options.ts +39 -5
- 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-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/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 +24 -11
- package/dist/agent/durable-background.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/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/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
|
@@ -55,6 +55,36 @@ function withAppBasePath(path: string): string {
|
|
|
55
55
|
return `${basePath}${cleanPath}`;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
const AGENT_NATIVE_TERMS_URL = "https://www.agent-native.com/terms";
|
|
59
|
+
const AGENT_NATIVE_PRIVACY_URL = "https://www.agent-native.com/privacy";
|
|
60
|
+
|
|
61
|
+
export interface SignupLegalNoticeOptions {
|
|
62
|
+
termsUrl: string;
|
|
63
|
+
privacyUrl: string;
|
|
64
|
+
termsLabel?: string;
|
|
65
|
+
privacyLabel?: string;
|
|
66
|
+
prefix?: string;
|
|
67
|
+
connector?: string;
|
|
68
|
+
suffix?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeRequestHostname(host: string | undefined): string {
|
|
72
|
+
const firstHost = host?.split(",")[0]?.trim().toLowerCase() ?? "";
|
|
73
|
+
if (!firstHost) return "";
|
|
74
|
+
if (firstHost.startsWith("[")) {
|
|
75
|
+
const close = firstHost.indexOf("]");
|
|
76
|
+
return close > 0 ? firstHost.slice(1, close) : firstHost;
|
|
77
|
+
}
|
|
78
|
+
return firstHost.replace(/:\d+$/, "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isAgentNativeHostedHost(host: string | undefined): boolean {
|
|
82
|
+
const hostname = normalizeRequestHostname(host);
|
|
83
|
+
return (
|
|
84
|
+
hostname === "agent-native.com" || hostname.endsWith(".agent-native.com")
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
58
88
|
export interface OnboardingHtmlOptions {
|
|
59
89
|
/**
|
|
60
90
|
* Hide email/password forms and show ONLY the Google sign-in button.
|
|
@@ -93,6 +123,12 @@ export interface OnboardingHtmlOptions {
|
|
|
93
123
|
continueLabel?: string;
|
|
94
124
|
cancelLabel?: string;
|
|
95
125
|
};
|
|
126
|
+
/**
|
|
127
|
+
* Optional email signup legal copy. Builder-hosted `*.agent-native.com`
|
|
128
|
+
* deployments get the Agent Native links automatically; self-hosted and
|
|
129
|
+
* custom-domain apps must opt in with their own URLs.
|
|
130
|
+
*/
|
|
131
|
+
signupLegalNotice?: SignupLegalNoticeOptions | false;
|
|
96
132
|
/**
|
|
97
133
|
* Google sign-in flow: `'popup'`, `'redirect'`, or `'auto'` (default).
|
|
98
134
|
* Falls back to `GOOGLE_AUTH_MODE` env var, then `'auto'`. Builder web
|
|
@@ -139,6 +175,21 @@ export function getOnboardingHtml(opts: OnboardingHtmlOptions = {}): string {
|
|
|
139
175
|
.replace(/</g, "<")
|
|
140
176
|
.replace(/>/g, ">")
|
|
141
177
|
.replace(/"/g, """);
|
|
178
|
+
const hostedSignupLegalNotice: SignupLegalNoticeOptions | undefined =
|
|
179
|
+
opts.signupLegalNotice === undefined &&
|
|
180
|
+
isAgentNativeHostedHost(opts.requestHost)
|
|
181
|
+
? {
|
|
182
|
+
termsUrl: AGENT_NATIVE_TERMS_URL,
|
|
183
|
+
privacyUrl: AGENT_NATIVE_PRIVACY_URL,
|
|
184
|
+
}
|
|
185
|
+
: undefined;
|
|
186
|
+
const signupLegalNotice =
|
|
187
|
+
opts.signupLegalNotice === false
|
|
188
|
+
? undefined
|
|
189
|
+
: (opts.signupLegalNotice ?? hostedSignupLegalNotice);
|
|
190
|
+
const signupLegalNoteHtml = signupLegalNotice
|
|
191
|
+
? ` <p class="legal-note">${esc(signupLegalNotice.prefix ?? "By signing up, you accept our")} <a href="${esc(signupLegalNotice.termsUrl)}" target="_blank" rel="noreferrer">${esc(signupLegalNotice.termsLabel ?? "Terms")}</a> ${esc(signupLegalNotice.connector ?? "and")} <a href="${esc(signupLegalNotice.privacyUrl)}" target="_blank" rel="noreferrer">${esc(signupLegalNotice.privacyLabel ?? "Privacy Policy")}</a>${esc(signupLegalNotice.suffix ?? ".")}</p>`
|
|
192
|
+
: "";
|
|
142
193
|
const googleSignInNotice = opts.googleSignInNotice;
|
|
143
194
|
const googleNoticeBodyParts = googleSignInNotice
|
|
144
195
|
? (Array.isArray(googleSignInNotice.body)
|
|
@@ -177,11 +228,16 @@ export function getOnboardingHtml(opts: OnboardingHtmlOptions = {}): string {
|
|
|
177
228
|
aria-describedby="google-preflight-copy"
|
|
178
229
|
tabindex="-1"
|
|
179
230
|
>
|
|
180
|
-
<
|
|
181
|
-
|
|
182
|
-
<
|
|
183
|
-
|
|
231
|
+
<button type="button" class="google-preflight-close" aria-label="Close Google sign-in choices" onclick="__anHideGoogleNotice()">×</button>
|
|
232
|
+
<div class="google-preflight-main">
|
|
233
|
+
<span class="google-preflight-icon" aria-hidden="true">
|
|
234
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.24 3.957l-8.422 14.06a1.989 1.989 0 0 0 1.7 2.983h16.845a1.989 1.989 0 0 0 1.7 -2.983l-8.423 -14.06a1.989 1.989 0 0 0 -3.4 0z"/><path d="M12 9v4"/><path d="M12 16h.01"/></svg>
|
|
235
|
+
</span>
|
|
236
|
+
<div class="google-preflight-text">
|
|
237
|
+
<p class="google-preflight-title" id="google-preflight-title">${esc(googleSignInNotice.title)}</p>
|
|
184
238
|
${googleNoticeBodyHtml}
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
185
241
|
<div class="google-preflight-actions">
|
|
186
242
|
<button type="button" class="btn-primary" id="google-preflight-continue" onclick="__anAcceptGoogleNotice()">${esc(googleSignInNotice.continueLabel ?? "Continue")}</button>
|
|
187
243
|
${googleNoticeRunLocalHtml}
|
|
@@ -707,6 +763,19 @@ ${
|
|
|
707
763
|
cursor: pointer;
|
|
708
764
|
}
|
|
709
765
|
.btn-secondary:hover { color: #bbb; border-color: rgba(255,255,255,0.2); }
|
|
766
|
+
.legal-note {
|
|
767
|
+
margin-top: 0.625rem;
|
|
768
|
+
color: #666;
|
|
769
|
+
font-size: 0.6875rem;
|
|
770
|
+
line-height: 1.45;
|
|
771
|
+
text-align: center;
|
|
772
|
+
}
|
|
773
|
+
.legal-note a {
|
|
774
|
+
color: #777;
|
|
775
|
+
text-decoration: underline;
|
|
776
|
+
text-underline-offset: 2px;
|
|
777
|
+
}
|
|
778
|
+
.legal-note a:hover { color: #aaa; }
|
|
710
779
|
.msg { margin-top: 0.75rem; font-size: 0.8125rem; display: none; }
|
|
711
780
|
.msg.error { color: #f87171; }
|
|
712
781
|
.msg.success { color: #33C4FF; }
|
|
@@ -898,25 +967,40 @@ ${
|
|
|
898
967
|
border-left: 1px solid rgba(255,255,255,0.12);
|
|
899
968
|
border-top: 1px solid rgba(255,255,255,0.12);
|
|
900
969
|
}
|
|
901
|
-
.google-preflight-
|
|
970
|
+
.google-preflight-main {
|
|
902
971
|
display: flex;
|
|
903
972
|
align-items: flex-start;
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
margin-bottom: 0.375rem;
|
|
973
|
+
gap: 0.625rem;
|
|
974
|
+
padding-right: 1.25rem;
|
|
907
975
|
}
|
|
976
|
+
.google-preflight-icon {
|
|
977
|
+
display: inline-flex;
|
|
978
|
+
align-items: center;
|
|
979
|
+
justify-content: center;
|
|
980
|
+
flex: none;
|
|
981
|
+
width: 1.75rem;
|
|
982
|
+
height: 1.75rem;
|
|
983
|
+
border-radius: 7px;
|
|
984
|
+
background: rgba(245,158,11,0.15);
|
|
985
|
+
color: #fcd34d;
|
|
986
|
+
}
|
|
987
|
+
.google-preflight-icon svg { width: 1rem; height: 1rem; }
|
|
988
|
+
.google-preflight-text { min-width: 0; }
|
|
908
989
|
.google-preflight-title {
|
|
909
990
|
color: #fff;
|
|
910
991
|
font-size: 0.8125rem;
|
|
911
992
|
font-weight: 600;
|
|
993
|
+
margin-bottom: 0.25rem;
|
|
912
994
|
}
|
|
913
995
|
.google-preflight-close {
|
|
996
|
+
position: absolute;
|
|
997
|
+
top: 0.5rem;
|
|
998
|
+
right: 0.5rem;
|
|
914
999
|
display: inline-flex;
|
|
915
1000
|
align-items: center;
|
|
916
1001
|
justify-content: center;
|
|
917
1002
|
width: 1.5rem;
|
|
918
1003
|
height: 1.5rem;
|
|
919
|
-
margin: -0.25rem -0.25rem 0 0;
|
|
920
1004
|
background: transparent;
|
|
921
1005
|
border: none;
|
|
922
1006
|
border-radius: 999px;
|
|
@@ -942,6 +1026,7 @@ ${
|
|
|
942
1026
|
flex: 1;
|
|
943
1027
|
width: auto;
|
|
944
1028
|
margin-top: 0;
|
|
1029
|
+
white-space: nowrap;
|
|
945
1030
|
}
|
|
946
1031
|
.google-preflight-command {
|
|
947
1032
|
margin-top: 0.75rem;
|
|
@@ -1039,6 +1124,7 @@ ${
|
|
|
1039
1124
|
<label for="s-pass2">Confirm password</label>
|
|
1040
1125
|
<input id="s-pass2" type="password" autocomplete="new-password" placeholder="Confirm password" required minlength="8" />
|
|
1041
1126
|
<button type="submit">Create account</button>
|
|
1127
|
+
${signupLegalNoteHtml}
|
|
1042
1128
|
<p class="msg" id="s-msg"></p>
|
|
1043
1129
|
</form>
|
|
1044
1130
|
|
|
@@ -38,6 +38,10 @@ Read these local package docs before implementing advanced Agent Native
|
|
|
38
38
|
features. Prefer this app's own `AGENTS.md` and `.agents/skills/` for
|
|
39
39
|
app-specific rules, then use the corpus for reusable framework/template
|
|
40
40
|
patterns.
|
|
41
|
+
After updating `@agent-native/core`, run `pnpm skills:update` or
|
|
42
|
+
`npx @agent-native/core@latest skills update scaffold --project` from the app
|
|
43
|
+
root to refresh framework-provided `.agents/skills` and repair `CLAUDE.md` /
|
|
44
|
+
`.claude/skills` compatibility links.
|
|
41
45
|
|
|
42
46
|
### Database Code
|
|
43
47
|
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"start": "agent-native start",
|
|
9
9
|
"typecheck": "agent-native typecheck",
|
|
10
10
|
"action": "agent-native action",
|
|
11
|
-
"script": "agent-native script"
|
|
11
|
+
"script": "agent-native script",
|
|
12
|
+
"skills:update": "agent-native skills update scaffold --project"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
15
|
"@agent-native/core": "latest",
|
|
@@ -38,6 +38,10 @@ first-party template patterns ships in `node_modules/@agent-native/core/corpus`.
|
|
|
38
38
|
Read these local package docs before implementing advanced Agent Native
|
|
39
39
|
features. Prefer this app's own `AGENTS.md` for app-specific rules, then use
|
|
40
40
|
the corpus for reusable framework/template patterns.
|
|
41
|
+
After updating `@agent-native/core`, run `pnpm skills:update` or
|
|
42
|
+
`npx @agent-native/core@latest skills update scaffold --project` from the app
|
|
43
|
+
root to refresh framework-provided `.agents/skills` and repair `CLAUDE.md` /
|
|
44
|
+
`.claude/skills` compatibility links.
|
|
41
45
|
|
|
42
46
|
## Actions
|
|
43
47
|
|
|
@@ -27,6 +27,10 @@ first-party template patterns ships in `node_modules/@agent-native/core/corpus`.
|
|
|
27
27
|
Use package docs for framework APIs, the package corpus for reusable
|
|
28
28
|
framework/template patterns, and `packages/shared/AGENTS.md` plus
|
|
29
29
|
`packages/shared/.agents/skills/` for workspace-specific conventions.
|
|
30
|
+
After updating `@agent-native/core`, run `pnpm skills:update` or
|
|
31
|
+
`npx @agent-native/core@latest skills update scaffold --project` from the
|
|
32
|
+
workspace root to refresh framework-provided shared skills and repair
|
|
33
|
+
`CLAUDE.md` / `.claude/skills` compatibility links.
|
|
30
34
|
|
|
31
35
|
## Core Agent Rule
|
|
32
36
|
|
|
@@ -35,6 +35,10 @@ the shared package (`@{{APP_NAME}}/shared`).
|
|
|
35
35
|
|
|
36
36
|
The workspace root also links `.agents/skills` to the shared package so coding
|
|
37
37
|
agents launched from the root can discover the same workspace-wide skills.
|
|
38
|
+
Run `pnpm skills:update` (or
|
|
39
|
+
`npx @agent-native/core@latest skills update scaffold --project`) after updating
|
|
40
|
+
`@agent-native/core` to refresh framework-provided shared skills and repair
|
|
41
|
+
Claude compatibility links.
|
|
38
42
|
|
|
39
43
|
Runtime-editable global resources live in Dispatch, not in `packages/shared`.
|
|
40
44
|
Use Dispatch **Resources** for company context and guardrails that admins should
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
"build": "pnpm -r build",
|
|
8
8
|
"typecheck": "pnpm -r typecheck",
|
|
9
9
|
"repair:workspace-org": "tsx scripts/repair-workspace-org.ts",
|
|
10
|
+
"skills:update": "agent-native skills update scaffold --project",
|
|
10
11
|
"fmt:check": "prettier --check .",
|
|
11
12
|
"lint": "pnpm fmt:check"
|
|
12
13
|
},
|
|
@@ -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 =
|
|
@@ -1067,9 +1157,17 @@ function BarRenderer({
|
|
|
1067
1157
|
formatXLabel(String(value ?? ""), panel);
|
|
1068
1158
|
const seriesNameFormatter = (name: string) =>
|
|
1069
1159
|
formatSeriesLabelForPanel(panel, name);
|
|
1160
|
+
const { hiddenKeys, toggleSeries } = useSeriesVisibility(yKeys);
|
|
1070
1161
|
|
|
1071
1162
|
return (
|
|
1072
|
-
<ChartFrame
|
|
1163
|
+
<ChartFrame
|
|
1164
|
+
panel={panel}
|
|
1165
|
+
legendKeys={yKeys}
|
|
1166
|
+
colors={colors}
|
|
1167
|
+
hiddenKeys={hiddenKeys}
|
|
1168
|
+
onToggleLegendKey={toggleSeries}
|
|
1169
|
+
showCustomLegend
|
|
1170
|
+
>
|
|
1073
1171
|
<ResponsiveContainer width="100%" height="100%">
|
|
1074
1172
|
<BarChart data={rows}>
|
|
1075
1173
|
<XAxis
|
|
@@ -1104,10 +1202,6 @@ function BarRenderer({
|
|
|
1104
1202
|
}
|
|
1105
1203
|
itemSorter={(item) => -(Number(item.value) || 0)}
|
|
1106
1204
|
/>
|
|
1107
|
-
{!usesPrometheusPresentation(panel) &&
|
|
1108
|
-
shouldShowLegend(panel, yKeys.length) && (
|
|
1109
|
-
<Legend {...CHART_LEGEND_PROPS} />
|
|
1110
|
-
)}
|
|
1111
1205
|
{yKeys.map((key, i) => (
|
|
1112
1206
|
<Bar
|
|
1113
1207
|
key={key}
|
|
@@ -1118,6 +1212,7 @@ function BarRenderer({
|
|
|
1118
1212
|
stacked && i < yKeys.length - 1 ? [0, 0, 0, 0] : [4, 4, 0, 0]
|
|
1119
1213
|
}
|
|
1120
1214
|
stackId={stacked ? "stack" : undefined}
|
|
1215
|
+
hide={hiddenKeys.has(key)}
|
|
1121
1216
|
/>
|
|
1122
1217
|
))}
|
|
1123
1218
|
</BarChart>
|
|
@@ -1149,10 +1244,18 @@ function TimeSeriesRenderer({
|
|
|
1149
1244
|
formatXLabel(String(value ?? ""), panel);
|
|
1150
1245
|
const seriesNameFormatter = (name: string) =>
|
|
1151
1246
|
formatSeriesLabelForPanel(panel, name);
|
|
1247
|
+
const { hiddenKeys, visibleKeys, toggleSeries } = useSeriesVisibility(yKeys);
|
|
1152
1248
|
|
|
1153
1249
|
if (chartType === "line") {
|
|
1154
1250
|
return (
|
|
1155
|
-
<ChartFrame
|
|
1251
|
+
<ChartFrame
|
|
1252
|
+
panel={panel}
|
|
1253
|
+
legendKeys={yKeys}
|
|
1254
|
+
colors={colors}
|
|
1255
|
+
hiddenKeys={hiddenKeys}
|
|
1256
|
+
onToggleLegendKey={toggleSeries}
|
|
1257
|
+
showCustomLegend
|
|
1258
|
+
>
|
|
1156
1259
|
<ResponsiveContainer width="100%" height="100%">
|
|
1157
1260
|
<LineChart data={rows}>
|
|
1158
1261
|
<XAxis
|
|
@@ -1187,10 +1290,6 @@ function TimeSeriesRenderer({
|
|
|
1187
1290
|
}
|
|
1188
1291
|
itemSorter={(item) => -(Number(item.value) || 0)}
|
|
1189
1292
|
/>
|
|
1190
|
-
{!usesPrometheusPresentation(panel) &&
|
|
1191
|
-
shouldShowLegend(panel, yKeys.length) && (
|
|
1192
|
-
<Legend {...CHART_LEGEND_PROPS} />
|
|
1193
|
-
)}
|
|
1194
1293
|
{yKeys.map((key, i) => (
|
|
1195
1294
|
<Line
|
|
1196
1295
|
key={key}
|
|
@@ -1200,6 +1299,7 @@ function TimeSeriesRenderer({
|
|
|
1200
1299
|
stroke={colors[i % colors.length]}
|
|
1201
1300
|
strokeWidth={2}
|
|
1202
1301
|
dot={false}
|
|
1302
|
+
hide={hiddenKeys.has(key)}
|
|
1203
1303
|
/>
|
|
1204
1304
|
))}
|
|
1205
1305
|
</LineChart>
|
|
@@ -1211,10 +1311,17 @@ function TimeSeriesRenderer({
|
|
|
1211
1311
|
// With multiple series, filled areas stack and obscure lines behind them,
|
|
1212
1312
|
// so only draw the gradient fill when there's a single series — unless
|
|
1213
1313
|
// the caller asked for an explicit stacked area.
|
|
1214
|
-
const showFill =
|
|
1314
|
+
const showFill = visibleKeys.length === 1 || stacked;
|
|
1215
1315
|
|
|
1216
1316
|
return (
|
|
1217
|
-
<ChartFrame
|
|
1317
|
+
<ChartFrame
|
|
1318
|
+
panel={panel}
|
|
1319
|
+
legendKeys={yKeys}
|
|
1320
|
+
colors={colors}
|
|
1321
|
+
hiddenKeys={hiddenKeys}
|
|
1322
|
+
onToggleLegendKey={toggleSeries}
|
|
1323
|
+
showCustomLegend
|
|
1324
|
+
>
|
|
1218
1325
|
<ResponsiveContainer width="100%" height="100%">
|
|
1219
1326
|
<AreaChart data={rows}>
|
|
1220
1327
|
{showFill && (
|
|
@@ -1274,10 +1381,6 @@ function TimeSeriesRenderer({
|
|
|
1274
1381
|
}
|
|
1275
1382
|
itemSorter={(item) => -(Number(item.value) || 0)}
|
|
1276
1383
|
/>
|
|
1277
|
-
{!usesPrometheusPresentation(panel) &&
|
|
1278
|
-
shouldShowLegend(panel, yKeys.length) && (
|
|
1279
|
-
<Legend {...CHART_LEGEND_PROPS} />
|
|
1280
|
-
)}
|
|
1281
1384
|
{yKeys.map((key, i) => (
|
|
1282
1385
|
<Area
|
|
1283
1386
|
key={key}
|
|
@@ -1289,6 +1392,7 @@ function TimeSeriesRenderer({
|
|
|
1289
1392
|
fillOpacity={showFill ? 1 : 0}
|
|
1290
1393
|
fill={showFill ? `url(#sql-gradient-${key})` : "none"}
|
|
1291
1394
|
stackId={stacked ? "stack" : undefined}
|
|
1395
|
+
hide={hiddenKeys.has(key)}
|
|
1292
1396
|
/>
|
|
1293
1397
|
))}
|
|
1294
1398
|
</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
|
}
|