package widgetpreview import ( "fmt" "math" "strings" ) const ( MaxSeries = 20 MaxPoints = 2000 MaxRows = 1000 ) var validTypes = map[string]bool{ "stat": true, "timeseries": true, "gauge": true, "ranked-list": true, "status-grid": true, "table": true, "heatmap": true, "event-timeline": true, "storage-map": true, "topology": true, "service-matrix": true, "alert-summary": true, "text": true, "query-inspector": true, } var validSources = map[string]bool{ "semantic-metric": true, "inventory": true, "events": true, "alerts": true, "incidents": true, "text": true, } var validStates = map[string]string{ "loading": "Voorbeeld wordt geladen.", "empty": "Geen gegevens binnen de ingestelde scope.", "error": "De bron gaf een fout terug.", "stale": "De gegevens zijn verouderd en moeten opnieuw worden gecontroleerd.", } var validRanges = map[string]bool{"live": true, "15m": true, "1h": true, "6h": true, "24h": true, "7d": true} var validAggregations = map[string]bool{"avg": true, "min": true, "max": true, "sum": true, "last": true} type InvalidConfig struct{ Fields map[string]string } func (e InvalidConfig) Error() string { return "widget configuration is invalid" } type Limits struct { MaxSeries int `json:"maxSeries"` MaxPoints int `json:"maxPoints"` MaxRows int `json:"maxRows"` RequestedRows int `json:"requestedRows"` AppliedRows int `json:"appliedRows"` } type Response struct { Type string `json:"type"` Title string `json:"title"` State string `json:"state"` Message string `json:"message"` Sample map[string]any `json:"sample"` Limits Limits `json:"limits"` } func Preview(widget map[string]any, state string) (Response, error) { fields := Validate(widget) if len(fields) > 0 { return Response{}, InvalidConfig{Fields: fields} } if state == "" { state = "loading" } message, ok := validStates[state] if !ok { return Response{}, InvalidConfig{Fields: map[string]string{"state": "Kies loading, empty, error of stale."}} } data, _ := object(widget["data"]) requested := intValue(data["limit"], 100) applied := requested if applied > MaxRows { applied = MaxRows } return Response{ Type: stringValue(widget["type"]), Title: stringValue(widget["title"]), State: state, Message: message, Sample: map[string]any{"status": state, "text": message}, Limits: Limits{MaxSeries: MaxSeries, MaxPoints: MaxPoints, MaxRows: MaxRows, RequestedRows: requested, AppliedRows: applied}, }, nil } func Validate(widget map[string]any) map[string]string { fields := make(map[string]string) if id := stringValue(widget["id"]); id == "" { fields["id"] = "Een widget-id is verplicht." } typ := stringValue(widget["type"]) if !validTypes[typ] { fields["type"] = "Dit widgettype wordt niet ondersteund." } title := stringValue(widget["title"]) if title == "" { fields["title"] = "Een titel is verplicht." } else if len([]rune(title)) > 120 { fields["title"] = "De titel mag maximaal 120 tekens bevatten." } data, ok := object(widget["data"]) if !ok { fields["data"] = "Dataconfiguratie is verplicht." } else { source := stringValue(data["sourceType"]) if !validSources[source] { fields["data.sourceType"] = "Kies een ondersteund brontype." } if source == "semantic-metric" { metric := stringValue(data["metric"]) if metric == "" { fields["data.metric"] = "Een semantische metric is verplicht." } if strings.Contains(strings.ToLower(metric), "promql") || strings.Contains(strings.ToLower(metric), "query=") { fields["data.metric"] = "Gebruik een semantische metric; vrije PromQL is niet toegestaan." } } if value := stringValue(data["range"]); value != "" && !validRanges[value] && !strings.HasPrefix(value, "$") { fields["data.range"] = "Kies een begrensde periode." } if value := stringValue(data["aggregation"]); value != "" && !validAggregations[value] { fields["data.aggregation"] = "Kies een ondersteunde aggregatie." } if number, ok := numberValue(data["limit"]); ok && (number < 1 || number > MaxRows || math.Trunc(number) != number) { fields["data.limit"] = fmt.Sprintf("De limiet moet een geheel getal tussen 1 en %d zijn.", MaxRows) } } if visualization, ok := object(widget["visualization"]); ok { if number, ok := numberValue(visualization["decimals"]); ok && (number < 0 || number > 6 || math.Trunc(number) != number) { fields["visualization.decimals"] = "Decimalen moeten tussen 0 en 6 liggen." } minimum, hasMin := numberValue(visualization["min"]) maximum, hasMax := numberValue(visualization["max"]) if hasMin && hasMax && minimum > maximum { fields["visualization.max"] = "Maximum moet groter of gelijk aan minimum zijn." } if thresholds, ok := visualization["thresholds"].([]any); ok && len(thresholds) > 10 { fields["visualization.thresholds"] = "Er zijn maximaal 10 drempels toegestaan." } } if behavior, ok := object(widget["behavior"]); ok { if number, ok := numberValue(behavior["liveIntervalSeconds"]); ok && (number < 1 || number > 300 || math.Trunc(number) != number) { fields["behavior.liveIntervalSeconds"] = "Verversing moet tussen 1 en 300 seconden liggen." } } return fields } func object(value any) (map[string]any, bool) { result, ok := value.(map[string]any) return result, ok } func stringValue(value any) string { result, _ := value.(string) return strings.TrimSpace(result) } func numberValue(value any) (float64, bool) { switch result := value.(type) { case float64: return result, true case float32: return float64(result), true case int: return float64(result), true case int64: return float64(result), true default: return 0, false } } func intValue(value any, fallback int) int { number, ok := numberValue(value) if !ok || number < 1 || math.Trunc(number) != number { return fallback } return int(number) }