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]

Reply via email to