Alert Rule
Scope: Alert Rules → “Add new rule group” flow, plus the Rule Group List page it launches from.
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 second feature analyzed in this document is the Alert Rules flow:
- Alert Rules → Add new rule group — src/pages/rules/rule-group-list/index.vue (list + entry point) and src/pages/rules/rule-group-detail/index.vue (shared create/edit page), backed by the Monitoring service’s Prometheus rule-group endpoints, with client-side Zod validation and an event-bus-orchestrated multi-form submit.
Two route definitions are involved:
- List: monitoringRuleGroups → /rules/rule-groups
- Create/Edit: monitoringRuleGroupCreate → /rules/rule-groups/create and monitoringRuleGroupDetail → /rules/rule-groups/detail/:ruleGroupId
Route Design
Alert Rules (list, create, detail)
| Purpose | Route name | Path | Component | meta | Evidence |
|---|---|---|---|---|---|
| List | monitoringRuleGroups | /rules/rule-groups | Pages.RuleGroups → src/pages/rules/rule-group-list/index.vue | requiresPermission: true, apis: ruleGroupListApis (GET list, DELETE) | src/router/route-defs.ts:267-279 |
| Create | monitoringRuleGroupCreate | /rules/rule-groups/create | Pages.RuleGroupDetail → src/pages/rules/rule-group-detail/index.vue (same component as edit) | hidden: true, requiresPermission: true, layout: 'monitoringRuleGroups', apis: ruleGroupDetailApis | src/router/route-defs.ts:280-292 |
| Edit/Detail | monitoringRuleGroupDetail | /rules/rule-groups/detail/:ruleGroupId | same component | same shape as Create | src/router/route-defs.ts:293-305 |
- There is no id === 'new' sentinel pattern. Create and Edit are two distinct routes that both map to the same lazily-loaded RuleGroupDetail page component (src/router/index.ts:23-24, src/pages/index.ts:27-28).
- The page itself determines create vs. edit purely by comparing the literal path, not the route name:
// src/pages/rules/rule-group-detail/index.vue:48-56
const computedMode = computed<Mode>(() => {
if (route.path === SUB_ROUTE_PATH.RULE_GROUPS_CREATE) return 'create';
return 'edit';
});
- meta.layout: 'monitoringRuleGroups' on both Create and Detail means these two routes bypass the route-level permission guard entirely and instead inherit menu verbs from the parent list page.
UI Structure
Alert Rules — List page src/pages/rules/rule-group-list/index.vue
pages/rules/rule-group-list/index.vue (route: /rules/rule-groups)
├─ SubHeader (title "Rule Groups", total = ruleGroups.length)
│ └─ RouterLink :to="SUB_ROUTE_PATH.RULE_GROUPS_CREATE"
│ └─ Button "Add new rule group" (v-if="authorization.edit")
└─ modules/rules/rule-group-list/components/RuleGroupListTable.vue
├─ AdvancedFilter (search + column filters)
├─ RuleGroupSubListTable.vue (expanded row: rules under a group)
├─ RuleGroupColumnAction.vue (row menu: Edit / Delete)
└─ RuleGroupDeleteModal.vue (delete confirmation, PopupDelete)
Entry point for “Add new rule group” is a plain RouterLink navigation (not a modal):
<!-- src/pages/rules/rule-group-list/index.vue:4-9 -->
<RouterLink :to="SUB_ROUTE_PATH.RULE_GROUPS_CREATE">
<button v-if="authorization.edit" prefix-icon="plus" type="primary">Add new rule group</button>
</RouterLink>
Alert Rules — Create/Edit page src/pages/rules/rule-group-detail/index.vue
pages/rules/rule-group-detail/index.vue (route: /rules/rule-groups/create OR /rules/rule-groups/detail/:ruleGroupId)
├─ SubHeader (title: "Add New Rule Group" | "Edit Rule Group")
│ └─ RGDHeaderActions.vue
│ ├─ create: Cancel, Save
│ ├─ edit: Delete, Back to list, Update
│ └─ RuleGroupDeleteModal.vue (reused from rule-group-list)
├─ RGDRuleGroupForm.vue (group name + interval + template picker)
│ └─ RuleDropdown.vue (generic popover dropdown, reused)
└─ RGDRule.vue (rules section orchestrator)
├─ rgd-rule/RGDRuleHeader.vue ("Rule" title + "Add new rule" button)
├─ rgd-rule/RDGRuleTable.vue (edit mode only, v-if mode !== 'create')
│ ├─ rgd-rule/RDGRuleTableActions.vue (duplicate / delete)
│ └─ rgd-rule/RGDRuleForm.vue (expanded-row editor, mode='edit' insideTable=true)
└─ rgd-rule/RGDRuleForm.vue ×N (create mode: one card per new rule)
└─ rgd-rule/RGDRuleAlertPreview.vue (live preview panel, alert type only)
<Spin fullscreen v-if="ruleGroupDetailLoading" /> overlays the page while fetching an existing group’s detail in edit mode (src/pages/rules/rule-group-detail/index.vue:14).
Component Design
Alert Rules components (create/edit rule group)
| Component | File | Props | Emits | Responsibility |
|---|---|---|---|---|
| RuleGroupListTable | rule-group-list/components/RuleGroupListTable.vue | ruleGroups: RuleGroup[], loading: boolean | — | SmartTable + AdvancedFilter; client-side search/filter over group name and nested rule fields; owns delete-modal open state. |
| RuleGroupColumnAction | rule-group-list/components/RuleGroupColumnAction.vue | ruleGroup: RuleGroup | delete | Row action menu — “Edit” navigates to RULE_GROUP_DETAIL (gated by authorization.edit), “Delete” emits (gated by authorization.delete). |
| RuleGroupDeleteModal | rule-group-list/components/RuleGroupDeleteModal.vue | open, ruleGroup?, groupName? | update:open, delete | Confirmation modal (PopupDelete); calls useRuleGroup().remove(), on success toasts + refresh()s the list. Reused (via groupName prop) inside RGDHeaderActions. |
| RGDRuleGroupForm | rule-group-detail/components/RGDRuleGroupForm.vue | mode: Mode, initialValues: RuleGroupDetail | null | — |
| RGDHeaderActions | rule-group-detail/components/RGDHeaderActions.vue | mode: Mode, loading?: boolean | — | Renders Cancel/Save (create) or Delete/Back/Update (edit); emits the event-bus submit sequence; owns delete-modal open state. |
| RuleDropdown | rule-group-detail/components/RuleDropdown.vue | options, placeholder, disabled, width, maxHeight | select | Generic reusable popover dropdown; used for both group-template and per-rule-template pickers. |
| RGDRule | rule-group-detail/components/RGDRule.vue | mode: Mode, initialValues: RuleGroupDetail | null | — |
| RGDRuleHeader | .../rgd-rule/RGDRuleHeader.vue | — | — | “Add new rule” button, emits EVENT_BUS_KEY.RULES.CREATE (gated by authorization.edit). |
| RDGRuleTable | .../rgd-rule/RDGRuleTable.vue | rules, mode, errorRowKeys | delete, update:rules, duplicate | Edit-mode table of existing rules; expands a row into an inline RGDRuleForm (insideTable=true) for editing; scrolls/highlights error rows. |
| RGDRuleForm | .../rgd-rule/RGDRuleForm.vue | id, value?, initialValues?, newRules?, mode?, showActions?, insideTable? | update:value, change, delete, duplicate, mount, unmount, finish | The actual per-rule form (useForm + getRuleFormSchema); fields conditional on ruleType (alert vs. record); rule-name template picker; emits finish on successful validation. |
| RGDRuleAlertPreview | .../rgd-rule/RGDRuleAlertPreview.vue | summary?, description?, priority?, severity? | — | Static mock-up preview card of how the alert will render (hardcoded cluster/namespace/timestamps — see Risks). |
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.
- 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); used by useRuleGroups() for the rule-group list.
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).
Alert Rules endpoints (base /monitoring/v1beta1/realms/{realm}, namespace hardcoded to 'zcp')
| Action | Method | Constant | Resolved path | Called from |
|---|---|---|---|---|
| List rule groups | GET | RULES.RULE_GROUPS = /prometheus/rules/{namespace} | .../prometheus/rules/zcp | useRuleGroups() — rule-group-list/composables/index.ts:16-19 (via useFetch) |
| Get rule group detail | GET | RULES.RULE_GROUP_V2 = /prometheus/rules/{namespace}/{groupName} | .../prometheus/rules/zcp/{groupName} | useRuleGroupDetail().fetch() — rule-group-detail/composables/index.ts:24-35 |
| Create rule group | `POST | RULES.RULE_GROUPS_V2 = /rules/{namespace}` | .../rules/zcp | useRuleGroup().create() — rule-group-detail/composables/index.ts:133-141 |
| Update rule group | `POST | RULES.RULE_GROUPS_V2 = /rules/{namespace}` (same endpoint as create, no PUT/PATCH) | .../rules/zcp | useRuleGroup().update() — lines 143-151 |
| Delete rule group | DELETE | RULES.RULE_GROUP = /rules/{namespace}/{groupName} | .../rules/zcp/{groupName} | useRuleGroup().remove() — lines 117-123, invoked from RuleGroupDeleteModal.vue:47-50 |
| Get rule templates (predefined groups) | GET | API_ROUTES_CORE.ALERT_RULE.TEMPLATES = /core/v1/alert-rule-templates | — | fetchRuleTemplates()/loadRuleTemplates() — rule-group-detail/apis/rule-templates.ts:108-133, called from RGDRuleGroupForm.vue:123 |
useRuleGroup() (rule-group-detail/composables/index.ts:84-161) is one generic composable with internal _action/_method/_endpoint/_payload refs reused for get/remove/create/update — all funnel through a single useAsyncData + createRequest call.
⚠ API-path discrepancy found: the permission-binding metadata for the detail route declares GET /api/monitoring/v1beta1/realms/{realm}/rules/{namespace}/{groupName} (no /prometheus segment — src/router/route-defs.ts:58, ruleGroupDetailApis), but the runtime fetch in useRuleGroupDetail() actually calls RULES.RULE_GROUP_V2 = /prometheus/rules/{namespace}/{groupName} (with /prometheus — src/constants/apiRoutes/index.ts:120, used at rule-group-detail/composables/index.ts:31). The declared permission-verb path and the actual network call differ by the /prometheus segment. See Risks.
Rule templates request pattern: loadRuleTemplates() calls axiosInstance.get(...) directly (rule-group-detail/apis/rule-templates.ts:109), bypassing createRequest/useAsyncData used everywhere else — not cancellable, no shared loading/error state.
State Management
- Pinia store useRuleGroupStore (src/store/rulesStore.ts, id STORES.RULE_GROUP = 'rule-group') is the cross-component hub for the create/edit rule-group flow: | State | Type | Set by | Read by | |—|—|—|—| | ruleGroup | RuleGroupFormValues | RGDRuleGroupForm.handleFinish | page-level watch | | ruleGroupSubmitted | boolean | setRuleGroup() (auto true) | isReadyForSubmit watchEffect | | rules | RuleFormValues[] | RGDRule’s aggregation watchEffect | page-level watch | | rulesSubmitted | boolean | setRules() (auto true) | isReadyForSubmit watchEffect | | isReadyForSubmit | boolean (derived) | internal watchEffect: ruleGroupSubmitted && rulesSubmitted | page-level watch (triggers the actual API call) | | templateGroup | string | undefined | RGDRuleGroupForm.onPickGroup | RGDRuleForm (per-rule template dropdown), RGDRule (template rule replacement) | | createPayload | CreateRuleGroupPayload | undefined | never set anywhere | never read — dead field | reset() clears all of the above; called on page onMounted, after a successful create/update toast closes, and after a failed submit.
- Module-level singleton refs in rule-group-list/composables/index.ts:8-9 (data, loading declared outside useRuleGroups()) — every caller of useRuleGroups() shares the same underlying refs, which behaves like a singleton store even though it is not a Pinia store.
- Event bus (useEventBus from @cloudz-mp/zmp-common-ui; keys in src/constants/event-bus/index.ts) is the primary cross-sibling communication mechanism for the rule-group form: RULE_FORM_PRE_SUBMIT, RULE_FORM_SUBMIT, RULE_GROUP_FORM_SUBMIT, RULE_GROUP_TABLE_SUBMIT, RULE_RESET, RULE_GROUP_TEMPLATE_SELECT, RULE_GROUP_FORM_VALIDATE, RULE_GROUP_NEW_RULES_VALIDATE, RULE_GROUP_SCROLL_TO_ERROR, RULE_GROUP_TABLE_SCROLL_TO_ERROR, RULES.CREATE.
- Form-local state: both RGDRuleGroupForm and RGDRuleForm use useForm<T>(fields,
{ validationSchema }) (VeeValidate wrapper from @cloudz-mp/zmp-common-ui) for field-level reactive refs + Zod validation, independent of the Pinia store until handleFinish/finish fires. - Authorization state: useAuthorization() injects host:authorization; if not provided, it falls back to
{ view: true, edit: true, delete: true, admin: true }— all-permissive by default.
Business Logic
Alert Rules — Add new rule group
- Rule group name: required, must match /^[A-Za-z0-9-._]+$/ (letters, digits, , _, .) — schemas/index.ts:128-141 (ruleGroupSchema).
- Interval: required, must match /^(\d+y)?\s*(\d+w)?\s*(\d+d)?\s*(\d+h)?\s*(\d{1,2}m)?\s*(\d{1,2}s)?\s*(\d{1,2}ms)?$/ (utils/index.ts:208-213, validateTime).
- Client-side duplicate-name guard (not a schema rule): typing a name that matches an existing predefined template group or an already-loaded custom group is rejected with “This group already exists.” — unless it equals the currently picked group (RGDRuleGroupForm.vue:125-141). This check is only against locally known template/custom names, not a live server-side existing-groups lookup.
- Group name immutable after creation: isGroupDisabled = mode === 'edit' || !authorization.edit disables the group-name input entirely in edit mode; interval remains editable in edit mode as long as authorization.edit (RGDRuleGroupForm.vue:44-51,109).
- Picking a predefined template auto-fills interval, sets ruleGroupStore.templateGroup, and (create mode only) replaces any rules already entered with the template’s rules via RULE_GROUP_TEMPLATE_SELECT (RGDRuleGroupForm.vue:149-168, RGDRule.vue:101-110); a group without predefined rules resets to a single blank rule.
- Rule name validation differs by ruleType: record → /^[a-zA-Z0-9_]+$/; alert → /^[A-Za-z0-9-._]+$/ (schemas/index.ts:32-58).
- record rules relax required fields: priority, severity, description, summary become optional when ruleType === 'record' (checkRequired() returns true unconditionally for record type — schemas/index.ts:19-27); expression is always required regardless of type.
- forValue (Prometheus for duration) is optional but, if present, must satisfy the same validateTime regex as interval.
- Label validation: keys must match /^[a-zA-Z0-9_]+$/ and must not start with a digit; values must match /^[A-Za-z0-9-._]+$/ (schemas/index.ts:153-175, inputTagKeyRules/inputTagValueRules).
- Payload trimming on submit (utils/index.ts:53-110, prepareRuleGroupSubmit): empty for, empty annotations.summary, empty annotations.description are deleted from the payload; the whole annotations object is dropped if both summary and description end up empty. recordtype rules submit only { record, expr } (no labels/annotations/for).
- Minimum one rule row: create mode always starts with exactly one blank rule (getNewRuleTemplate() — utils/index.ts:14-33); a rule card cannot be deleted while fewer than 2 rows remain (computedIsDeleteDisabled = mode==='create' && newRules.length < 2 — RGDRuleForm.vue:281-283).
- Two-phase, event-bus-orchestrated submit — client-side “all-or-nothing” aggregation, no partial submit:
- Click Save/Update → RGDHeaderActions emits RULE_FORM_PRE_SUBMIT → RULE_FORM_SUBMIT → RULE_GROUP_FORM_SUBMIT, or (edit, additionally) RULE_GROUP_TABLE_SUBMIT, RULE_GROUP_NEW_RULES_VALIDATE, RULE_GROUP_SCROLL_TO_ERROR before the same two submit events.
- Every RGDRuleForm and RGDRuleGroupForm listens for RULE_GROUP_FORM_SUBMIT/is triggered by RULE_FORM_SUBMIT and calls its own form.forceSubmit(), running Zod validation independently.
- Each rule form’s successful @finish pushes into RGDRule’s submittingRules; the group form’s @finish writes ruleGroupStore.setRuleGroup(...).
- Only once all rule forms have finished (submittingRules.length === newRules.length) does RGDRule’s watchEffect call ruleGroupStore.setRules(...), which flips rulesSubmitted; combined with ruleGroupSubmitted, the store’s isReadyForSubmit becomes true.
- The page-level watch(() => isReadyForSubmit.value, ...) (pages/rules/rule-group-detail/index.vue:68-117) is the only place that actually calls the create/update API — i.e. the network call only fires after every child form has independently passed validation.
- On create success: toast.success(...); on toast close, ruleGroupStore.reset() then router.push(RULE_GROUP_DETAIL.replace(':ruleGroupId', ruleGroup.ruleGroup)) — navigates using the just-submitted name directly, not an ID returned by the API response (index.vue:84-92).
- On update success: toast, then on close emit(RULE_RESET) (clears RGDRule local state), ruleGroupStore.reset(), and fetch(groupName) to reload fresh data — stays on the page rather than navigating (index.vue:98-115).
- On create/update failure (status.value !== 'success'): only ruleGroupStore.reset() runs; there is no dedicated failure toast in this flow — the only error surfacing is the axios interceptor’s generic toast.error, fired for any failed request that doesn’t pass notifyError: false.
- Delete confirmation: deleting a rule group (edit mode, requires authorization.delete) opens RuleGroupDeleteModal (PopupDelete); on confirm, DELETE .../rules/zcp/
{groupName}, then toast + navigate back to the list (RGDHeaderActions.handleDeleteSuccess). - Hardcoded namespace: every rule-group API call (list/detail/create/update/delete) uses the literal namespace 'zcp' — not derived from route, project, or realm (rule-group-list/composables/index.ts:17, rule-group-detail/composables/index.ts:31,106).
- Authorization gating: “Add new rule group” button, group-name/interval inputs, per-rule fields, Save/Update, and Delete are all conditionally rendered/disabled based on authorization.edit/authorization.delete from the injected host:authorization (default all-true when absent).
Data Flow
Alert Rules — User creates a new rule group
User Action → Event Handler → State Update → API Call → Response Processing → UI Update
- Action: user clicks “Add new rule group” on /rules/rule-groups. Handler: RouterLink :to="SUB_ROUTE_PATH.RULE_GROUPS_CREATE" → router navigation (no explicit click handler needed). State/UI: route becomes /rules/rule-groups/create; RuleGroupDetail page mounts; computedMode evaluates 'create'; onMounted calls ruleGroupStore.reset(); header title = “Add New Rule Group”.
- Action: page/form mount. Effect: RGDRuleGroupForm fires loadRuleTemplates() → GET /core/v1/alert-rule-templates (falls back to the bundled RULE_GROUP_TEMPLATES constants dataset on failure/empty response, never leaving the picker empty); RGDRule seeds newRules with one blank rule card.
- Action: user types a group name or picks a predefined template from the RuleDropdown. Handler: onNewGroupInput/onPickGroup (RGDRuleGroupForm.vue). State: on pick — pickedGroup, newGroupName, form field ruleGroup/interval set; ruleGroupStore.setTemplateGroup(slug); emits RULE_GROUP_TEMPLATE_SELECT, which RGDRule.replaceRulesFromGroupTemplate consumes to replace newRules with the template’s rules.
- Action: user fills in one or more rule cards (RGDRuleForm: type, name, priority, severity, labels, summary, description, expression, for) — optionally clicking “Add new rule” (RULES.CREATE event) for more rows, or “duplicate”/“delete” on a card. State: each field is local useForm state per card; RGDRule tracks the newRules array.
- Action: user clicks Save. Handler: RGDHeaderActions.handleSave() emits RULE_FORM_PRE_SUBMIT → RULE_FORM_SUBMIT → RULE_GROUP_FORM_SUBMIT in sequence. State update (validation phase): every RGDRuleForm/RGDRuleGroupForm runs form.forceSubmit() (Zod schema validation); each successful rule’s @finish appends to RGDRule.submittingRules; the group form’s @finish calls ruleGroupStore.setRuleGroup(...). Once all rule forms have finished, RGDRule’s watchEffect calls ruleGroupStore.setRules(...). The store’s isReadyForSubmit (ruleGroupSubmitted && rulesSubmitted) becomes true.
- API Call: the page’s watch(isReadyForSubmit) fires, builds the payload via prepareRuleGroupSubmit(ruleGroup, rules), and calls create(payload) → useRuleGroup().create() →
POST /api/monitoring/v1beta1/realms/{realm}/rules/zcp withbody { name, interval, rules: [...] }. - Response Processing: on status.value === 'success' → toast.success('Rule group has been created successfully',
{ onClose: ... }); on any other status, silently ruleGroupStore.reset() (errors surfaced only via the global axios error toast). - UI Update: on toast close → ruleGroupStore.reset() then router.push(RULE_GROUP_DETAIL.replace(':ruleGroupId', ruleGroup.ruleGroup)) — the app navigates to the new group’s edit/detail page, which re-mounts the same RuleGroupDetail component in 'edit' mode and fetches the just-created group via GET .../prometheus/rules/zcp/
{groupName}.
Dependency Graph
mfeBootstrap.ts (mount)
└─ createRoutes() ── router/index.ts ── router/route-defs.ts (systemRouteDefs)
└─ pages/index.ts (lazy component map)
├─ RuleGroups (list) ──────────┐
│ pages/rules/rule-group-list/index.vue
│ ├─ composables/useAuthorization, useContentLayout
│ └─ modules/rules/rule-group-list/*
│ ├─ composables (useRuleGroups) ── composables/useFetch ── services/http
│ │ └─ constants/apiRoutes (API_ROUTES_MONITORING.RULES.RULE_GROUPS)
│ └─ components (RuleGroupListTable, RuleGroupColumnAction, RuleGroupDeleteModal, RuleGroupSubListTable, RuleGroupTag)
│
└─ RuleGroupDetail (create + edit) ─┐
pages/rules/rule-group-detail/index.vue
├─ store/rulesStore.ts (useRuleGroupStore, Pinia)
├─ constants/event-bus (EVENT_BUS_KEY.RULES.*)
├─ composables/useContentLayout
└─ modules/rules/rule-group-detail/*
├─ components/RGDRuleGroupForm.vue
│ ├─ schemas (ruleGroupSchema, Zod)
│ ├─ constants/rule-templates.ts (bundled fallback dataset)
│ └─ apis/rule-templates.ts ── services/http (axiosInstance) ── constants/apiRoutes (API_ROUTES_CORE.ALERT_RULE.TEMPLATES)
├─ components/RGDHeaderActions.vue
│ └─ modules/rules/rule-group-list/components/RuleGroupDeleteModal.vue (cross-module reuse)
├─ components/RGDRule.vue
│ └─ components/rgd-rule/`{RGDRuleHeader, RDGRuleTable, RGDRuleForm, RGDRuleAlertPreview}`.vue
│ └─ schemas (getRuleFormSchema, Zod) / utils (validateRule(s), prepareRuleGroupSubmit)
└─ composables/index.ts (useRuleGroupDetail, useRuleGroup)
└─ composables/useAsyncData ── services/http (createRequest → axiosInstance)
└─ constants/apiRoutes (API_ROUTES_MONITORING.RULES.*)
Shared cross-cutting dependencies: 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
Add new rule group
User → click "Add new rule group" → RouterLink navigation → /rules/rule-groups/create
→ RuleGroupDetail mounts (mode=create) → ruleGroupStore.reset() → loadRuleTemplates() [GET /core/v1/alert-rule-templates]
→ User fills RGDRuleGroupForm (name/interval [/ pick template]) + N× RGDRuleForm (rule fields)
→ User clicks Save → RGDHeaderActions emits RULE_FORM_PRE_SUBMIT → RULE_FORM_SUBMIT → RULE_GROUP_FORM_SUBMIT
→ each RGDRuleForm.form.forceSubmit() [Zod validate] → @finish → RGDRule.submittingRules += rule
→ RGDRuleGroupForm.form.forceSubmit() [Zod validate] → @finish → ruleGroupStore.setRuleGroup(...)
→ RGDRule.watchEffect (all rules finished) → ruleGroupStore.setRules(...)
→ ruleGroupStore.isReadyForSubmit = true (watchEffect: ruleGroupSubmitted && rulesSubmitted)
→ page watch(isReadyForSubmit) fires → prepareRuleGroupSubmit(ruleGroup, rules)
→ useRuleGroup().create(payload) → `POST /api/monitoring/v1beta1/realms/{realm}`/rules/zcp
→ axiosInstance response
success → toast.success(onClose: reset store + router.push(RULE_GROUP_DETAIL/:ruleGroupId))
error → global toast.error (axios interceptor) + ruleGroupStore.reset() (no navigation)
→ (success path) RuleGroupDetail re-mounts in mode=edit → GET .../prometheus/rules/zcp/`{groupName}` → renders saved group
Key Findings
- Create and Edit rule-group pages are the same component, distinguished only by comparing route.path to a hardcoded constant (SUB_ROUTE_PATH.RULE_GROUPS_CREATE), not by route name or a route param sentinel — src/pages/rules/rule-group-detail/index.vue:48-56.
- Update uses POST, not PUT/PATCH: both create and update call the identical endpoint (
POST .../rules/{namespace}) — src/modules/rules/rule-group-detail/composables/index.ts:133-151. - API path discrepancy: the route’s declared permission-binding API (ruleGroupDetailApis in route-defs.ts:58) documents GET .../rules/
{namespace}/{groupName}, but the actual runtime fetch (useRuleGroupDetail) hits GET .../prometheus/rules/{namespace}/{groupName}(extra /prometheus segment) — src/constants/apiRoutes/index.ts:118-122 vs. src/router/route-defs.ts:51-60. - Two dead route-definition files: src/router/routes/default.ts (DEFAULT_ROUTES) and src/router/routes/rules.ts (RULE_ROUTES) duplicate Dashboard/Rule-Group route shapes but are never imported anywhere — confirmed by repo-wide grep. Likewise Pages.Layout/src/layouts/DefaultLayout/index.vue is exported but never referenced by the router.
- Namespace is hardcoded to 'zcp' for every rule-group API call — not derived from route, project, or realm.
- useRuleGroupStore.createPayload is declared and returned from the store but never assigned anywhere in the codebase (dead state field) — src/store/rulesStore.ts:13.
- Rule-template loading bypasses the shared HTTP helper: loadRuleTemplates() calls axiosInstance.get() directly instead of createRequest/useAsyncData, so it has no cancellation, no shared loading/error ref, and doesn’t route through useAsyncData’s status machine — src/modules/rules/rule-group-detail/apis/rule-templates.ts:108-113.
- Bundled rule-template dataset contains documented anomalies: several RULE_GROUP_TEMPLATES entries have inline ⚠ anomaly comments where the PromQL expression doesn’t match its own description/rule name.
- RGDRuleAlertPreview.vue renders hardcoded mock values regardless of the actual rule being edited — it is a static UI mock-up, not a live preview.
Risks / Technical Debt
- Dual, partially-inconsistent route/API definitions (route-defs.ts vs. orphaned routes/*.ts; declared meta.apis vs. actual endpoint constants) increase the chance that a future permission-model change updates one source and silently diverges from the other.
- Silent submit failure: on a failed create/update, the only user feedback is the generic Axios interceptor toast — there’s no rule-group-specific error state, retry affordance, or field-level server-error mapping; the store is reset immediately, discarding the user’s in-progress edits.
- Client-only duplicate-name check: the “This group already exists” guard only checks locally-cached template/custom names, not a live server lookup — a name collision with a group created by another user/session concurrently is only caught by the backend on submit.
- Hardcoded 'zcp' namespace across all rule-group calls is a scalability/multi-tenancy risk if the product needs to support multiple namespaces in the future.
Assumptions
- “Alert Rules (add new rule group)” refers to the /rules/rule-groups/create flow reachable from the Rule Groups list page (/rules/rule-groups) — the only “Add new …” entry point found under the Rules module in code.
- Backend response shapes for all /prometheus/rules/* endpoints are assumed from the TypeScript interfaces declared client-side (modules/rules/rule-group-detail/types/api.ts) — actual server 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.