Chuyển tới nội dung chính

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)

PurposeRoute namePathComponentmetaEvidence
ListmonitoringRuleGroups/rules/rule-groupsPages.RuleGroups → src/pages/rules/rule-group-list/index.vuerequiresPermission: true, apis: ruleGroupListApis (GET list, DELETE)src/router/route-defs.ts:267-279
CreatemonitoringRuleGroupCreate/rules/rule-groups/createPages.RuleGroupDetail → src/pages/rules/rule-group-detail/index.vue (same component as edit)hidden: true, requiresPermission: true, layout: 'monitoringRuleGroups', apis: ruleGroupDetailApissrc/router/route-defs.ts:280-292
Edit/DetailmonitoringRuleGroupDetail/rules/rule-groups/detail/:ruleGroupIdsame componentsame shape as Createsrc/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)

&lt;Spin fullscreen v-if="ruleGroupDetailLoading" /&gt; 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)

ComponentFilePropsEmitsResponsibility
RuleGroupListTablerule-group-list/components/RuleGroupListTable.vueruleGroups: RuleGroup[], loading: booleanSmartTable + AdvancedFilter; client-side search/filter over group name and nested rule fields; owns delete-modal open state.
RuleGroupColumnActionrule-group-list/components/RuleGroupColumnAction.vueruleGroup: RuleGroupdeleteRow action menu — “Edit” navigates to RULE_GROUP_DETAIL (gated by authorization.edit), “Delete” emits (gated by authorization.delete).
RuleGroupDeleteModalrule-group-list/components/RuleGroupDeleteModal.vueopen, ruleGroup?, groupName?update:open, deleteConfirmation modal (PopupDelete); calls useRuleGroup().remove(), on success toasts + refresh()s the list. Reused (via groupName prop) inside RGDHeaderActions.
RGDRuleGroupFormrule-group-detail/components/RGDRuleGroupForm.vuemode: Mode, initialValues: RuleGroupDetailnull
RGDHeaderActionsrule-group-detail/components/RGDHeaderActions.vuemode: Mode, loading?: booleanRenders Cancel/Save (create) or Delete/Back/Update (edit); emits the event-bus submit sequence; owns delete-modal open state.
RuleDropdownrule-group-detail/components/RuleDropdown.vueoptions, placeholder, disabled, width, maxHeightselectGeneric reusable popover dropdown; used for both group-template and per-rule-template pickers.
RGDRulerule-group-detail/components/RGDRule.vuemode: Mode, initialValues: RuleGroupDetailnull
RGDRuleHeader.../rgd-rule/RGDRuleHeader.vue“Add new rule” button, emits EVENT_BUS_KEY.RULES.CREATE (gated by authorization.edit).
RDGRuleTable.../rgd-rule/RDGRuleTable.vuerules, mode, errorRowKeysdelete, update:rules, duplicateEdit-mode table of existing rules; expands a row into an inline RGDRuleForm (insideTable=true) for editing; scrolls/highlights error rows.
RGDRuleForm.../rgd-rule/RGDRuleForm.vueid, value?, initialValues?, newRules?, mode?, showActions?, insideTable?update:value, change, delete, duplicate, mount, unmount, finishThe 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.vuesummary?, 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 &#123;realm&#125; 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/&#123;realm&#125;', 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/&#123;realm&#125;, namespace hardcoded to 'zcp')

ActionMethodConstantResolved pathCalled from
List rule groupsGETRULES.RULE_GROUPS = /prometheus/rules/&#123;namespace&#125;.../prometheus/rules/zcpuseRuleGroups() — rule-group-list/composables/index.ts:16-19 (via useFetch)
Get rule group detailGETRULES.RULE_GROUP_V2 = /prometheus/rules/&#123;namespace&#125;/{groupName}.../prometheus/rules/zcp/&#123;groupName&#125;useRuleGroupDetail().fetch() — rule-group-detail/composables/index.ts:24-35
Create rule group`POSTRULES.RULE_GROUPS_V2 = /rules/{namespace}`.../rules/zcpuseRuleGroup().create() — rule-group-detail/composables/index.ts:133-141
Update rule group`POSTRULES.RULE_GROUPS_V2 = /rules/{namespace}` (same endpoint as create, no PUT/PATCH).../rules/zcpuseRuleGroup().update() — lines 143-151
Delete rule groupDELETERULES.RULE_GROUP = /rules/&#123;namespace&#125;/{groupName}.../rules/zcp/&#123;groupName&#125;useRuleGroup().remove() — lines 117-123, invoked from RuleGroupDeleteModal.vue:47-50
Get rule templates (predefined groups)GETAPI_ROUTES_CORE.ALERT_RULE.TEMPLATES = /core/v1/alert-rule-templatesfetchRuleTemplates()/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/&#123;realm&#125;/rules/{namespace}/&#123;groupName&#125; (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}/&#123;groupName&#125; (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, &#123; validationSchema &#125;) (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 &#123; view: true, edit: true, delete: true, admin: true &#125; — all-permissive by default.

Business Logic

Alert Rules — Add new rule group

  1. Rule group name: required, must match /^[A-Za-z0-9-._]+$/ (letters, digits, , _, .) — schemas/index.ts:128-141 (ruleGroupSchema).
  2. 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).
  3. 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.
  4. 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).
  5. 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.
  6. Rule name validation differs by ruleType: record → /^[a-zA-Z0-9_]+$/; alert → /^[A-Za-z0-9-._]+$/ (schemas/index.ts:32-58).
  7. 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.
  8. forValue (Prometheus for duration) is optional but, if present, must satisfy the same validateTime regex as interval.
  9. 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).
  10. 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).
  11. 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).
  12. 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.
  13. 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).
  14. 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).
  15. 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.
  16. Delete confirmation: deleting a rule group (edit mode, requires authorization.delete) opens RuleGroupDeleteModal (PopupDelete); on confirm, DELETE .../rules/zcp/&#123;groupName&#125;, then toast + navigate back to the list (RGDHeaderActions.handleDeleteSuccess).
  17. 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).
  18. 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

  1. 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”.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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/&#123;realm&#125;/rules/zcp with body &#123; name, interval, rules: [...] &#125;.
  7. Response Processing: on status.value === 'success' → toast.success('Rule group has been created successfully', &#123; onClose: ... &#125;); on any other status, silently ruleGroupStore.reset() (errors surfaced only via the global axios error toast).
  8. 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/&#123;groupName&#125;.

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/&#123;index,axiosInstance&#125;.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

  1. 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.
  2. Update uses POST, not PUT/PATCH: both create and update call the identical endpoint (POST .../rules/&#123;namespace&#125;) — src/modules/rules/rule-group-detail/composables/index.ts:133-151.
  3. API path discrepancy: the route’s declared permission-binding API (ruleGroupDetailApis in route-defs.ts:58) documents GET .../rules/&#123;namespace&#125;/{groupName}, but the actual runtime fetch (useRuleGroupDetail) hits GET .../prometheus/rules/&#123;namespace&#125;/&#123;groupName&#125; (extra /prometheus segment) — src/constants/apiRoutes/index.ts:118-122 vs. src/router/route-defs.ts:51-60.
  4. 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.
  5. Namespace is hardcoded to 'zcp' for every rule-group API call — not derived from route, project, or realm.
  6. useRuleGroupStore.createPayload is declared and returned from the store but never assigned anywhere in the codebase (dead state field) — src/store/rulesStore.ts:13.
  7. 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.
  8. 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.
  9. 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.