Publish ITWorx Pulse source
Public source validation / validate (push) Failing after 3m8s

This commit is contained in:
ITWorx Pulse release export
2026-09-03 02:09:19 +02:00
commit bd774932d5
614 changed files with 77116 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
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)
}
+49
View File
@@ -0,0 +1,49 @@
package widgetpreview
import "testing"
func validWidget() map[string]any {
return map[string]any{
"id": "00000000-0000-0000-0000-000000000001",
"type": "timeseries",
"title": "CPU",
"data": map[string]any{"sourceType": "semantic-metric", "metric": "host.cpu.utilization", "range": "1h", "aggregation": "avg", "limit": float64(100)},
"visualization": map[string]any{"decimals": float64(1), "min": float64(0), "max": float64(100), "thresholds": []any{}},
"behavior": map[string]any{"liveIntervalSeconds": float64(5)},
}
}
func TestValidateRejectsUnboundedOrRawConfiguration(t *testing.T) {
widget := validWidget()
widget["data"].(map[string]any)["metric"] = "promql:up"
if fields := Validate(widget); fields["data.metric"] == "" {
t.Fatal("expected raw query rejection")
}
widget["data"].(map[string]any)["limit"] = float64(1001)
if fields := Validate(widget); fields["data.limit"] == "" {
t.Fatal("expected limit rejection")
}
}
func TestValidateRejectsInvertedThresholdRange(t *testing.T) {
widget := validWidget()
widget["visualization"].(map[string]any)["min"] = float64(90)
widget["visualization"].(map[string]any)["max"] = float64(10)
if fields := Validate(widget); fields["visualization.max"] == "" {
t.Fatal("expected min/max rejection")
}
}
func TestPreviewReturnsExplicitStatesAndLimits(t *testing.T) {
widget := validWidget()
widget["data"].(map[string]any)["limit"] = float64(1000)
for _, state := range []string{"loading", "empty", "error", "stale"} {
result, err := Preview(widget, state)
if err != nil {
t.Fatalf("state %s: %v", state, err)
}
if result.State != state || result.Limits.AppliedRows != 1000 || result.Limits.MaxPoints != MaxPoints {
t.Fatalf("unexpected %s result: %+v", state, result)
}
}
}