This is an automated email from the ASF dual-hosted git repository.
zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/2.0.0 by this push:
new 1eeed55335 Wire operation drilldown dashboard action
1eeed55335 is described below
commit 1eeed55335b1de5328e80b2edbbad0047e5d16a1
Author: Logic <[email protected]>
AuthorDate: Mon Jun 8 10:10:07 2026 +0800
Wire operation drilldown dashboard action
---
.../app/dashboard/dashboard-draft-workspace.tsx | 49 +++++++++++
web-next/app/dashboard/page.test.ts | 10 +++
web-next/lib/i18n-runtime-messages.ts | 2 +
.../dashboard-source-edit-browser-smoke.spec.ts | 95 +++++++++++++++++++++-
.../dashboard-source-edit-browser-smoke.test.ts | 29 +++++++
5 files changed, 184 insertions(+), 1 deletion(-)
diff --git a/web-next/app/dashboard/dashboard-draft-workspace.tsx
b/web-next/app/dashboard/dashboard-draft-workspace.tsx
index 7ece164bbb..a849f3488a 100644
--- a/web-next/app/dashboard/dashboard-draft-workspace.tsx
+++ b/web-next/app/dashboard/dashboard-draft-workspace.tsx
@@ -31,6 +31,7 @@ import {
buildSignalDashboardRuntimeSyncCrosshair,
buildSignalDashboardRuntimeSyncTooltip,
buildSignalDashboardVariableOptions,
+ buildSignalOperationDrilldownDashboard,
buildSignalServiceOverviewDashboard,
createSignalDashboardPanelDraftsFromFilterSelection,
createSignalDashboardPanelDraftFromRuntimeBreakout,
@@ -776,6 +777,7 @@ export default function DashboardDraftWorkspace({
const [savingPreviewLayout, setSavingPreviewLayout] = useState(false);
const [savingVariables, setSavingVariables] = useState(false);
const [savingServiceOverview, setSavingServiceOverview] = useState(false);
+ const [savingOperationDrilldown, setSavingOperationDrilldown] =
useState(false);
const [variableNameDraft, setVariableNameDraft] = useState('service.name');
const [variableTypeDraft, setVariableTypeDraft] =
useState<SignalDashboardVariableType>('textbox');
const [variableValueDraft, setVariableValueDraft] = useState('');
@@ -811,6 +813,14 @@ export default function DashboardDraftWorkspace({
template: firstParamValue(initialContext.template)?.trim() || undefined
};
}, [initialContext]);
+ const operationDrilldownContext = useMemo(() => {
+ const operationName =
firstParamValue(initialContext.operationName)?.trim() || '';
+ if (!serviceOverviewContext || !operationName) return null;
+ return {
+ ...serviceOverviewContext,
+ operationName
+ };
+ }, [initialContext, serviceOverviewContext]);
const defaultDashboardTitle = t('dashboard.composition.default-title');
const defaultDashboardDescription =
t('dashboard.composition.default-description');
const runtimeTableLabels = useMemo(() => ({
@@ -1151,6 +1161,29 @@ export default function DashboardDraftWorkspace({
}
};
+ const saveOperationDrilldownDashboard = async () => {
+ if (!operationDrilldownContext) return;
+ setSavingOperationDrilldown(true);
+ setCompositionState('saving');
+ try {
+ const dashboard = buildSignalOperationDrilldownDashboard({
+ ...operationDrilldownContext,
+ ...dashboardTimeRange
+ });
+ const saved = await saveSignalDashboard(dashboard);
+ setDashboardKeyDraft(saved.dashboardKey);
+ setDashboardTitleDraft(saved.title);
+ setDashboardDescriptionDraft(saved.description ||
defaultDashboardDescription);
+ replaceDashboardDeepLink(saved.dashboardKey);
+ setDashboards(current => [saved, ...current.filter(item =>
item.dashboardKey !== saved.dashboardKey)]);
+ setCompositionState('saved');
+ } catch {
+ setCompositionState('error');
+ } finally {
+ setSavingOperationDrilldown(false);
+ }
+ };
+
const replaceSelectedDashboardVariables = (variables:
SignalDashboardVariable[]) => {
if (!selectedDashboard) return;
setDashboards(current => current.map(dashboard =>
@@ -1966,6 +1999,8 @@ export default function DashboardDraftWorkspace({
data-dashboard-composition-preview-key={selectedDashboard?.dashboardKey ||
normalizeSignalDashboardKey(dashboardKeyDraft || dashboardTitleDraft)}
data-dashboard-service-overview-context={serviceOverviewContext ?
'ready' : 'missing'}
data-dashboard-service-overview-service={serviceOverviewContext?.serviceName ||
''}
+
data-dashboard-operation-drilldown-context={operationDrilldownContext ? 'ready'
: 'missing'}
+
data-dashboard-operation-drilldown-operation={operationDrilldownContext?.operationName
|| ''}
data-dashboard-composition-preview-panels={previewPanels.length}
data-dashboard-composition-time-range-mode={dashboardTimeRangeMode}
data-dashboard-composition-time-range-start={dashboardTimeRange.start}
@@ -2036,6 +2071,20 @@ export default function DashboardDraftWorkspace({
<LayoutDashboard size={13} />
{t('dashboard.composition.action.save-service-overview')}
</HzButton>
+ <HzButton
+ type="button"
+ size="sm"
+ intent="secondary"
+ disabled={!operationDrilldownContext ||
savingOperationDrilldown}
+ onClick={() => void saveOperationDrilldownDashboard()}
+ data-dashboard-operation-drilldown-action="save"
+
data-dashboard-operation-drilldown-action-state={operationDrilldownContext ?
'ready' : 'missing'}
+
data-dashboard-operation-drilldown-action-service={operationDrilldownContext?.serviceName
|| ''}
+
data-dashboard-operation-drilldown-action-operation={operationDrilldownContext?.operationName
|| ''}
+ >
+ <ExternalLink size={13} />
+ {t('dashboard.composition.action.save-operation-drilldown')}
+ </HzButton>
</div>
<div
className="grid gap-2 border-b border-[#252b35] bg-[#080b10]
px-4 py-3"
diff --git a/web-next/app/dashboard/page.test.ts
b/web-next/app/dashboard/page.test.ts
index d3fb0bc637..eeb1914fe4 100644
--- a/web-next/app/dashboard/page.test.ts
+++ b/web-next/app/dashboard/page.test.ts
@@ -126,16 +126,26 @@ describe('dashboard panel draft workspace route', () => {
expect(workspaceSource).toContain('loadSignalDashboards');
expect(workspaceSource).toContain('saveSignalDashboard');
expect(workspaceSource).toContain('deleteSignalDashboard');
+
expect(workspaceSource).toContain('buildSignalOperationDrilldownDashboard');
expect(workspaceSource).toContain('buildSignalServiceOverviewDashboard');
expect(workspaceSource).toContain('const serviceOverviewContext =
useMemo(');
expect(workspaceSource).toContain('const serviceName =
firstParamValue(initialContext.serviceName)?.trim()');
+ expect(workspaceSource).toContain('const operationDrilldownContext =
useMemo(');
+ expect(workspaceSource).toContain('const operationName =
firstParamValue(initialContext.operationName)?.trim()');
expect(workspaceSource).toContain('const saveServiceOverviewDashboard =
async () =>');
expect(workspaceSource).toContain('buildSignalServiceOverviewDashboard({');
+ expect(workspaceSource).toContain('const saveOperationDrilldownDashboard =
async () =>');
+
expect(workspaceSource).toContain('buildSignalOperationDrilldownDashboard({');
expect(workspaceSource).toContain('data-dashboard-service-overview-context');
expect(workspaceSource).toContain('data-dashboard-service-overview-service');
expect(workspaceSource).toContain('data-dashboard-service-overview-action="save"');
expect(workspaceSource).toContain('data-dashboard-service-overview-action-state');
expect(workspaceSource).toContain('dashboard.composition.action.save-service-overview');
+
expect(workspaceSource).toContain('data-dashboard-operation-drilldown-context');
+
expect(workspaceSource).toContain('data-dashboard-operation-drilldown-operation');
+
expect(workspaceSource).toContain('data-dashboard-operation-drilldown-action="save"');
+
expect(workspaceSource).toContain('data-dashboard-operation-drilldown-action-state');
+
expect(workspaceSource).toContain('dashboard.composition.action.save-operation-drilldown');
expect(workspaceSource).toContain('normalizeSignalDashboardKey');
expect(workspaceSource).toContain('buildDashboardVariableDeepLinkHref');
expect(workspaceSource).toContain('buildDashboardTimeRangeDeepLinkHref');
diff --git a/web-next/lib/i18n-runtime-messages.ts
b/web-next/lib/i18n-runtime-messages.ts
index b78346557c..8274ab2d1e 100644
--- a/web-next/lib/i18n-runtime-messages.ts
+++ b/web-next/lib/i18n-runtime-messages.ts
@@ -1913,6 +1913,7 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'dashboard.composition.action.save-layout': 'Save layout',
'dashboard.composition.action.save-preview-layout': 'Save preview layout',
'dashboard.composition.action.save-service-overview': 'Service overview',
+ 'dashboard.composition.action.save-operation-drilldown': 'Operation
drilldown',
'dashboard.composition.action.save-variables': 'Save variables',
'dashboard.composition.action.add-variable': 'Add variable',
'dashboard.composition.action.delete-variable': 'Delete',
@@ -6374,6 +6375,7 @@ export const SUPPLEMENTAL_MESSAGES:
Partial<Record<LocaleCode, Messages>> = {
'dashboard.composition.action.save-layout': '保存布局',
'dashboard.composition.action.save-preview-layout': '保存预览布局',
'dashboard.composition.action.save-service-overview': '服务概览',
+ 'dashboard.composition.action.save-operation-drilldown': '操作钻取',
'dashboard.composition.action.save-variables': '保存变量',
'dashboard.composition.action.add-variable': '添加变量',
'dashboard.composition.action.delete-variable': '删除',
diff --git a/web-next/scripts/dashboard-source-edit-browser-smoke.spec.ts
b/web-next/scripts/dashboard-source-edit-browser-smoke.spec.ts
index b3988366b1..9364b481e9 100644
--- a/web-next/scripts/dashboard-source-edit-browser-smoke.spec.ts
+++ b/web-next/scripts/dashboard-source-edit-browser-smoke.spec.ts
@@ -354,7 +354,8 @@ function metricsPayload(query: string) {
schema: {
labels: {
'service.name': 'checkout',
- 'deployment.environment.name': 'prod'
+ 'deployment.environment.name': 'prod',
+ operation: 'POST /checkout'
}
},
data: [
@@ -1461,12 +1462,17 @@ test.describe('dashboard source edit browser smoke', ()
=> {
await
expect(runtimeTooltipRow).toHaveAttribute('data-dashboard-composition-runtime-sync-tooltip-row-handoff',
/serviceName=checkout/);
await
expect(runtimeTooltipRow).toHaveAttribute('data-dashboard-composition-runtime-sync-tooltip-row-related-handoff',
/\/trace\/manage/);
await
expect(runtimeTooltipRow).toHaveAttribute('data-dashboard-composition-runtime-sync-tooltip-row-related-handoff',
/serviceName=checkout/);
+ await
expect(runtimeTooltipRow).toHaveAttribute('data-dashboard-composition-runtime-sync-tooltip-row-related-handoff',
/operationName=POST\+%2Fcheckout/);
await
expect(runtimeTooltipRow).toHaveAttribute('data-dashboard-composition-runtime-sync-tooltip-row-related-handoff',
/spanScope=all/);
await
expect(runtimeTooltipRow).toHaveAttribute('data-dashboard-composition-runtime-sync-tooltip-row-breakout-attributes',
/[1-9]/);
await
expect(runtimeTooltipRow.locator('[data-dashboard-composition-runtime-sync-tooltip-row-action="open-related"]')).toHaveAttribute(
'data-dashboard-composition-runtime-sync-tooltip-row-action-href',
/\/trace\/manage/
);
+ await
expect(runtimeTooltipRow.locator('[data-dashboard-composition-runtime-sync-tooltip-row-action="open-related"]')).toHaveAttribute(
+ 'data-dashboard-composition-runtime-sync-tooltip-row-action-href',
+ /operationName=POST\+%2Fcheckout/
+ );
await
expect(runtimeTooltipRow.locator('[data-dashboard-composition-runtime-sync-tooltip-row-action="breakout-panel-draft"]').first()).toHaveAttribute(
'data-dashboard-composition-runtime-sync-tooltip-row-action-attribute',
'service.name'
@@ -1714,6 +1720,93 @@ test.describe('dashboard source edit browser smoke', ()
=> {
]));
});
+ test('saves an operation drilldown dashboard from URL operation context',
async ({ page }) => {
+ test.setTimeout(BROWSER_SMOKE_TIMEOUT);
+ const smokeState = await installDashboardServiceOverviewMocks(page);
+
+ await
page.goto(routeUrl('/dashboard?serviceName=checkout&serviceNamespace=payments&environment=prod&operationName=POST%20%2Fcheckout&entityId=4200&entityType=service&entityName=Checkout%20API&source=otlp&collector=collector-a&template=spring-boot&timeRange=last-1h'),
{
+ timeout: BROWSER_SMOKE_TIMEOUT,
+ waitUntil: 'domcontentloaded'
+ });
+
+ const operationDrilldownAction =
page.locator('[data-dashboard-operation-drilldown-action="save"]').first();
+ await
expect(page.locator('[data-dashboard-operation-drilldown-context="ready"]')).toHaveAttribute(
+ 'data-dashboard-operation-drilldown-operation',
+ 'POST /checkout',
+ { timeout: WORKBENCH_READY_TIMEOUT }
+ );
+ await
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-state',
'ready');
+ await
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-service',
'checkout');
+ await
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-operation',
'POST /checkout');
+ await operationDrilldownAction.click();
+
+ await expect.poll(() => smokeState.savedDashboards, {
+ timeout: WORKBENCH_READY_TIMEOUT
+ }).toHaveLength(1);
+ const [savedDashboard] = smokeState.savedDashboards;
+ const savedWidgets = JSON.parse(String(savedDashboard.widgets || '[]')) as
Array<Record<string, unknown>>;
+ const savedVariables = JSON.parse(String(savedDashboard.variables ||
'[]')) as Array<Record<string, unknown>>;
+ expect(savedDashboard).toEqual(expect.objectContaining({
+ dashboardKey: 'service-checkout-operation-post-checkout-drilldown',
+ title: 'Checkout API POST /checkout operation drilldown',
+ tags: 'service,operation,apm,metrics,logs,traces'
+ }));
+ expect(savedWidgets).toHaveLength(8);
+ expect(savedWidgets.map(widget => widget.title)).toEqual([
+ 'Operation drilldown latency p95: operation.name=POST /checkout',
+ 'Operation drilldown request rate: operation.name=POST /checkout',
+ 'Operation drilldown error rate: operation.name=POST /checkout',
+ 'Operation drilldown logs: operation.name=POST /checkout',
+ 'Operation drilldown log errors: operation.name=POST /checkout',
+ 'Operation drilldown traces: operation.name=POST /checkout',
+ 'Operation drilldown trace errors: operation.name=POST /checkout',
+ 'Operation drilldown exceptions: operation.name=POST /checkout'
+ ]);
+ expect(savedVariables).toEqual(expect.arrayContaining([
+ expect.objectContaining({ name: 'operation.name', type: 'query', value:
'POST /checkout' }),
+ expect.objectContaining({ name: 'service.name', type: 'query', value:
'checkout' }),
+ expect.objectContaining({ name: 'hertzbeat.entity_id', type: 'dynamic',
value: '4200' }),
+ expect.objectContaining({ name: 'hertzbeat.entity_type', type:
'dynamic', value: 'service' })
+ ]));
+
+ await
expect(page).toHaveURL(/dashboard=service-checkout-operation-post-checkout-drilldown/,
{
+ timeout: WORKBENCH_READY_TIMEOUT
+ });
+ await
expect(page.locator('[data-dashboard-composition-preview-panel]')).toHaveCount(8,
{
+ timeout: WORKBENCH_READY_TIMEOUT
+ });
+
+ const latencyPanel =
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText:
'Operation drilldown latency p95' }).first();
+ await
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-preview-signal',
'metrics');
+ await
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/operation%3D%22POST\+%2Fcheckout%22/);
+ await
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/entityType=service/);
+
+ const logsPanel =
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText:
'Operation drilldown logs' }).first();
+ await
expect(logsPanel).toHaveAttribute('data-dashboard-composition-preview-signal',
'logs');
+ await
expect(logsPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/\/logs\/list\?/);
+ await
expect(logsPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/attributeFilter=http.route%3APOST\+%2Fcheckout/);
+
+ const tracesPanel =
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText:
'Operation drilldown traces' }).first();
+ await
expect(tracesPanel).toHaveAttribute('data-dashboard-composition-preview-signal',
'traces');
+ await
expect(tracesPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/\/traces\/list\?/);
+ await
expect(tracesPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/operationName=POST\+%2Fcheckout/);
+
+ const exceptionsPanel =
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText:
'Operation drilldown exceptions' }).first();
+ await
expect(exceptionsPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/\/traces\/stats\/group-by\?/);
+ await
expect(exceptionsPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/groupBy=exception.type/);
+
+ await expect.poll(() => smokeState.executedUrls.some(url =>
+ url.includes('/api/ingestion/otlp/metrics/console') &&
url.includes('operation%3D%22POST+%2Fcheckout%22')
+ ), {
+ timeout: WORKBENCH_READY_TIMEOUT
+ }).toBe(true);
+ expect(smokeState.executedUrls).toEqual(expect.arrayContaining([
+ expect.stringContaining('/api/logs/list'),
+ expect.stringContaining('/api/traces/list'),
+ expect.stringContaining('/api/traces/stats/group-by')
+ ]));
+ });
+
for (const signalCase of SIGNAL_CASES) {
test(`edits a ${signalCase.signal} dashboard panel from the source
workbench and returns to the updated dashboard preview`, async ({ page }) => {
test.setTimeout(BROWSER_SMOKE_TIMEOUT);
diff --git a/web-next/scripts/dashboard-source-edit-browser-smoke.test.ts
b/web-next/scripts/dashboard-source-edit-browser-smoke.test.ts
index 11ab5aaadc..23e98ffd48 100644
--- a/web-next/scripts/dashboard-source-edit-browser-smoke.test.ts
+++ b/web-next/scripts/dashboard-source-edit-browser-smoke.test.ts
@@ -99,6 +99,8 @@ describe('dashboard source edit browser smoke coverage', ()
=> {
expect(source).toContain('data-dashboard-composition-runtime-sync-tooltip-row-source="metrics-point"');
expect(source).toContain('data-dashboard-composition-runtime-sync-tooltip-row-related-handoff');
expect(source).toContain('data-dashboard-composition-runtime-sync-tooltip-row-action="open-related"');
+ expect(source).toContain("operation: 'POST /checkout'");
+ expect(source).toContain('operationName=POST\\+%2Fcheckout');
expect(source).toContain('data-dashboard-composition-runtime-sync-tooltip-row-breakout-attributes');
expect(source).toContain('data-dashboard-composition-runtime-sync-tooltip-row-action="breakout-panel-draft"');
expect(source).toContain("source: 'signal-dashboard-runtime-breakout'");
@@ -129,8 +131,35 @@ describe('dashboard source edit browser smoke coverage',
() => {
expect(source).toContain('Service overview log errors:
service.name=checkout');
expect(source).toContain('Service overview exceptions:
service.name=checkout');
expect(source).toContain('Service overview firing alerts:
service.name=checkout');
+ expect(source).toContain('saves an operation drilldown dashboard from URL
operation context');
+
expect(source).toContain('data-dashboard-operation-drilldown-action="save"');
+
expect(source).toContain('data-dashboard-operation-drilldown-context="ready"');
+
expect(source).toContain('service-checkout-operation-post-checkout-drilldown');
+ expect(source).toContain('Checkout API POST /checkout operation
drilldown');
+ expect(source).toContain('Operation drilldown latency p95:
operation.name=POST /checkout');
+ expect(source).toContain('Operation drilldown traces: operation.name=POST
/checkout');
+
expect(source).toContain('attributeFilter=http.route%3APOST\\+%2Fcheckout');
expect(source).toContain('hertzbeat.entity_type');
expect(source).toContain('entityType=service');
+ expect(source).toContain('saves an operation drilldown dashboard from URL
operation context');
+
expect(source).toContain('data-dashboard-operation-drilldown-action="save"');
+
expect(source).toContain('data-dashboard-operation-drilldown-context="ready"');
+ expect(source).toContain('data-dashboard-operation-drilldown-operation');
+
expect(source).toContain('service-checkout-operation-post-checkout-drilldown');
+ expect(source).toContain('Checkout API POST /checkout operation
drilldown');
+ expect(source).toContain('Operation drilldown latency p95:
operation.name=POST /checkout');
+ expect(source).toContain('Operation drilldown request rate:
operation.name=POST /checkout');
+ expect(source).toContain('Operation drilldown error rate:
operation.name=POST /checkout');
+ expect(source).toContain('Operation drilldown logs: operation.name=POST
/checkout');
+ expect(source).toContain('Operation drilldown log errors:
operation.name=POST /checkout');
+ expect(source).toContain('Operation drilldown traces: operation.name=POST
/checkout');
+ expect(source).toContain('Operation drilldown trace errors:
operation.name=POST /checkout');
+ expect(source).toContain('Operation drilldown exceptions:
operation.name=POST /checkout');
+ expect(source).toContain("name: 'operation.name'");
+ expect(source).toContain("tags:
'service,operation,apm,metrics,logs,traces'");
+ expect(source).toContain('operation%3D%22POST\\+%2Fcheckout%22');
+
expect(source).toContain('attributeFilter=http.route%3APOST\\+%2Fcheckout');
+ expect(source).toContain('operationName=POST\\+%2Fcheckout');
expect(source).toContain('data-dashboard-composition-runtime-sync-tooltip-row-action="add-panel-draft"');
expect(source).toContain('spanScope=all');
expect(source).toContain('runtimeEvidenceDrafts');
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]