Skip to content

Commit ebe8fbf

Browse files
committed
health rule wizard
1 parent ad3ef6e commit ebe8fbf

50 files changed

Lines changed: 5232 additions & 268 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AGENTS.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,12 @@ Add new API endpoint for zone filtering:
101101
```text
102102
FlowCollector CRD field changed in operator:
103103
1. Follow instructions in README.md#updating-schemas
104-
2. Update web/src/components/forms/config/uiSchema.ts with new field display rules
104+
2. Update web/src/components/forms/flowCollector/uiSchema.ts with new field display rules
105105
3. Update web/src/model/flow-query.ts if query params change
106106
4. Update web/src/components/tabs/netflow-overview/ if overview UI changes
107107
5. Regenerate schemas using scripts/generate-schemas.sh (requires running cluster)
108-
6. Test with both old and new FlowCollector versions
108+
6. After operator DefaultHealthRules changes, run scripts/generate-health-rule-defaults.sh
109+
7. Test with both old and new FlowCollector versions
109110
```
110111

111112
## Repository-Specific Context
@@ -184,7 +185,7 @@ make image-build image-push # Build and push image
184185
- Backend routes: [pkg/server/routes.go](pkg/server/routes.go)
185186
- Backend handlers: [pkg/handler/handlers.go](pkg/handler/handlers.go)
186187
- Table columns: [web/src/utils/columns.ts](web/src/utils/columns.ts)
187-
- UI schema: [web/src/components/forms/config/uiSchema.ts](web/src/components/forms/config/uiSchema.ts)
188+
- UI schema: [web/src/components/forms/flowCollector/uiSchema.ts](web/src/components/forms/flowCollector/uiSchema.ts)
188189
- Sample config: [config/sample-config.yaml](config/sample-config.yaml)
189190

190191
## AI Workflow Example

Makefile

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,13 @@ fmt-backend: ## Run backend go fmt
181181
.PHONY: lint-backend
182182
lint-backend: prereqs ## Lint backend code
183183
@echo "### Linting backend code"
184-
./bin/golangci-lint-${GOLANGCI_LINT_VERSION} run ./...
184+
./bin/golangci-lint-${GOLANGCI_LINT_VERSION} run ./cmd/... ./pkg/...
185185

186186
.PHONY: test-backend
187187
test-backend: ## Test backend using go test
188188
@echo "### Testing backend"
189-
go test ./... -coverpkg=./... -coverprofile cover.out
189+
# Limit to cmd/pkg — scripts/ (e.g. gen-health-rule-defaults) and web/node_modules are not backend packages.
190+
go test ./cmd/... ./pkg/... -coverpkg=./cmd/...,./pkg/... -coverprofile cover.out
190191

191192
##@ Performance Testing
192193

README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,9 +190,17 @@ This console plugin comes with several panels allowing GUI-based configuration f
190190

191191
When you update the operator CRDs, you may have to update some schemas here as well, which contain some rules that drive how forms are displayed. Especially:
192192

193-
- [uiSchema.ts](./web/src/components/forms/config/uiSchema.ts) contains a display-oriented description of every CRD field, including whether or not they should be hidden, or if they have relationships with other fields.
194-
- [< CRD name >-wizard.tsx](./web/src/components/forms/flowCollector-wizard.tsx) contains specific fields to be displayed in wizards.
193+
- [flowCollectorUISchema.ts](./web/src/components/forms/flowCollector/uiSchema.ts) (and related `*UISchema.ts` files in each feature folder) contain a display-oriented description of every CRD field, including whether or not they should be hidden, or if they have relationships with other fields.
194+
- [<feature>/<CRD>-wizard.tsx](./web/src/components/forms/flowCollector/wizard.tsx) contains specific fields to be displayed in wizards.
195195

196196
When a CRD field is added, consider whether you need to update these files.
197197

198198
Additionally, [schemas.ts](./web/pluginToStandaloneMapper/schemas.ts) contains the full CRD schema as JSON, used in tests and in standalone mode, and should also be kept up to date. Use [generate-schemas.sh](./scripts/generate-schemas.sh) to regenerate them (you need a running cluster with the CRDs installed).
199+
200+
Health-rule template defaults (names, modes, first-variant thresholds) are synced from the operator `DefaultHealthRules` into [variantDefaults.ts](./web/src/components/forms/healthRule/variantDefaults.ts). After changing defaults in the operator, regenerate with:
201+
202+
```bash
203+
OPERATOR_PATH=../network-observability-operator ./scripts/generate-health-rule-defaults.sh
204+
```
205+
206+
(`OPERATOR_PATH` defaults to `../network-observability-operator` relative to this repo.)
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
// Package main dumps operator DefaultHealthRules as TypeScript for the console plugin.
2+
// Run via scripts/generate-health-rule-defaults.sh (do not invoke directly unless go.mod replace is set).
3+
package main
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"regexp"
9+
"strings"
10+
"time"
11+
12+
flowsv1beta2 "github.com/netobserv/netobserv-operator/api/flowcollector/v1beta2"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
)
15+
16+
var regTrim = regexp.MustCompile(`([a-zA-Z])(0[a-zA-Z])+`)
17+
18+
func durationTrimmed(d *metav1.Duration) string {
19+
if d == nil {
20+
return ""
21+
}
22+
return regTrim.ReplaceAllString(d.Duration.String(), "$1")
23+
}
24+
25+
func modeOrDefault(m flowsv1beta2.HealthRuleMode) string {
26+
if m == "" {
27+
return string(flowsv1beta2.ModeAlert)
28+
}
29+
return string(m)
30+
}
31+
32+
func tsString(s string) string {
33+
return fmt.Sprintf("%q", s)
34+
}
35+
36+
func writeThresholdFields(b *strings.Builder, indent string, t flowsv1beta2.HealthRuleThresholds) {
37+
if t.Info != "" {
38+
fmt.Fprintf(b, "%sinfo: %s,\n", indent, tsString(t.Info))
39+
}
40+
if t.Warning != "" {
41+
fmt.Fprintf(b, "%swarning: %s,\n", indent, tsString(t.Warning))
42+
}
43+
if t.Critical != "" {
44+
fmt.Fprintf(b, "%scritical: %s,\n", indent, tsString(t.Critical))
45+
}
46+
}
47+
48+
func formatThresholdsInline(t flowsv1beta2.HealthRuleThresholds) string {
49+
parts := []string{}
50+
if t.Info != "" {
51+
parts = append(parts, fmt.Sprintf("info: %s", tsString(t.Info)))
52+
}
53+
if t.Warning != "" {
54+
parts = append(parts, fmt.Sprintf("warning: %s", tsString(t.Warning)))
55+
}
56+
if t.Critical != "" {
57+
parts = append(parts, fmt.Sprintf("critical: %s", tsString(t.Critical)))
58+
}
59+
return "{ " + strings.Join(parts, ", ") + " }"
60+
}
61+
62+
func writeVariant(b *strings.Builder, indent string, v flowsv1beta2.HealthRuleVariant) {
63+
fmt.Fprintf(b, "%s{\n", indent)
64+
fmt.Fprintf(b, "%s thresholds: %s", indent, formatThresholdsInline(v.Thresholds))
65+
hasExtra := v.LowVolumeThreshold != "" || v.GroupBy != "" || v.TrendOffset != nil || v.TrendDuration != nil || v.Mode != nil
66+
if hasExtra {
67+
b.WriteString(",\n")
68+
if v.LowVolumeThreshold != "" {
69+
fmt.Fprintf(b, "%s lowVolumeThreshold: %s,\n", indent, tsString(v.LowVolumeThreshold))
70+
}
71+
if v.GroupBy != "" {
72+
fmt.Fprintf(b, "%s groupBy: %s,\n", indent, tsString(string(v.GroupBy)))
73+
}
74+
if off := durationTrimmed(v.TrendOffset); off != "" {
75+
fmt.Fprintf(b, "%s trendOffset: %s,\n", indent, tsString(off))
76+
}
77+
if dur := durationTrimmed(v.TrendDuration); dur != "" {
78+
fmt.Fprintf(b, "%s trendDuration: %s,\n", indent, tsString(dur))
79+
}
80+
if v.Mode != nil {
81+
fmt.Fprintf(b, "%s mode: %s,\n", indent, tsString(string(*v.Mode)))
82+
}
83+
fmt.Fprintf(b, "%s}", indent)
84+
} else {
85+
b.WriteString(fmt.Sprintf("\n%s}", indent))
86+
}
87+
}
88+
89+
func main() {
90+
date := time.Now().Format(time.RFC1123)
91+
var b strings.Builder
92+
93+
fmt.Fprintf(&b, "// Auto-generated on %s by scripts/generate-health-rule-defaults.sh ; DO NOT EDIT.\n", date)
94+
b.WriteString("// Source: network-observability-operator api/flowcollector/v1beta2.DefaultHealthRules\n")
95+
b.WriteString("// Re-run: OPERATOR_PATH=../network-observability-operator ./scripts/generate-health-rule-defaults.sh\n\n")
96+
97+
b.WriteString("export type VariantFieldPlaceholders = {\n")
98+
b.WriteString(" critical?: string;\n")
99+
b.WriteString(" warning?: string;\n")
100+
b.WriteString(" info?: string;\n")
101+
b.WriteString(" lowVolumeThreshold?: string;\n")
102+
b.WriteString(" trendOffset?: string;\n")
103+
b.WriteString(" trendDuration?: string;\n")
104+
b.WriteString("};\n\n")
105+
106+
b.WriteString("export type HealthRuleDefaultVariant = {\n")
107+
b.WriteString(" thresholds: { info?: string; warning?: string; critical?: string };\n")
108+
b.WriteString(" lowVolumeThreshold?: string;\n")
109+
b.WriteString(" groupBy?: string;\n")
110+
b.WriteString(" trendOffset?: string;\n")
111+
b.WriteString(" trendDuration?: string;\n")
112+
b.WriteString(" mode?: 'Alert' | 'Recording';\n")
113+
b.WriteString("};\n\n")
114+
115+
b.WriteString("export type HealthRuleDefaultSummary = {\n")
116+
b.WriteString(" template: string;\n")
117+
b.WriteString(" /** Effective rule mode (Alert when unset in operator defaults). */\n")
118+
b.WriteString(" mode: 'Alert' | 'Recording';\n")
119+
b.WriteString(" /** All default variants for this template (used when customizing). */\n")
120+
b.WriteString(" variants: HealthRuleDefaultVariant[];\n")
121+
b.WriteString("};\n\n")
122+
123+
b.WriteString("/** Default health-rule templates from the operator (name, mode, variants). */\n")
124+
b.WriteString("export const HEALTH_RULE_DEFAULTS: HealthRuleDefaultSummary[] = [\n")
125+
126+
for _, rule := range flowsv1beta2.DefaultHealthRules {
127+
if len(rule.Variants) == 0 {
128+
fmt.Fprintf(os.Stderr, "default health rule %s has no variants\n", rule.Template)
129+
os.Exit(1)
130+
}
131+
mode := modeOrDefault(rule.Mode)
132+
133+
fmt.Fprintf(&b, " {\n")
134+
fmt.Fprintf(&b, " template: %s,\n", tsString(string(rule.Template)))
135+
fmt.Fprintf(&b, " mode: %s,\n", tsString(mode))
136+
b.WriteString(" variants: [\n")
137+
for i, v := range rule.Variants {
138+
writeVariant(&b, " ", v)
139+
if i < len(rule.Variants)-1 {
140+
b.WriteString(",\n")
141+
} else {
142+
b.WriteString("\n")
143+
}
144+
}
145+
b.WriteString(" ]\n")
146+
b.WriteString(" },\n")
147+
}
148+
b.WriteString("];\n\n")
149+
150+
b.WriteString("/** Primary (first) variant defaults per health-rule template (form placeholders). */\n")
151+
b.WriteString("export const VARIANT_DEFAULT_PLACEHOLDERS: Record<string, VariantFieldPlaceholders> = {\n")
152+
for _, rule := range flowsv1beta2.DefaultHealthRules {
153+
v := rule.Variants[0]
154+
fmt.Fprintf(&b, " %s: {\n", rule.Template)
155+
writeThresholdFields(&b, " ", v.Thresholds)
156+
if v.LowVolumeThreshold != "" {
157+
fmt.Fprintf(&b, " lowVolumeThreshold: %s,\n", tsString(v.LowVolumeThreshold))
158+
}
159+
if off := durationTrimmed(v.TrendOffset); off != "" {
160+
fmt.Fprintf(&b, " trendOffset: %s,\n", tsString(off))
161+
}
162+
if dur := durationTrimmed(v.TrendDuration); dur != "" {
163+
fmt.Fprintf(&b, " trendDuration: %s,\n", tsString(dur))
164+
}
165+
b.WriteString(" },\n")
166+
}
167+
b.WriteString("};\n\n")
168+
169+
b.WriteString("export const getVariantPlaceholders = (template?: string): VariantFieldPlaceholders => {\n")
170+
b.WriteString(" if (!template) {\n")
171+
b.WriteString(" return {};\n")
172+
b.WriteString(" }\n")
173+
b.WriteString(" return VARIANT_DEFAULT_PLACEHOLDERS[template] ?? {};\n")
174+
b.WriteString("};\n\n")
175+
176+
b.WriteString("export const getHealthRuleDefault = (template?: string): HealthRuleDefaultSummary | undefined =>\n")
177+
b.WriteString(" template ? HEALTH_RULE_DEFAULTS.find(d => d.template === template) : undefined;\n\n")
178+
179+
b.WriteString("/** Convert an operator default into a FlowCollector healthRules entry. */\n")
180+
b.WriteString("export const healthRuleDefaultToFLP = (def: HealthRuleDefaultSummary): {\n")
181+
b.WriteString(" template: string;\n")
182+
b.WriteString(" mode: 'Alert' | 'Recording';\n")
183+
b.WriteString(" variants: HealthRuleDefaultVariant[];\n")
184+
b.WriteString("} => ({\n")
185+
b.WriteString(" template: def.template,\n")
186+
b.WriteString(" mode: def.mode,\n")
187+
b.WriteString(" variants: def.variants.map(v => ({\n")
188+
b.WriteString(" thresholds: { ...v.thresholds },\n")
189+
b.WriteString(" ...(v.lowVolumeThreshold ? { lowVolumeThreshold: v.lowVolumeThreshold } : {}),\n")
190+
b.WriteString(" ...(v.groupBy ? { groupBy: v.groupBy } : {}),\n")
191+
b.WriteString(" ...(v.trendOffset ? { trendOffset: v.trendOffset } : {}),\n")
192+
b.WriteString(" ...(v.trendDuration ? { trendDuration: v.trendDuration } : {}),\n")
193+
b.WriteString(" ...(v.mode ? { mode: v.mode } : {})\n")
194+
b.WriteString(" }))\n")
195+
b.WriteString("});\n\n")
196+
197+
b.WriteString("/** Format threshold map for compact UI display (e.g. Manage rules drawer). */\n")
198+
b.WriteString("export const formatThresholdsSummary = (thresholds?: {\n")
199+
b.WriteString(" info?: string | number;\n")
200+
b.WriteString(" warning?: string | number;\n")
201+
b.WriteString(" critical?: string | number;\n")
202+
b.WriteString("}): string => {\n")
203+
b.WriteString(" if (!thresholds) {\n")
204+
b.WriteString(" return '';\n")
205+
b.WriteString(" }\n")
206+
b.WriteString(" const parts: string[] = [];\n")
207+
b.WriteString(" if (thresholds.info != null && thresholds.info !== '') {\n")
208+
b.WriteString(" parts.push(`info: ${thresholds.info}`);\n")
209+
b.WriteString(" }\n")
210+
b.WriteString(" if (thresholds.warning != null && thresholds.warning !== '') {\n")
211+
b.WriteString(" parts.push(`warning: ${thresholds.warning}`);\n")
212+
b.WriteString(" }\n")
213+
b.WriteString(" if (thresholds.critical != null && thresholds.critical !== '') {\n")
214+
b.WriteString(" parts.push(`critical: ${thresholds.critical}`);\n")
215+
b.WriteString(" }\n")
216+
b.WriteString(" return parts.join(', ');\n")
217+
b.WriteString("};\n")
218+
219+
os.Stdout.WriteString(b.String())
220+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env bash
2+
#
3+
# Sync health-rule template defaults from the Network Observability Operator
4+
# (DefaultHealthRules) into the console plugin TypeScript used by forms and
5+
# the Network Health "Manage rules" drawer.
6+
#
7+
# Requires a local checkout of the operator (Go module) — similar to how
8+
# generate-schemas.sh requires a cluster with CRDs installed.
9+
#
10+
# Usage:
11+
# ./scripts/generate-health-rule-defaults.sh
12+
# OPERATOR_PATH=/path/to/network-observability-operator ./scripts/generate-health-rule-defaults.sh
13+
#
14+
set -euo pipefail
15+
16+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
17+
GEN_DIR="${ROOT}/scripts/gen-health-rule-defaults"
18+
OUT="${ROOT}/web/src/components/forms/healthRule/variantDefaults.ts"
19+
20+
OPERATOR_PATH="${OPERATOR_PATH:-${ROOT}/../network-observability-operator}"
21+
22+
if [[ ! -f "${OPERATOR_PATH}/api/flowcollector/v1beta2/flowcollector_defaults.go" ]]; then
23+
echo "error: operator defaults not found at ${OPERATOR_PATH}" >&2
24+
echo "Set OPERATOR_PATH to your network-observability-operator checkout." >&2
25+
exit 1
26+
fi
27+
28+
OPERATOR_PATH="$(cd "${OPERATOR_PATH}" && pwd)"
29+
30+
tmpdir="$(mktemp -d)"
31+
trap 'rm -rf "${tmpdir}"' EXIT
32+
33+
cp "${GEN_DIR}/main.go" "${tmpdir}/main.go"
34+
35+
cat > "${tmpdir}/go.mod" <<EOF
36+
module github.com/netobserv/network-observability-console-plugin/scripts/gen-health-rule-defaults
37+
38+
go 1.26.0
39+
40+
require github.com/netobserv/netobserv-operator v0.0.0
41+
42+
replace github.com/netobserv/netobserv-operator => ${OPERATOR_PATH}
43+
EOF
44+
45+
echo "Generating ${OUT} from ${OPERATOR_PATH} ..."
46+
(
47+
cd "${tmpdir}"
48+
go mod tidy
49+
go run .
50+
) > "${OUT}"
51+
52+
echo "Wrote ${OUT}"

0 commit comments

Comments
 (0)