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
commit c4d242fa6af1dd28a8d9527c82fef1574d55d4c0 Author: Logic <[email protected]> AuthorDate: Fri May 29 02:35:16 2026 +0800 feat(web-next): add M10 frontend convergence gate --- web-app/src/assets/i18n/en-US.json | 54 ++++++++- web-app/src/assets/i18n/zh-CN.json | 56 ++++++++- web-next/lib/types.ts | 30 +++++ .../scripts/frontend-convergence-contract.test.ts | 133 +++++++++++++++++++++ 4 files changed, 266 insertions(+), 7 deletions(-) diff --git a/web-app/src/assets/i18n/en-US.json b/web-app/src/assets/i18n/en-US.json index 7a0eb3fff4..42add5ad74 100644 --- a/web-app/src/assets/i18n/en-US.json +++ b/web-app/src/assets/i18n/en-US.json @@ -1427,6 +1427,43 @@ "common.week.5": "Friday", "common.week.6": "Saturday", "common.week.7": "Sunday", + "time.range.preset": "Time range", + "time.range.relative": "Relative", + "time.range.start": "Start", + "time.range.end": "End", + "time.range.from": "From", + "time.range.to": "To", + "time.range.absolute-title": "Absolute time range", + "time.range.quick-ranges": "Quick ranges", + "time.range.recent-ranges": "Recent ranges", + "time.range.custom-range": "Custom range", + "time.range.custom-name": "Range name", + "time.range.save-custom-range": "Save range", + "time.range.delete-custom-range": "Delete", + "time.range.validation-valid": "Valid time expression", + "time.range.validation-invalid": "Invalid time expression", + "time.range.year": "Year", + "time.range.month": "Month", + "time.range.date": "Date", + "time.range.unset": "Not set", + "time.range.hour": "Hour", + "time.range.minute": "Minute", + "time.range.second": "Second", + "time.range.previous-month": "Previous month", + "time.range.next-month": "Next month", + "time.range.refresh": "Refresh interval", + "time.range.manual-refresh": "Manual", + "time.range.live": "Live", + "time.range.live-on": "Pause live updates", + "time.range.live-off": "Resume live updates", + "time.range.timezone": "Timezone", + "time.range.local-timezone": "Local timezone", + "time.range.apply": "Apply", + "time.range.apply-aria": "Apply time range", + "time.range.refresh-action": "Refresh now", + "time.range.reset": "Reset", + "time.range.reset-aria": "Reset time range", + "time.range.relative-placeholder": "45m", "common.yes": "Yes", "dashboard.alerts.deal": "Alarms Dealing", "dashboard.alerts.deal-percent": "Dealing Rate", @@ -3022,7 +3059,7 @@ "monitors.loading": "Loading the monitor list from the existing monitor API.", "monitors.kicker": "Monitoring Workspace", "monitors.title": "Monitoring Center", - "monitors.subtitle": "Monitoring filters, result list, and detail rail now share the same workspace shell.", + "monitors.subtitle": "Monitor collector-backed resources, status, labels, and lifecycle actions.", "monitors.workspace": "Monitoring", "monitors.fact.total": "Total monitors", "monitors.fact.page-count": "Current page", @@ -3037,9 +3074,15 @@ "monitors.labels.placeholder": "key=value or keyword", "common.status": "Status", "monitors.status.all": "All statuses", - "monitors.status.up": "Running", - "monitors.status.down": "Abnormal", + "monitors.status.up": "Up", + "monitors.status.down": "Down", "monitors.status.paused": "Paused", + "monitors.type.all": "All types", + "monitors.app-picker.catalog-title": "Type catalog", + "monitors.app-picker.close": "Close", + "monitors.app-picker.empty": "No matching types", + "monitors.app-picker.item-count": "items", + "monitors.app-picker.search-placeholder": "Search visible names", "monitors.section.list.title": "Monitor List", "monitors.section.list.copy": "Results are consolidated into a flat workspace list.", "monitors.empty-results.title": "No Matching Monitors", @@ -3222,6 +3265,8 @@ "common.none": "None", "common.clear-selection": "Clear selection", "common.select-all": "Select page", + "common.select-page": "Select page", + "common.select-all-results": "Select all", "common.import": "Import monitors", "common.selected": "Selected", "common.export-selected-json": "Export selected JSON", @@ -3233,12 +3278,15 @@ "common.page-size": "Page size", "common.previous": "Previous", "common.next": "Next", + "common.previous-page": "Previous page", + "common.next-page": "Next page", "common.pause": "Pause", "common.delete-success": "Deleted successfully", "common.delete-failed": "Delete failed", "monitor.copy.action": "Copy monitor", "monitor.detail.delete-grafana": "Delete Grafana", "monitors.controls.enable-selected": "Enable selected", + "monitors.controls.more-actions": "More monitor operations", "monitors.controls.pause-selected": "Pause selected", "dashboard.alerts.center-title": "Alert Center", "dashboard.alerts.notifications-title": "Alert Notifications", diff --git a/web-app/src/assets/i18n/zh-CN.json b/web-app/src/assets/i18n/zh-CN.json index 657196dbd5..bda05956b7 100644 --- a/web-app/src/assets/i18n/zh-CN.json +++ b/web-app/src/assets/i18n/zh-CN.json @@ -1451,6 +1451,43 @@ "common.week.5": "星期五", "common.week.6": "星期六", "common.week.7": "星期日", + "time.range.preset": "时间范围", + "time.range.relative": "相对时间", + "time.range.start": "开始", + "time.range.end": "结束", + "time.range.from": "开始", + "time.range.to": "结束", + "time.range.absolute-title": "绝对时间范围", + "time.range.quick-ranges": "快捷时间范围", + "time.range.recent-ranges": "最近使用", + "time.range.custom-range": "自定义范围", + "time.range.custom-name": "范围名称", + "time.range.save-custom-range": "保存范围", + "time.range.delete-custom-range": "删除", + "time.range.validation-valid": "时间表达式有效", + "time.range.validation-invalid": "时间表达式无效", + "time.range.year": "年", + "time.range.month": "月", + "time.range.date": "日期", + "time.range.unset": "未设置", + "time.range.hour": "时", + "time.range.minute": "分", + "time.range.second": "秒", + "time.range.previous-month": "上个月", + "time.range.next-month": "下个月", + "time.range.refresh": "刷新间隔", + "time.range.manual-refresh": "手动", + "time.range.live": "实时", + "time.range.live-on": "暂停实时刷新", + "time.range.live-off": "恢复实时刷新", + "time.range.timezone": "时区", + "time.range.local-timezone": "本地时区", + "time.range.apply": "应用", + "time.range.apply-aria": "应用时间范围", + "time.range.refresh-action": "立即刷新", + "time.range.reset": "重置", + "time.range.reset-aria": "重置时间范围", + "time.range.relative-placeholder": "45m", "common.yes": "是", "dashboard.alerts.deal": "告警处理", "dashboard.alerts.deal-percent": "告警处理率", @@ -3046,7 +3083,7 @@ "monitors.loading": "正在从现有监控 API 加载监控列表。", "monitors.kicker": "监控工作区", "monitors.title": "监控中心", - "monitors.subtitle": "监控筛选、结果列表和详情侧栏现在共用同一个工作区外壳。", + "monitors.subtitle": "查看传统采集资源、运行状态、标签和基础操作。", "monitors.workspace": "监控", "monitors.fact.total": "监控总数", "monitors.fact.page-count": "当前页", @@ -3061,9 +3098,15 @@ "monitors.labels.placeholder": "key=value 或关键词", "common.status": "状态", "monitors.status.all": "全部状态", - "monitors.status.up": "运行中", - "monitors.status.down": "异常", - "monitors.status.paused": "已暂停", + "monitors.status.up": "正常", + "monitors.status.down": "宕机", + "monitors.status.paused": "暂停", + "monitors.type.all": "全部类型", + "monitors.app-picker.catalog-title": "类型目录", + "monitors.app-picker.close": "关闭", + "monitors.app-picker.empty": "没有匹配项", + "monitors.app-picker.item-count": "项", + "monitors.app-picker.search-placeholder": "搜索可见名称", "monitors.section.list.title": "监控列表", "monitors.section.list.copy": "结果已整合为一个扁平的工作区列表。", "monitors.empty-results.title": "没有匹配的监控", @@ -3246,6 +3289,8 @@ "common.none": "无", "common.clear-selection": "清除选择", "common.select-all": "选择当前页", + "common.select-page": "选择当前页", + "common.select-all-results": "选择全部", "common.import": "导入监控", "common.selected": "已选择", "common.export-selected-json": "导出所选 JSON", @@ -3257,12 +3302,15 @@ "common.page-size": "每页条数", "common.previous": "上一条", "common.next": "下一条", + "common.previous-page": "上一页", + "common.next-page": "下一页", "common.pause": "暂停", "common.delete-success": "删除成功", "common.delete-failed": "删除失败", "monitor.copy.action": "复制监控", "monitor.detail.delete-grafana": "删除 Grafana", "monitors.controls.enable-selected": "启用选中项", + "monitors.controls.more-actions": "更多监控操作", "monitors.controls.pause-selected": "暂停选中项", "dashboard.alerts.center-title": "告警中心", "dashboard.alerts.notifications-title": "告警通知", diff --git a/web-next/lib/types.ts b/web-next/lib/types.ts index aad9d0f81a..cfbb6e2c3f 100644 --- a/web-next/lib/types.ts +++ b/web-next/lib/types.ts @@ -138,6 +138,9 @@ export interface Monitor { description?: string; labels?: Record<string, string>; annotations?: Record<string, string>; + _displayStatus?: 'ACTIVE' | 'DISAPPEARED'; + _disappearTime?: number; + _graceTimer?: unknown; gmtUpdate?: number; gmtCreate?: number; } @@ -305,10 +308,23 @@ export interface OtlpBoundEntity { monitorBindCount: number; } +export interface OtlpUnboundEntityCandidate { + suggestedName?: string; + suggestedType?: string; + namespace?: string; + environment?: string; + primaryIdentityKey?: string; + primaryIdentityValue?: string; + signals?: string[]; + canonicalIdentities?: Record<string, string>; + latestObservedAt?: number | null; +} + export interface OtlpEntityBindingSummary { canonicalIdentityKeys: string[]; recentServices: string[]; recentBoundEntities: OtlpBoundEntity[]; + recentUnboundCandidates?: OtlpUnboundEntityCandidate[]; recentIdentitySamples?: Array<{ key: string; value: string; @@ -493,12 +509,25 @@ export interface EntityDetailDto { monitorSummary?: EntityMonitorSummary; logSummary?: EntityLogSummary; traceSummary?: EntityTraceSummary; + unifiedEvidenceSummary?: EntityUnifiedEvidenceSummary; boundMonitors?: Monitor[]; activeAlerts?: unknown[]; nextActions?: EntityNextAction[]; noiseControlSummary?: EntityNoiseControlSummary; } +export interface EntityUnifiedEvidenceSummary { + activeSignalCount?: number; + metricsActive?: boolean; + logsActive?: boolean; + tracesActive?: boolean; + metricEvidenceCount?: number; + logEvidenceCount?: number; + traceEvidenceCount?: number; + latestObservedAt?: number | string | null; + activeSignals?: string[]; +} + export interface EntityEvidenceSummary { activeAlertCount?: number; collectorLastSeenAt?: number | string | null; @@ -787,6 +816,7 @@ export interface StatusPageIncident { components?: StatusPageComponent[]; contents?: Array<{ id?: number; + incidentId?: number; message?: string; state?: number; timestamp?: number | null; diff --git a/web-next/scripts/frontend-convergence-contract.test.ts b/web-next/scripts/frontend-convergence-contract.test.ts new file mode 100644 index 0000000000..755b4ce553 --- /dev/null +++ b/web-next/scripts/frontend-convergence-contract.test.ts @@ -0,0 +1,133 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; + +import { describe, expect, it } from 'vitest'; + +import { buildLegacyFrontendParityAudit, validateLegacyFrontendParityGate } from '../lib/legacy-frontend-parity'; +import { cutoverHoldRoutes, placeholderRoutes, routeMatrixPaths } from '../lib/nav'; + +const webNextRoot = resolve(__dirname, '..'); +const repoRoot = resolve(webNextRoot, '..'); + +function readWebNext(path: string): string { + return readFileSync(resolve(webNextRoot, path), 'utf8'); +} + +function readRepo(path: string): string { + return readFileSync(resolve(repoRoot, path), 'utf8'); +} + +function readJson(path: string): Record<string, unknown> { + return JSON.parse(readRepo(path)); +} + +describe('M10 frontend convergence contract', () => { + it('keeps the final route cutover free of primary hold or placeholder blockers', () => { + const audit = buildLegacyFrontendParityAudit(); + const gate = validateLegacyFrontendParityGate(audit); + + expect(cutoverHoldRoutes.map(route => route.href)).toEqual([]); + expect(placeholderRoutes.map(route => route.href)).toEqual([]); + expect(audit.releaseBlocked).toBe(false); + expect(gate).toEqual({ valid: true, issues: [] }); + }); + + it('keeps the release route matrix covering the M10 action and incident workbenches', () => { + const routeMatrixScript = readWebNext('scripts/route-matrix.mjs'); + + expect(routeMatrixPaths).toEqual(expect.arrayContaining(['/actions', '/incidents', '/log/manage', '/trace/manage'])); + expect(routeMatrixScript).toContain("'/actions'"); + expect(routeMatrixScript).toContain("'/incidents'"); + expect(routeMatrixScript).toContain("'/passport/login'"); + }); + + it('keeps core workbench loading and first-screen cache behavior centralized', () => { + const appFrameSource = readWebNext('components/shell/app-frame.tsx'); + const clientWorkbenchSource = readWebNext('components/workbench/client-workbench.tsx'); + const cacheSource = readWebNext('lib/workbench-load-cache.ts'); + + expect(cacheSource).toContain('settledTtlMs'); + expect(appFrameSource).toContain("import { consumeWorkbenchLoad } from '@/lib/workbench-load-cache'"); + expect(appFrameSource).toContain('APP_FRAME_HEADER_STATE_CACHE_TTL_MS = 60_000'); + expect(appFrameSource).toContain('app-frame:header-state:${locale}'); + expect(appFrameSource).not.toContain('app-frame:header-state:${locale}:${pathname}'); + expect(clientWorkbenchSource).toContain('cacheSettledTtlMs?: number'); + expect(clientWorkbenchSource).toContain('consumeWorkbenchLoad(cacheKey, load, { settledTtlMs: cacheSettledTtlMs })'); + }); + + it('keeps the migrated M10 workbenches on UI Lab backed shared components', () => { + const uiSource = readWebNext('packages/hertzbeat-ui/src/index.tsx'); + const uiLabSource = readWebNext('app/ui-lab/page.tsx'); + const checks = [ + { + route: readWebNext('app/actions/actions-page.tsx'), + sharedComponent: 'HzActionWorkbench', + uiLabMarker: 'data-hz-ui-lab-action-workbench="shared"', + routeMarker: 'data-actions-shared-workbench="hertzbeat-ui"', + forbidden: ['OpsSurfacePage', 'angular-dark-ops-placeholder'] + }, + { + route: readWebNext('app/incidents/incidents-page.tsx'), + sharedComponent: 'HzIncidentWorkbench', + uiLabMarker: 'data-hz-ui-lab-incident-workbench="shared"', + routeMarker: 'data-incidents-shared-workbench="hertzbeat-ui"', + forbidden: ['OpsSurfacePage', 'angular-dark-ops-placeholder'] + }, + { + route: readWebNext('app/explorer/explorer-page.tsx'), + sharedComponent: 'HzExplorerFrame', + uiLabMarker: '@hertzbeat/ui explorer', + routeMarker: 'data-explorer-shared-frame="hertzbeat-ui"', + forbidden: ['OpsSurfacePage', 'buildExplorerSurfaceConfig'] + }, + { + route: readWebNext('app/topology/topology-page.tsx'), + sharedComponent: 'HzTopologyWorkbenchFrame', + uiLabMarker: 'data-hz-ui-lab-topology-workbench-frame="shared"', + routeMarker: 'data-topology-workbench-frame-owner="hertzbeat-ui-workbench-frame"', + forbidden: ['data-topology-static-seed', 'checkout-api/orders-db/redis'] + } + ]; + + checks.forEach(check => { + expect(uiSource).toContain(`export function ${check.sharedComponent}`); + expect(uiLabSource).toContain(check.uiLabMarker); + expect(check.route).toContain(check.sharedComponent); + expect(check.route).toContain(check.routeMarker); + check.forbidden.forEach(forbidden => { + expect(check.route).not.toContain(forbidden); + }); + }); + }); + + it('keeps shared time-range and monitor operator copy available in both legacy catalogs', () => { + const enMessages = readJson('web-app/src/assets/i18n/en-US.json'); + const zhMessages = readJson('web-app/src/assets/i18n/zh-CN.json'); + + [ + 'time.range.preset', + 'time.range.apply', + 'time.range.refresh-action', + 'monitors.app-picker.catalog-title', + 'monitors.app-picker.search-placeholder', + 'monitors.controls.more-actions', + 'common.select-page', + 'common.select-all-results' + ].forEach(key => { + expect(enMessages[key], `${key} en-US`).toBeTruthy(); + expect(zhMessages[key], `${key} zh-CN`).toBeTruthy(); + }); + }); + + it('keeps frontend DTOs aligned with the final M10 entity, OTLP, monitor, and status evidence surfaces', () => { + const typesSource = readWebNext('lib/types.ts'); + + expect(typesSource).toContain("_displayStatus?: 'ACTIVE' | 'DISAPPEARED'"); + expect(typesSource).toContain('_disappearTime?: number'); + expect(typesSource).toContain('export interface OtlpUnboundEntityCandidate'); + expect(typesSource).toContain('recentUnboundCandidates?: OtlpUnboundEntityCandidate[]'); + expect(typesSource).toContain('unifiedEvidenceSummary?: EntityUnifiedEvidenceSummary'); + expect(typesSource).toContain('export interface EntityUnifiedEvidenceSummary'); + expect(typesSource).toContain('incidentId?: number'); + }); +}); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
