/** * Alerting option lists and enum types used by the rule editor. * * These are **string-literal unions mirrored by hand from the backend Java * enums**. Why not derived from `schema.d.ts`? Springdoc emits polymorphic * `@JsonSubTypes` conditions with two conflicting `kind` discriminators (the * class-name literal + the Jackson enum), whose intersection is `never` — * indexed access like `RouteMetricCondition['comparator']` resolves to * `never` and silently breaks `Record` exhaustiveness. Until the * OpenAPI shape is fixed upstream, we declare the unions here. * * How to keep this file honest: * 1. Regenerate the schema (`npm run generate-api:live` or `generate-api`). * 2. Search `src/api/schema.d.ts` for the enum unions on the relevant * condition type (e.g. `comparator?: "GT" | "GTE" | …`). * 3. Update the matching union below. * 4. `enums.test.ts` dumps every exported `*_OPTIONS` array as an inline * snapshot — the diff in a PR shows exactly which values / labels moved. * * Fields whose backend type is `String` (agent state, log level, deployment * states, exchange filter status, JVM metric names) aren't in here at all — * springdoc emits them as open-ended strings. Follow-up: add * `@Schema(allowableValues = …)` on the Java record components, then migrate * those string literals into this file too. * * Condition-kind-wide enums (ConditionKind, Severity, TargetKind) are * derived from `schema.d.ts`, because they appear on flat fields that * springdoc types correctly. */ import type { components } from '../../api/schema'; type AlertRuleRequest = components['schemas']['AlertRuleRequest']; type AlertRuleTarget = components['schemas']['AlertRuleTarget']; // Derived — these schema fields are correctly typed (flat, no polymorphism). export type ConditionKind = NonNullable; export type Severity = NonNullable; export type TargetKind = NonNullable; // Manual — schema's polymorphic condition types resolve to `never`. // Mirrors: cameleer-server-core RouteMetric, Comparator, AggregationOp enums // and ExchangeMatchCondition.fireMode. export type RouteMetric = 'ERROR_RATE' | 'AVG_DURATION_MS' | 'P99_LATENCY_MS' | 'THROUGHPUT' | 'ERROR_COUNT'; export type Comparator = 'GT' | 'GTE' | 'LT' | 'LTE' | 'EQ'; export type JvmAggregation = 'MAX' | 'MIN' | 'AVG' | 'LATEST'; export type ExchangeFireMode = 'PER_EXCHANGE' | 'COUNT_IN_WINDOW'; export interface Option { value: T; label: string } /** * Build a dropdown option array from a `Record` label map. * * Declaration order is load-bearing: ES2015+ guarantees `Object.keys` yields * string-keyed entries in insertion order, which is what the user sees. * * Hidden values stay in the `Record` map (so `Record`-exhaustiveness * still pins labels to the union) and in the type `T` (so rules carrying a * hidden value round-trip fine through save/load) — they're simply filtered * out of the visible option array. */ function toOptions(labels: Record, hidden?: readonly T[]): Option[] { const skip: ReadonlySet = new Set(hidden ?? []); return (Object.keys(labels) as T[]) .filter((value) => !skip.has(value)) .map((value) => ({ value, label: labels[value] })); } // --------------------------------------------------------------------------- // Label maps — declaration order = dropdown order. // --------------------------------------------------------------------------- const CONDITION_KIND_LABELS: Record = { ROUTE_METRIC: 'Route metric (error rate, latency, throughput)', EXCHANGE_MATCH: 'Exchange match (specific failures)', AGENT_STATE: 'Agent state (DEAD / STALE)', DEPLOYMENT_STATE: 'Deployment state (FAILED / DEGRADED)', LOG_PATTERN: 'Log pattern (count of matching logs)', JVM_METRIC: 'JVM metric (heap, GC, inflight)', }; const SEVERITY_LABELS: Record = { CRITICAL: 'Critical', WARNING: 'Warning', INFO: 'Info', }; const ROUTE_METRIC_LABELS: Record = { ERROR_RATE: 'Error rate', P99_LATENCY_MS: 'P99 latency (ms)', AVG_DURATION_MS: 'Avg duration (ms)', THROUGHPUT: 'Throughput (msg/s)', ERROR_COUNT: 'Error count', }; const COMPARATOR_LABELS: Record = { GT: '>', GTE: '\u2265', LT: '<', LTE: '\u2264', EQ: '=', }; // Previous UI ordering (MAX, AVG, MIN) preserved; LATEST is declared last // and hidden — see comment on JVM_AGGREGATION_HIDDEN below. const JVM_AGGREGATION_LABELS: Record = { MAX: 'MAX', AVG: 'AVG', MIN: 'MIN', LATEST: 'LATEST', }; const EXCHANGE_FIRE_MODE_LABELS: Record = { PER_EXCHANGE: 'One alert per matching exchange', COUNT_IN_WINDOW: 'Threshold: N matches in window', }; const TARGET_KIND_LABELS: Record = { USER: 'User', GROUP: 'Group', ROLE: 'Role', }; // --------------------------------------------------------------------------- // Hidden values — legal on the wire (saved/loaded rules round-trip fine) but // intentionally not surfaced in dropdowns. Document *why* — silent omission // is exactly what caused the original drift problem. // --------------------------------------------------------------------------- const COMPARATOR_HIDDEN: readonly Comparator[] = [ 'EQ', // Exact equality on floating-point metrics is rarely useful and noisy // — users should pick GT/LT with a sensible threshold instead. ]; const JVM_AGGREGATION_HIDDEN: readonly JvmAggregation[] = [ 'LATEST', // Point-in-time reads belong on a metric dashboard, not an alert // rule — a windowed MAX/MIN/AVG is what you want for alerting. ]; // --------------------------------------------------------------------------- // Exported option arrays (visible in dropdowns). // --------------------------------------------------------------------------- export const CONDITION_KIND_OPTIONS: Option[] = toOptions(CONDITION_KIND_LABELS); export const SEVERITY_OPTIONS: Option[] = toOptions(SEVERITY_LABELS); export const ROUTE_METRIC_OPTIONS: Option[] = toOptions(ROUTE_METRIC_LABELS); export const COMPARATOR_OPTIONS: Option[] = toOptions(COMPARATOR_LABELS, COMPARATOR_HIDDEN); export const JVM_AGGREGATION_OPTIONS: Option[] = toOptions(JVM_AGGREGATION_LABELS, JVM_AGGREGATION_HIDDEN); export const EXCHANGE_FIRE_MODE_OPTIONS: Option[] = toOptions(EXCHANGE_FIRE_MODE_LABELS); export const TARGET_KIND_OPTIONS: Option[] = toOptions(TARGET_KIND_LABELS);