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
| Field | Value | Evidence |
|---|---|---|
| Route name | monitoringDashboard | src/router/route-defs.ts:120 |
| Path | /dashboard | src/router/route-defs.ts:121 |
| Component | Pages.Dashboard → lazy () => import('./dashboard/index.vue') | src/router/index.ts:10, src/pages/index.ts:5 |
| meta | title: 'Dashboard', icon: 'laptop-star', module: 'system', order: 1, parent: 'monitoring', requiresPermission: true | src/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
| Component | File | Props | Emits | Responsibility |
|---|---|---|---|---|
| AlertOverview | modules/dashboard/components/AlertOverview.vue | — | — | Owns includeClosed toggle; calls useNumberOfAlerts(); derives 7 computed() chart datasets (predefined label/color maps merged with API counts); renders 4 RACountingChart + 3 RATrendingChart. |
| MTTAROverview | modules/dashboard/components/MTTAROverview.vue | — | — | Owns 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.vue | data: DoughnutChartDataPoint[], title: string | — | Presentational — Card + DoughnutChart. |
| RATrendingChart | .../RATrendingChart.vue | data: {chartData, labels}, title, description | — | Presentational — horizontal BarLineChart, y-axis labels truncated via truncateLabel. |
| RAChartComboText | .../RAChartComboText.vue | title (required), description (required) | — | Presentational title+description block, reused by 3 other chart components. |
| RAMeanTimeCompoundChart | .../RAMeanTimeCompoundChart.vue | title, description, chartData, labels | — | Presentational — dual-axis BarLineChart (y count, y1 minutes). |
| RAPriorityPerDay | .../RAPriorityPerDay.vue | range: RARangePicker | — | Independently calls useAlertsTrendByPriority(); watches props.range, debounced (500 ms) fetch; builds stacked P1–P5 chart. |
| RARangePicker | .../RARangePicker.vue | value: RARangePicker | update:value | Wraps shared AdvancedTimeRange; enforces MAX_PERIOD = 31 days client-side. |
| RAStatistics | .../RAStatistics.vue | createdAlerts, 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)
| Constant | Path | Called from | Query params |
|---|---|---|---|
| API_ROUTES.ALERT.REPORT.ALERTS_BY_STATUS | /report/alerts/by/status | useNumberOfAlerts() | exclude_closed |
| ALERTS_BY_PRIORITY | /report/alerts/by/priority | useNumberOfAlerts() | exclude_closed |
| ALERTS_BY_ALERTNAME | /report/alerts/by/alertname | useNumberOfAlerts() | exclude_closed |
| ALERTS_BY_CLUSTER | /report/alerts/by/cluster | useNumberOfAlerts() | exclude_closed |
| ALERTS_BY_LABEL | /report/alerts/by/label | useNumberOfAlerts() ×3 | exclude_closed, label=namespace |
| ALERTS_TREND_BY_PRIORITY | /report/alerts/trend/by/priority | useAlertsTrendByPriority() (via RAPriorityPerDay) | start_date, end_date |
| ALERTS_MTTAR_COUNT | /report/alerts/mttar/counts | useAlertsMttarOverview() | start_date, end_date |
| ALERTS_MTTAR | /report/alerts/mttar | useAlertsMttarOverview() | start_date, end_date |
| ALERTS_MTTA_TREND | /report/alerts/mtta/trend | useAlertsMttarOverview() | start_date, end_date |
| ALERTS_MTTR_TREND | /report/alerts/mttr/trend | useAlertsMttarOverview() | 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
- Toggle inversion: the “Include Closed” Switch (includeClosed, default true) is sent inverted as exclude_closed — fetch(!includeClosed.value) (AlertOverview.vue:230).
- Server-side top-N: namespace/pod/container breakdowns request limit: 10 server-side (modules/dashboard/composables/index.ts:103-105).
- 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.
- 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).
- 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).
- 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. - 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.
- 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
- Router resolves monitoringDashboard → guard checks verbMap['monitoringDashboard'] includes 'view' (mfeBootstrap.ts:103-120).
- pages/dashboard/index.vue mounts, onMounted sets breadcrumbs via injected host:setBreadcrumbs.
- AlertOverview.onMounted calls useNumberOfAlerts().fetch(!includeClosed.value) → 7 concurrent GET requests fired through createRequest → axiosInstance (XSRF header + baseURL injected by the request interceptor).
- 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'.
- Component computed()s re-derive chart-ready shapes (
{labels, chartData}/ DoughnutChartDataPoint[]) as data.value refs update. - Chart components (RACountingChart, RATrendingChart, etc.) re-render reactively via @cloudz-mp/zmp-common-ui’s DoughnutChart/BarLineChart.
- 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
- Dashboard is the primary analytics landing page with no global store and no persisted local state across navigation.
- AlertOverview and MTTAROverview each fetch independently, using the same shared HTTP layer but different async lifecycles.
- 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.
- MTTA/MTTR range is capped to 31 days to prevent over-large requests.
- 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.