跳到主要内容

Dashboard

Scope: Overview / Dashboard page.

Repo: zcp-monitoring-ui — Vue 3 + TypeScript single-page app, built as a micro-frontend (MFE) mounted by a host shell (src/mfeBootstrap.ts). All findings below are traced directly from source; anything not found in code is explicitly marked Not Found in Code.


Overview

The application is a Vue 3 <script setup> MFE using vue-router (history mode) and pinia. It does not self-bootstrap — src/App.vue is a bare <RouterView />, and mounting is driven externally by a host app calling mount(options) in src/mfeBootstrap.ts, which builds the router (createRoutes(isProjectMode)), injects host-provided authorization/breadcrumb/navigate callbacks via Vue provide, and installs a global router.beforeEach permission guard.

Two independently-shipped route/permission definitions exist side by side:

  • Live: src/router/route-defs.ts (systemRouteDefs) → composed by src/router/index.ts → consumed by mfeBootstrap.ts. This is what actually renders.
  • Dead code: src/router/routes/default.ts and src/router/routes/rules.ts (export DEFAULT_ROUTES / RULE_ROUTES) are never imported by src/router/index.ts or anywhere else in src (verified by repo-wide grep). They duplicate route shapes for Dashboard and Rule Groups but are orphaned.

The feature analyzed in this file is the Overview / Dashboard page:

  • Overview (Dashboard) — src/pages/dashboard/index.vue, read-only analytics page composed of two sections: Alert Overview (status/priority/alertname/cluster/namespace/pod/container breakdowns) and MTTA/R Overview (mean-time-to-acknowledge/resolve statistics + trend charts), each independently fetching from the Alert service’s /report/* endpoints.

Route Design

Dashboard

FieldValueEvidence
Route namemonitoringDashboardsrc/router/route-defs.ts:120
Path/dashboardsrc/router/route-defs.ts:121
ComponentPages.Dashboard → lazy () => import('./dashboard/index.vue')src/router/index.ts:10, src/pages/index.ts:5
metatitle: 'Dashboard', icon: 'laptop-star', module: 'system', order: 1, parent: 'monitoring', requiresPermission: truesrc/router/route-defs.ts:118-130
meta.apis (permission binding)Not Found in Code — no apis array is declared for this route (unlike Alerts/Rules/Channels/etc.)src/router/route-defs.ts:118-130
  • Route composition: compose(systemRouteDefs, systemComponentMap) builds {$([regex]::Match({ path, name, component, meta }, "(?<=\{)(.*?)(?=\})").Value } for every def (src/router/index.ts:44-52).
  • In DEV builds only, / redirects to {$([regex]::Match({ name: 'monitoringDashboard' }, "(?<=\{)(.*?)(?=\})").Value } (src/router/index.ts:54-56, gated by import.meta.env.DEV).
  • Runtime route name is prefixed with mfeId (default 'monitoring', from VITE_MFE_ID env var — src/constants/common.ts:18-20), so ROUTER_NAMES.DASHBOARD === 'monitoringDashboard' (src/constants/routes/index.ts:134-170), matching the literal name in route-defs.ts.
  • ROUTE_PATHS.DASHBOARD resolves to a /system/{mfeId}prefixed path when used for navigation links inside SYSTEM_BASE_ROUTES (src/constants/routes/index.ts:54-132); SUB_ROUTE_PATH.DASHBOARD is the raw unprefixed /dashboard.

Router permission guard

src/mfeBootstrap.ts:103-120:

router.beforeEach((to, _, next) => {
if (to.meta.layout) return next();
if (!to.meta.requiresPermission) return next();
const verbs = verbMap?.value[to.name as string] ?? [];
if (!verbs.includes('view')) {
denyView(to);
return next(false);
}
next();
});
  • Dashboard (requiresPermission: true, no layout) is guarded on the 'view' verb for monitoringDashboard.

UI Structure

Dashboard — src/pages/dashboard/index.vue

pages/dashboard/index.vue                              (route: /dashboard)
├─ modules/dashboard/components/AlertOverview.vue
│ ├─ SubHeader, Switch, Typography (@cloudz-mp/zmp-common-ui)
│ ├─ RACountingChart.vue ×4 (status / priority / alertname top-5 / cluster top-5)
│ │ └─ Card, DoughnutChart
│ └─ RATrendingChart.vue ×3 (namespace top-10 / pod top-10 / container top-10)
│ ├─ Card, BarLineChart
│ └─ RAChartComboText.vue
└─ modules/dashboard/components/MTTAROverview.vue
├─ RARangePicker.vue
│ └─ components/AdvancedTimeRange/index.vue (shared, outside dashboard module)
├─ RAStatistics.vue
├─ RAPriorityPerDay.vue
│ ├─ Card, BarLineChart (stacked)
│ └─ RAChartComboText.vue
└─ RAMeanTimeCompoundChart.vue ×2 (MTTA / MTTR)
├─ Card, BarLineChart (dual y-axis)
└─ RAChartComboText.vue

Page body (src/pages/dashboard/index.vue:1-6): a flex column rendering <AlertOverview /> then <MTTAROverview /> — no other page-level markup. Page’s only side-effect is onMounted → setBreadcrumbs(...) via useContentLayout (src/composables/useContentLayout/index.ts).


Component Design

Dashboard components

ComponentFilePropsEmitsResponsibility
AlertOverviewmodules/dashboard/components/AlertOverview.vueOwns includeClosed toggle; calls useNumberOfAlerts(); derives 7 computed() chart datasets (predefined label/color maps merged with API counts); renders 4 RACountingChart + 3 RATrendingChart.
MTTAROverviewmodules/dashboard/components/MTTAROverview.vueOwns range (date range); calls useAlertsMttarOverview(); formats MTTA/MTTR stats and builds 4-series trend chart data (bar + 3 line series on secondary axis); renders RARangePicker, RAStatistics, RAPriorityPerDay, 2× RAMeanTimeCompoundChart.
RACountingChart.../RACountingChart.vuedata: DoughnutChartDataPoint[], title: stringPresentational — Card + DoughnutChart.
RATrendingChart.../RATrendingChart.vuedata: {chartData, labels}, title, descriptionPresentational — horizontal BarLineChart, y-axis labels truncated via truncateLabel.
RAChartComboText.../RAChartComboText.vuetitle (required), description (required)Presentational title+description block, reused by 3 other chart components.
RAMeanTimeCompoundChart.../RAMeanTimeCompoundChart.vuetitle, description, chartData, labelsPresentational — dual-axis BarLineChart (y count, y1 minutes).
RAPriorityPerDay.../RAPriorityPerDay.vuerange: RARangePickerIndependently calls useAlertsTrendByPriority(); watches props.range, debounced (500 ms) fetch; builds stacked P1–P5 chart.
RARangePicker.../RARangePicker.vuevalue: RARangePickerupdate:valueWraps shared AdvancedTimeRange; enforces MAX_PERIOD = 31 days client-side.
RAStatistics.../RAStatistics.vuecreatedAlerts, closedAlerts, mtta, mttr (all String)Presentational stat tiles, falls back to '-'.

API Design

All requests go through the shared HTTP layer:

  • src/services/http/axiosInstance.ts — single axios.create() instance: withCredentials: true, XSRF cookie (XSRF-TOKEN) → header (X-XSRF-TOKEN); getBaseUrl() = window.baseConfig?.baseApiUrl || '/api' in production, '' in dev (Vite proxy); request interceptor substitutes the literal token {realm} in the URL with the value set via setRealm() (called from mfeBootstrap.ts:58 on the host-provided profile.realm); response/error interceptor globally toasts errors (toast.error, auto-close for GET) and redirects the browser on 401/302 in production.
  • src/services/http/index.ts — createRequest(method, url, options) wraps the instance with an AbortController (cancellable) and returns { request, cancel, isCancellable }; GET/POST/PUT/PATCH/DELETE convenience wrappers also exported (unused by the two features below, which call createRequest directly).
  • src/composables/useAsyncData — generic data/status/error/statusCode wrapper around a request factory, refresh()/cancel().
  • src/composables/useFetch — thin useAsyncData wrapper taking a URL directly (immediate: true default).

Endpoint namespacing (src/constants/apiRoutes/index.ts): SERVICES.ALERT = '/alert/v1', SERVICES.MONITORING = '/monitoring/v1beta1/realms/{realm}', SERVICES.CORE = '/core/v1'; applyPrefix() adds /api in dev (Vite proxy) or nothing in prod (host supplies gateway base URL).

Dashboard endpoints (base /alert/v1, all GET)

ConstantPathCalled fromQuery params
API_ROUTES.ALERT.REPORT.ALERTS_BY_STATUS/report/alerts/by/statususeNumberOfAlerts()exclude_closed
ALERTS_BY_PRIORITY/report/alerts/by/priorityuseNumberOfAlerts()exclude_closed
ALERTS_BY_ALERTNAME/report/alerts/by/alertnameuseNumberOfAlerts()exclude_closed
ALERTS_BY_CLUSTER/report/alerts/by/clusteruseNumberOfAlerts()exclude_closed
ALERTS_BY_LABEL/report/alerts/by/labeluseNumberOfAlerts() ×3exclude_closed, label=namespace
ALERTS_TREND_BY_PRIORITY/report/alerts/trend/by/priorityuseAlertsTrendByPriority() (via RAPriorityPerDay)start_date, end_date
ALERTS_MTTAR_COUNT/report/alerts/mttar/countsuseAlertsMttarOverview()start_date, end_date
ALERTS_MTTAR/report/alerts/mttaruseAlertsMttarOverview()start_date, end_date
ALERTS_MTTA_TREND/report/alerts/mtta/trenduseAlertsMttarOverview()start_date, end_date
ALERTS_MTTR_TREND/report/alerts/mttr/trenduseAlertsMttarOverview()start_date, end_date

All 10 calls are issued from a single internal helper useRAData<T>(apiRoute) (modules/dashboard/composables/index.ts:22-65), so every request/response shares the same useAsyncData state machine.


State Management

  • No global store for Dashboard. Confirmed by grep across src/store/** — no “dashboard” references. All Dashboard state is local ref/computed inside each composable/component (AlertOverview.includeClosed, MTTAROverview.range, and the response refs inside useNumberOfAlerts/useAlertsMttarOverview/useAlertsTrendByPriority); state is re-derived on every mount, nothing persists across navigation.
  • Authorization state: useAuthorization() (src/composables/useAuthorization/index.ts) injects host:authorization (provided by the host shell); if not provided (e.g., standalone/dev), it falls back to {$([regex]::Match({ view: true, edit: true, delete: true, admin: true }, "(?<=\{)(.*?)(?=\})").Value } — i.e., all-permissive by default.

Business Logic

Dashboard

  1. Toggle inversion: the “Include Closed” Switch (includeClosed, default true) is sent inverted as exclude_closed — fetch(!includeClosed.value) (AlertOverview.vue:230).
  2. Server-side top-N: namespace/pod/container breakdowns request limit: 10 server-side (modules/dashboard/composables/index.ts:103-105).
  3. Client-side top-N is NOT enforced for alertname/cluster: computedRAByAlertname/computedRAByCluster (AlertOverview.vue:112-150) sort all returned items descending by count but only map colors from a 5-entry predefinedColors array with no .slice(0, 5) — if the API returns more than 5 items, all are rendered (with color: undefined beyond index 4), even though the chart title says “top 5”. See Risks.
  4. Date-range cap: MTTA/R range picker rejects spans over MAX_PERIOD = 31 days client-side, blocking the fetch and showing “Max Period is 1 Month.” (RARangePicker.vue:36,64-76).
  5. Date formatting: formatRADate forces startDate → startOf('day'), endDate → endOf('day'), both formatted YYYY-MM-DDTHH:mm:ss.000Z (modules/dashboard/utils/index.ts:5-18).
  6. MTTA/MTTR display conversion: raw minute values converted via convertMinuteToMinuteAndSecond (src/utils/date) into "{m}m {s}s"; empty/0falsy values render as '' → '-' in RAStatistics.
  7. Fixed category axes: status (Closed/Acked/Snoozed/Open) and priority (P5..P1) charts use a hardcoded, fixed set of labels/colors merged with API counts via a Map lookup (default 0 for missing categories) — any category returned by the API but not in the predefined set is silently dropped.
  8. Debounce: AlertOverview fetch debounced 200 ms on toggle change; MTTAROverview and RAPriorityPerDay fetch debounced 500 ms on range change. RAPriorityPerDay runs an independent useAlertsTrendByPriority() instance from MTTAROverview’s own composable instance — they merely share the same range value via prop, not a shared request.

Data Flow

Dashboard — User loads the page

  1. Router resolves monitoringDashboard → guard checks verbMap['monitoringDashboard'] includes 'view' (mfeBootstrap.ts:103-120).
  2. pages/dashboard/index.vue mounts, onMounted sets breadcrumbs via injected host:setBreadcrumbs.
  3. AlertOverview.onMounted calls useNumberOfAlerts().fetch(!includeClosed.value) → 7 concurrent GET requests fired through createRequest → axiosInstance (XSRF header + baseURL injected by the request interceptor).
  4. Each request’s useAsyncData.refresh() sets status='loading' → awaits axios → on success sets data.value = response.data; on failure the axios errorHandler shows a toast.error and useAsyncData sets status='error'.
  5. Component computed()s re-derive chart-ready shapes ({labels, chartData} / DoughnutChartDataPoint[]) as data.value refs update.
  6. Chart components (RACountingChart, RATrendingChart, etc.) re-render reactively via @cloudz-mp/zmp-common-ui’s DoughnutChart/BarLineChart.
  7. Separately, once the user picks a date range in RARangePicker (embedded in MTTAROverview), watch(range) (debounced 500 ms) fires useAlertsMttarOverview().fetch(startDate, endDate) (4 concurrent GETs) and, independently, RAPriorityPerDay’s own watch(props.range) fires its own useAlertsTrendByPriority().fetch(...).

Dependency Graph

mfeBootstrap.ts (mount)
└─ createRoutes() ── router/index.ts ── router/route-defs.ts (systemRouteDefs)
└─ pages/index.ts (lazy component map)
├─ Dashboard ──────────────────┐
│ pages/dashboard/index.vue │
│ ├─ composables/useContentLayout
│ ├─ modules/dashboard/components/AlertOverview.vue
│ │ └─ modules/dashboard/composables (useNumberOfAlerts)
│ │ └─ composables/useAsyncData ── services/http (createRequest → axiosInstance)
│ │ └─ constants/apiRoutes (API_ROUTES.ALERT.REPORT.*)
│ └─ modules/dashboard/components/MTTAROverview.vue
│ ├─ components/AdvancedTimeRange (shared)
│ └─ modules/dashboard/composables (useAlertsMttarOverview, useAlertsTrendByPriority)

Shared cross-cutting dependencies for both features: src/composables/useAsyncData, src/services/http/{index,axiosInstance}.ts, src/constants/apiRoutes/index.ts, src/composables/useAuthorization, src/composables/useContentLayout, src/constants/routes/index.ts.


Sequence Flow

Dashboard load

User → Router (guard: view verb) → dashboard/index.vue (mount, setBreadcrumbs)
→ AlertOverview.onMounted → useNumberOfAlerts.fetch(excludeClosed)
→ 7× createRequest(GET) → axiosInstance (XSRF + baseURL + realm N/A) → Alert service
→ response → useAsyncData sets data/status → computed chart datasets → DoughnutChart/BarLineChart render
→ User picks date range → RARangePicker validates (<=31d) → MTTAROverview.watch(range) [debounce 500ms]
→ useAlertsMttarOverview.fetch(start,end) → 4× createRequest(GET) → Alert service
→ RAPriorityPerDay.watch(range) [debounce 500ms, independent] → 1× createRequest(GET) → Alert service
→ responses → computed stats/trend series → RAStatistics/RAMeanTimeCompoundChart render

Key Findings

  1. Dashboard is the primary analytics landing page with no global store and no persisted local state across navigation.
  2. AlertOverview and MTTAROverview each fetch independently, using the same shared HTTP layer but different async lifecycles.
  3. Client-side “top 5” claim not enforced: AlertOverview.vue titles say “top 5” but the data is only sorted and partially colored, not sliced to 5.
  4. MTTA/MTTR range is capped to 31 days to prevent over-large requests.
  5. Routes and permissions are controlled by route-defs.ts and the global router guard, not by the orphaned route file definitions.

Risks / Technical Debt

  • Silent data mismatches: hardcoded label sets and partial top-N behavior can lead to charts that look complete but omit expected categories.
  • Request duplication: the same trend data may be fetched more than once by independent widgets with no deduplication layer.
  • Dead route definitions: duplicate route files remain in the repo but are not used by the actual router setup.

Assumptions

  • “Overview” in the task request refers to the Dashboard page (/dashboard) — the only page in the codebase titled “Dashboard” (route-defs.ts:123) and matching common “overview” UI (top-level stat/analytics landing page).
  • Backend response shapes for all /report/* endpoints are assumed from the TypeScript interfaces declared client-side (modules/dashboard/types/apis/index.ts) — actual backend contracts were not verified against a live backend or OpenAPI spec.
  • authorization values (view/edit/delete/admin) are supplied by a host shell at runtime via verbMap/host:authorization; this document treats the all-true fallback as the effective behavior in any environment where the host doesn’t inject them (e.g., local/standalone dev), per src/composables/useAuthorization/index.ts:7-16.