diff --git a/go.mod b/go.mod index a35cc4757..ec639eb44 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,14 @@ module github.com/newrelic/terraform-provider-newrelic/v3 go 1.23.6 +replace github.com/newrelic/newrelic-client-go/v2 => github.com/newrelic/newrelic-client-go/v2 v2.70.3-0.20251003115336-c6f46391af16 + require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 github.com/mitchellh/go-homedir v1.1.0 github.com/newrelic/go-agent/v3 v3.30.0 github.com/newrelic/go-insights v1.0.3 - github.com/newrelic/newrelic-client-go/v2 v2.70.2 + github.com/newrelic/newrelic-client-go/v2 v2.70.3 github.com/stretchr/testify v1.9.0 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 ) diff --git a/go.sum b/go.sum index 99ec357ec..826eb13ac 100644 --- a/go.sum +++ b/go.sum @@ -270,8 +270,8 @@ github.com/newrelic/go-agent/v3 v3.30.0 h1:ZXHCT/Cot4iIPwcegCZURuRQOsfmGA6wilW+S github.com/newrelic/go-agent/v3 v3.30.0/go.mod h1:9utrgxlSryNqRrTvII2XBL+0lpofXbqXApvVWPpbzUg= github.com/newrelic/go-insights v1.0.3 h1:zSNp1CEZnXktzSIEsbHJk8v6ZihdPFP2WsO/fzau3OQ= github.com/newrelic/go-insights v1.0.3/go.mod h1:A20BoT8TNkqPGX2nS/Z2fYmKl3Cqa3iKZd4whzedCY4= -github.com/newrelic/newrelic-client-go/v2 v2.70.2 h1:G1SrAQhjH2bsWZ4FbO/DXqfOqvUsb1e0LrY/f2rE+pc= -github.com/newrelic/newrelic-client-go/v2 v2.70.2/go.mod h1:P6rXSHPtayzr50+UEYvvjzYPiADv7w2SqeyKz0z5HkU= +github.com/newrelic/newrelic-client-go/v2 v2.70.3-0.20251003115336-c6f46391af16 h1:PSUkHEUry211tQx94G3F2Sy5o+Nc5VSoKus/6Oy8RpY= +github.com/newrelic/newrelic-client-go/v2 v2.70.3-0.20251003115336-c6f46391af16/go.mod h1:P6rXSHPtayzr50+UEYvvjzYPiADv7w2SqeyKz0z5HkU= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= diff --git a/newrelic/resource_newrelic_one_dashboard.go b/newrelic/resource_newrelic_one_dashboard.go index 5b1a2efe4..cb05177ef 100644 --- a/newrelic/resource_newrelic_one_dashboard.go +++ b/newrelic/resource_newrelic_one_dashboard.go @@ -572,6 +572,14 @@ func dashboardWidgetBillboardSchemaElem() *schema.Resource { s["billboard_settings"] = dashboardWidgetBillboardSettingsSchema() + s["thresholds_with_series_overrides"] = &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Description: "Thresholds with series overrides configuration for the billboard widget.", + Elem: dashboardWidgetThresholdsWithSeriesOverridesSchemaElem(), + } + return &schema.Resource{ Schema: s, } @@ -1119,3 +1127,77 @@ func dashboardWidgetBillboardSettingsSchema() *schema.Schema { }, } } + +func dashboardWidgetThresholdsWithSeriesOverridesSchemaElem() *schema.Resource { + return &schema.Resource{ + Schema: map[string]*schema.Schema{ + "thresholds": { + Type: schema.TypeList, + Optional: true, + Description: "Thresholds for the billboard widget.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "from": { + Type: schema.TypeFloat, + Optional: true, + Description: "The number from which the range starts in thresholds.", + }, + "to": { + Type: schema.TypeFloat, + Optional: true, + Description: "The number at which the range ends in thresholds.", + }, + "severity": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.SUCCESS), + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.WARNING), + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.UNAVAILABLE), + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.SEVERE), + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.CRITICAL), + }, false), + Description: "Severity of the threshold, which would reflect in the widget.", + }, + }, + }, + }, + "series_overrides": { + Type: schema.TypeList, + Optional: true, + Description: "Series overrides for thresholds.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "from": { + Type: schema.TypeFloat, + Optional: true, + Description: "The number from which the range starts in thresholds.", + }, + "to": { + Type: schema.TypeFloat, + Optional: true, + Description: "The number at which the range ends in thresholds.", + }, + "series_name": { + Type: schema.TypeString, + Optional: true, + Description: "Name of the series to which the threshold would be applied.", + }, + "severity": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.SUCCESS), + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.WARNING), + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.UNAVAILABLE), + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.SEVERE), + string(dashboards.DashboardLineTableWidgetsAlertSeverityTypes.CRITICAL), + }, false), + Description: "Severity of the threshold, which would reflect in the widget.", + }, + }, + }, + }, + }, + } +} diff --git a/newrelic/resource_newrelic_one_dashboard_test.go b/newrelic/resource_newrelic_one_dashboard_test.go index 7b364ee39..21654961c 100644 --- a/newrelic/resource_newrelic_one_dashboard_test.go +++ b/newrelic/resource_newrelic_one_dashboard_test.go @@ -1545,6 +1545,225 @@ resource "newrelic_one_dashboard" "bar" { }` } +// TestAccNewRelicOneDashboard_BillboardThresholdsWithSeriesOverrides tests thresholds_with_series_overrides functionality +func TestAccNewRelicOneDashboard_BillboardThresholdsWithSeriesOverrides(t *testing.T) { + rName := fmt.Sprintf("tf-test-%s", acctest.RandString(5)) + rWidgetName := fmt.Sprintf("tf-test-widget-%s", acctest.RandString(5)) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckNewRelicOneDashboardDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCheckNewRelicOneDashboardConfig_BillboardWithThresholdsWithSeriesOverrides(rName, rWidgetName), + Check: resource.ComposeTestCheckFunc( + testAccCheckNewRelicOneDashboardExists("newrelic_one_dashboard.bar", 0), + ), + }, + { + Config: testAccCheckNewRelicOneDashboardConfig_BillboardWithThresholdsWithSeriesOverridesUpdated(rName, rWidgetName), + Check: resource.ComposeTestCheckFunc( + testAccCheckNewRelicOneDashboardExists("newrelic_one_dashboard.bar", 0), + ), + }, + // Import + { + ResourceName: "newrelic_one_dashboard.bar", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +// TestAccNewRelicOneDashboard_BillboardThresholdsWithSeriesOverridesValidation tests validation for thresholds_with_series_overrides +func TestAccNewRelicOneDashboard_BillboardThresholdsWithSeriesOverridesValidation(t *testing.T) { + rName := fmt.Sprintf("tf-test-%s", acctest.RandString(5)) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + // Test empty thresholds_with_series_overrides block + { + Config: testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptyThresholdsWithSeriesOverrides(rName), + ExpectError: regexp.MustCompile(`thresholds_with_series_overrides.*must contain at least one.*thresholds.*or.*series_overrides.*block with content`), + }, + // Test empty threshold block + { + Config: testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptyThresholdBlock(rName), + ExpectError: regexp.MustCompile(`threshold.*thresholds block cannot be null`), + }, + // Test empty series override block + { + Config: testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptySeriesOverrideBlock(rName), + ExpectError: regexp.MustCompile(`series_override.*series_overrides block cannot be null`), + }, + }, + }) +} + +// testAccCheckNewRelicOneDashboardConfig_BillboardWithThresholdsWithSeriesOverrides generates billboard with thresholds_with_series_overrides +func testAccCheckNewRelicOneDashboardConfig_BillboardWithThresholdsWithSeriesOverrides(dashboardName string, widgetName string) string { + return ` +resource "newrelic_one_dashboard" "bar" { + name = "` + dashboardName + `" + + page { + name = "` + dashboardName + `" + + widget_billboard { + title = "` + widgetName + `" + row = 1 + column = 1 + nrql_query { + query = "SELECT count(*) FROM ProcessSample FACET hostname SINCE 30 MINUTES AGO TIMESERIES" + } + thresholds_with_series_overrides { + thresholds { + from = 0 + to = 100 + severity = "warning" + } + thresholds { + from = 100 + to = 200 + severity = "critical" + } + series_overrides { + from = 50 + to = 150 + series_name = "test-1" + severity = "critical" + } + series_overrides { + from = 25 + to = 75 + series_name = "test-2" + severity = "warning" + } + } + } + } +}` +} + +// testAccCheckNewRelicOneDashboardConfig_BillboardWithThresholdsWithSeriesOverridesUpdated generates updated billboard with thresholds_with_series_overrides +func testAccCheckNewRelicOneDashboardConfig_BillboardWithThresholdsWithSeriesOverridesUpdated(dashboardName string, widgetName string) string { + return ` +resource "newrelic_one_dashboard" "bar" { + name = "` + dashboardName + `" + + page { + name = "` + dashboardName + `" + + widget_billboard { + title = "` + widgetName + `" + row = 1 + column = 1 + nrql_query { + query = "SELECT average(cpuPercent) FROM ProcessSample FACET hostname SINCE 30 MINUTES AGO TIMESERIES" + } + thresholds_with_series_overrides { + thresholds { + from = 0 + to = 80 + severity = "warning" + } + thresholds { + from = 80 + to = 100 + severity = "critical" + } + series_overrides { + from = 60 + to = 90 + series_name = "test" + severity = "critical" + } + } + } + } +}` +} + +// testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptyThresholdsWithSeriesOverrides generates billboard with empty thresholds_with_series_overrides +func testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptyThresholdsWithSeriesOverrides(dashboardName string) string { + return ` +resource "newrelic_one_dashboard" "bar" { + name = "` + dashboardName + `" + + page { + name = "` + dashboardName + `" + + widget_billboard { + title = "test widget" + row = 1 + column = 1 + nrql_query { + query = "SELECT count(*) FROM ProcessSample SINCE 30 MINUTES AGO TIMESERIES" + } + thresholds_with_series_overrides { + # Empty block - should trigger validation error + } + } + } +}` +} + +// testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptyThresholdBlock generates billboard with empty threshold block +func testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptyThresholdBlock(dashboardName string) string { + return ` +resource "newrelic_one_dashboard" "bar" { + name = "` + dashboardName + `" + + page { + name = "` + dashboardName + `" + + widget_billboard { + title = "test widget" + row = 1 + column = 1 + nrql_query { + query = "SELECT count(*) FROM ProcessSample SINCE 30 MINUTES AGO TIMESERIES" + } + thresholds_with_series_overrides { + thresholds { + # Empty threshold block - should trigger validation error + } + } + } + } +}` +} + +// testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptySeriesOverrideBlock generates billboard with empty series override block +func testAccCheckNewRelicOneDashboardConfig_BillboardWithEmptySeriesOverrideBlock(dashboardName string) string { + return ` +resource "newrelic_one_dashboard" "bar" { + name = "` + dashboardName + `" + + page { + name = "` + dashboardName + `" + + widget_billboard { + title = "test widget" + row = 1 + column = 1 + nrql_query { + query = "SELECT count(*) FROM ProcessSample FACET hostname SINCE 30 MINUTES AGO TIMESERIES" + } + thresholds_with_series_overrides { + series_overrides { + # Empty series override block - should trigger validation error + } + } + } + } +}` +} + // testAccCheckNewRelicOneDashboardExists fetches the dashboard back, with an optional sleep time // used when we know the async nature of the API will mess with consistent testing. func testAccCheckNewRelicOneDashboardExists(name string, sleepSeconds int) resource.TestCheckFunc { diff --git a/newrelic/structures_newrelic_one_dashboard.go b/newrelic/structures_newrelic_one_dashboard.go index 90e91874b..ce317bf7c 100644 --- a/newrelic/structures_newrelic_one_dashboard.go +++ b/newrelic/structures_newrelic_one_dashboard.go @@ -286,12 +286,20 @@ func expandDashboardPageInput(d *schema.ResourceData, pages []interface{}, meta return nil, err } - // Set thresholds - rawConfiguration.Thresholds = expandDashboardBillboardWidgetConfigurationInput(d, v.(map[string]interface{}), meta, pageIndex, widgetIndex) + // Set thresholds (old logic) + // rawConfiguration.Thresholds = expandDashboardBillboardWidgetConfigurationInput(d, v.(map[string]interface{}), meta, pageIndex, widgetIndex) + // Set data formatting rawConfiguration.DataFormat = expandDashboardTableWidgetConfigDataFormatInput(v.(map[string]interface{})) // Set billboard settings rawConfiguration.BillboardSettings = expandDashboardWidgetConfigurationBillboardSettingsInput(d, pageIndex, widgetIndex) + // Set thresholds with series overrides + rawConfiguration.ThresholdsWithSeriesOverrides = expandDashboardBillboardWidgetThresholdsWithSeriesOverridesInput(d, pageIndex, widgetIndex) + + // Set thresholds from warning and critical attributes + if rawConfiguration.ThresholdsWithSeriesOverrides == nil { + rawConfiguration.ThresholdsWithSeriesOverrides = expandDashboardBillboardWidgetThresholdsFromWarningCritical(d, pageIndex, widgetIndex) + } widget.RawConfiguration, err = json.Marshal(rawConfiguration) if err != nil { @@ -538,6 +546,209 @@ func expandDashboardBillboardWidgetConfigurationInput(d *schema.ResourceData, i return thresholds } +func expandDashboardBillboardWidgetThresholdsWithSeriesOverridesInput(d *schema.ResourceData, pageIndex int, widgetIndex int) *dashboards.DashboardBillboardWidgetThresholdsWithSeriesOverrides { + thresholdsPath := fmt.Sprintf("page.%d.widget_billboard.%d.thresholds_with_series_overrides", pageIndex, widgetIndex) + if thresholdsWithSeriesOverridesData, ok := d.GetOk(thresholdsPath); ok && len(thresholdsWithSeriesOverridesData.([]interface{})) > 0 { + thresholdsWithSeriesOverridesList := thresholdsWithSeriesOverridesData.([]interface{}) + if len(thresholdsWithSeriesOverridesList) > 0 && thresholdsWithSeriesOverridesList[0] != nil { + thresholdsWithSeriesOverridesMap := thresholdsWithSeriesOverridesList[0].(map[string]interface{}) + + var thresholdsWithSeriesOverrides dashboards.DashboardBillboardWidgetThresholdsWithSeriesOverrides + + // Handle thresholds + if thresholdsData, ok := thresholdsWithSeriesOverridesMap["thresholds"]; ok && len(thresholdsData.([]interface{})) > 0 { + thresholdsList := thresholdsData.([]interface{}) + thresholds := make([]dashboards.DashboardBillboardWidgetThreshold, len(thresholdsList)) + + for i, threshold := range thresholdsList { + if threshold != nil { + thresholdMap := threshold.(map[string]interface{}) + var thresholdObj dashboards.DashboardBillboardWidgetThreshold + + if from, ok := thresholdMap["from"]; ok { + thresholdObj.From = from.(float64) + } + if to, ok := thresholdMap["to"]; ok { + thresholdObj.To = to.(float64) + } + if severity, ok := thresholdMap["severity"]; ok { + thresholdObj.Severity = severity.(string) + } + + thresholds[i] = thresholdObj + } + } + + thresholdsWithSeriesOverrides.Thresholds = thresholds + } + + // Handle series overrides + if seriesOverridesData, ok := thresholdsWithSeriesOverridesMap["series_overrides"]; ok && len(seriesOverridesData.([]interface{})) > 0 { + seriesOverridesList := seriesOverridesData.([]interface{}) + seriesOverrides := make([]dashboards.DashboardBillboardWidgetThresholdSeriesOverride, len(seriesOverridesList)) + + for i, override := range seriesOverridesList { + if override != nil { + overrideMap := override.(map[string]interface{}) + var seriesOverride dashboards.DashboardBillboardWidgetThresholdSeriesOverride + + if from, ok := overrideMap["from"]; ok { + seriesOverride.From = from.(float64) + } + if to, ok := overrideMap["to"]; ok { + seriesOverride.To = to.(float64) + } + if seriesName, ok := overrideMap["series_name"]; ok { + seriesOverride.SeriesName = seriesName.(string) + } + if severity, ok := overrideMap["severity"]; ok { + seriesOverride.Severity = severity.(string) + } + + seriesOverrides[i] = seriesOverride + } + } + + thresholdsWithSeriesOverrides.SeriesOverrides = seriesOverrides + } + + return &thresholdsWithSeriesOverrides + } + } + + return nil +} + +func expandDashboardBillboardWidgetThresholdsFromWarningCritical(d *schema.ResourceData, pageIndex int, widgetIndex int) *dashboards.DashboardBillboardWidgetThresholdsWithSeriesOverrides { + warningPath := fmt.Sprintf("page.%d.widget_billboard.%d.warning", pageIndex, widgetIndex) + criticalPath := fmt.Sprintf("page.%d.widget_billboard.%d.critical", pageIndex, widgetIndex) + + warningData, warningExists := d.GetOk(warningPath) + criticalData, criticalExists := d.GetOk(criticalPath) + + // If neither warning nor critical exists, return nil + if !warningExists && !criticalExists { + return nil + } + + var warningValue, criticalValue *float64 + + // Parse warning value if it exists and is not empty + if warningExists { + warningStr := warningData.(string) + if warningStr != "" { + if parsed, err := strconv.ParseFloat(warningStr, 64); err == nil { + warningValue = &parsed + } + } + } + + // Parse critical value if it exists and is not empty + if criticalExists { + criticalStr := criticalData.(string) + if criticalStr != "" { + if parsed, err := strconv.ParseFloat(criticalStr, 64); err == nil { + criticalValue = &parsed + } + } + } + + // If both values are nil (empty strings), return nil + if warningValue == nil && criticalValue == nil { + return nil + } + + var thresholds []dashboards.DashboardBillboardWidgetThreshold + + // Case 1: Only warning exists + if warningValue != nil && criticalValue == nil { + thresholds = []dashboards.DashboardBillboardWidgetThreshold{ + { + To: *warningValue, + Severity: "success", + }, + { + From: *warningValue, + Severity: "warning", + }, + } + } + + // Case 2: Only critical exists + if criticalValue != nil && warningValue == nil { + thresholds = []dashboards.DashboardBillboardWidgetThreshold{ + { + To: *criticalValue, + Severity: "success", + }, + { + From: *criticalValue, + Severity: "critical", + }, + } + } + + // Case 3, 4 & 5: Both warning and critical exist + if warningValue != nil && criticalValue != nil { + if *warningValue < *criticalValue { + // Case 3: warning < critical + thresholds = []dashboards.DashboardBillboardWidgetThreshold{ + { + To: *warningValue, + Severity: "success", + }, + { + From: *warningValue, + To: *criticalValue, + Severity: "warning", + }, + { + From: *criticalValue, + Severity: "critical", + }, + } + } else if *warningValue == *criticalValue { + // Case 4: warning == critical + thresholds = []dashboards.DashboardBillboardWidgetThreshold{ + { + To: *warningValue, + Severity: "warning", + }, + { + From: *warningValue, + To: *criticalValue, + Severity: "critical", + }, + { + From: *criticalValue, + Severity: "success", + }, + } + } else { + // Case 5: critical < warning + thresholds = []dashboards.DashboardBillboardWidgetThreshold{ + { + To: *criticalValue, + Severity: "critical", + }, + { + From: *criticalValue, + To: *warningValue, + Severity: "warning", + }, + { + From: *warningValue, + Severity: "success", + }, + } + } + } + + return &dashboards.DashboardBillboardWidgetThresholdsWithSeriesOverrides{ + Thresholds: thresholds, + } +} + func expandDashboardLineWidgetConfigurationThresholdInput(d *schema.ResourceData, pageIndex int, widgetIndex int) dashboards.DashboardLineWidgetThresholdInput { // initialize a root object of the DashboardLineWidgetThresholdInput class, which is expected to include IsLabelVisible and Thresholds var lineWidgetThresholdsRoot dashboards.DashboardLineWidgetThresholdInput @@ -1420,6 +1631,10 @@ func flattenDashboardWidget(in *entities.DashboardWidget, pageGUID string) (stri out["billboard_settings"] = flattenDashboardWidgetBillboardSettings(rawCfg.BillboardSettings) } + if rawCfg.ThresholdsWithSeriesOverrides != nil { + out["thresholds_with_series_overrides"] = flattenDashboardBillboardWidgetThresholdsWithSeriesOverrides(rawCfg.ThresholdsWithSeriesOverrides) + } + case "viz.bullet": widgetType = "widget_bullet" out["limit"] = rawCfg.Limit @@ -1828,7 +2043,12 @@ func validateDashboardArguments(ctx context.Context, d *schema.ResourceDiff, met validateWidgetDataFormatterStructure(d, &errorsList, "widget_table") validateWidgetDataFormatterStructure(d, &errorsList, "widget_billboard") - // add any other validation functions here + + // Add validation for conflicting billboard threshold configurations + validateBillboardLegacyThresholdConflicts(d, &errorsList) + + // Add validation for thresholds_with_series_overrides + validateThresholdsWithSeriesOverrides(d, &errorsList) if len(errorsList) == 0 { return nil @@ -1993,6 +2213,113 @@ func validateWidgetDataFormatterStructure(d *schema.ResourceDiff, errorsList *[] } } +func validateThresholdsWithSeriesOverrides(d *schema.ResourceDiff, errorsList *[]string) { + _, pagesListObtained := d.GetChange("page") + pages := pagesListObtained.([]interface{}) + + for pageIndex, p := range pages { + page := p.(map[string]interface{}) + widgets, widgetOk := page["widget_billboard"] + if widgetOk { + for widgetIndex, w := range widgets.([]interface{}) { + widget := w.(map[string]interface{}) + thresholdsWithOverrides, thresholdsOk := widget["thresholds_with_series_overrides"] + if thresholdsOk && len(thresholdsWithOverrides.([]interface{})) > 0 { + for _, t := range thresholdsWithOverrides.([]interface{}) { + if t == nil { + *errorsList = append(*errorsList, fmt.Sprintf("thresholds_with_series_overrides in page %d widget_billboard %d cannot be empty - it must contain at least one 'thresholds' or 'series_overrides' block with content", pageIndex, widgetIndex)) + continue + } + + thresholdBlock := t.(map[string]interface{}) + + //hasValidThresholds := false + //hasValidSeriesOverrides := false + + // Check thresholds block + if thresholds, ok := thresholdBlock["thresholds"]; ok { + thresholdsList := thresholds.([]interface{}) + if len(thresholdsList) > 0 { + for thresholdIndex, threshold := range thresholdsList { + if threshold == nil { + *errorsList = append(*errorsList, fmt.Sprintf("threshold %d in page %d widget_billboard %d thresholds block cannot be null", thresholdIndex, pageIndex, widgetIndex)) + continue + } + + //thresholdMap := threshold.(map[string]interface{}) + //if hasThresholdContent(thresholdMap) { + // hasValidThresholds = true + //} else { + // *errorsList = append(*errorsList, fmt.Sprintf("threshold %d in page %d widget_billboard %d thresholds block is empty - it must contain at least one field: from, to, or severity", thresholdIndex, pageIndex, widgetIndex)) + //} + } + } + } + + // Check series_overrides block + if seriesOverrides, ok := thresholdBlock["series_overrides"]; ok { + seriesOverridesList := seriesOverrides.([]interface{}) + if len(seriesOverridesList) > 0 { + for overrideIndex, override := range seriesOverridesList { + if override == nil { + *errorsList = append(*errorsList, fmt.Sprintf("series_override %d in page %d widget_billboard %d series_overrides block cannot be null", overrideIndex, pageIndex, widgetIndex)) + continue + } + + //overrideMap := override.(map[string]interface{}) + //if hasSeriesOverrideContent(overrideMap) { + // hasValidSeriesOverrides = true + //} else { + // *errorsList = append(*errorsList, fmt.Sprintf("series_override %d in page %d widget_billboard %d series_overrides block is empty - it must contain at least one field: from, to, series_name, or severity", overrideIndex, pageIndex, widgetIndex)) + //} + } + } + } + + // Check if the entire thresholds_with_series_overrides block has no valid content + //if !hasValidThresholds && !hasValidSeriesOverrides { + // *errorsList = append(*errorsList, fmt.Sprintf("thresholds_with_series_overrides in page %d widget_billboard %d must contain at least one valid 'thresholds' or 'series_overrides' block with content", pageIndex, widgetIndex)) + //} + } + } + } + } + } +} + +// Helper function to check if a threshold has content +func hasThresholdContent(thresholdMap map[string]interface{}) bool { + if _, ok := thresholdMap["from"]; ok { + return true + } + if _, ok := thresholdMap["to"]; ok { + return true + } + if _, ok := thresholdMap["severity"]; ok { + return true + } + + return false +} + +// Helper function to check if a series override has content +func hasSeriesOverrideContent(overrideMap map[string]interface{}) bool { + if _, ok := overrideMap["from"]; ok { + return true + } + if _, ok := overrideMap["to"]; ok { + return true + } + if _, ok := overrideMap["series_name"]; ok { + return true + } + if _, ok := overrideMap["severity"]; ok { + return true + } + + return false +} + func expandDashboardWidgetConfigurationTooltipInput(d *schema.ResourceData, pageIndex int, widgetIndex int) *dashboards.DashboardWidgetTooltip { tooltipPath := fmt.Sprintf("page.%d.widget_line.%d.tooltip", pageIndex, widgetIndex) @@ -2159,3 +2486,102 @@ func flattenDashboardWidgetBillboardSettings(billboardSettings *dashboards.Dashb billboardSettingsFetchedInterface = append(billboardSettingsFetchedInterface, billboardSettingsFetched) return billboardSettingsFetchedInterface } + +func flattenDashboardBillboardWidgetThresholdsWithSeriesOverrides(thresholdsWithSeriesOverrides *dashboards.DashboardBillboardWidgetThresholdsWithSeriesOverrides) []interface{} { + if thresholdsWithSeriesOverrides == nil { + return nil + } + + var thresholdsFetched = make(map[string]interface{}) + var thresholdsFetchedInterface []interface{} + + // Handle thresholds + if len(thresholdsWithSeriesOverrides.Thresholds) > 0 { + thresholdsList := make([]interface{}, len(thresholdsWithSeriesOverrides.Thresholds)) + for i, threshold := range thresholdsWithSeriesOverrides.Thresholds { + thresholdFetched := make(map[string]interface{}) + + thresholdFetched["from"] = threshold.From + thresholdFetched["to"] = threshold.To + thresholdFetched["severity"] = threshold.Severity + + thresholdsList[i] = thresholdFetched + } + + thresholdsFetched["thresholds"] = thresholdsList + } + + // Handle series overrides + if len(thresholdsWithSeriesOverrides.SeriesOverrides) > 0 { + seriesOverridesList := make([]interface{}, len(thresholdsWithSeriesOverrides.SeriesOverrides)) + for i, override := range thresholdsWithSeriesOverrides.SeriesOverrides { + overrideFetched := make(map[string]interface{}) + + overrideFetched["from"] = override.From + overrideFetched["to"] = override.To + overrideFetched["series_name"] = override.SeriesName + overrideFetched["severity"] = override.Severity + + seriesOverridesList[i] = overrideFetched + } + thresholdsFetched["series_overrides"] = seriesOverridesList + } + + thresholdsFetchedInterface = append(thresholdsFetchedInterface, thresholdsFetched) + return thresholdsFetchedInterface +} + +func validateBillboardLegacyThresholdConflicts(d *schema.ResourceDiff, errorsList *[]string) { + _, pagesListObtained := d.GetChange("page") + pages := pagesListObtained.([]interface{}) + + for pageIndex, p := range pages { + page := p.(map[string]interface{}) + widgets, widgetOk := page["widget_billboard"] + if widgetOk { + for widgetIndex, w := range widgets.([]interface{}) { + widget := w.(map[string]interface{}) + + // Check if legacy threshold attributes exist + hasLegacyThresholds := false + hasNewThresholdsBlock := false + + // Check for legacy warning/critical attributes + if _, warningOk := widget["warning"]; warningOk && widget["warning"] != "" { + hasLegacyThresholds = true + } + + if _, criticalOk := widget["critical"]; criticalOk && widget["critical"] != "" { + hasLegacyThresholds = true + } + + // Check if thresholds_with_series_overrides contains a thresholds block + if thresholdsWithSeriesOverrides, thresholdsWithSeriesOverridesOk := widget["thresholds_with_series_overrides"]; thresholdsWithSeriesOverridesOk { + if thresholdsWithSeriesOverridesList, ok := thresholdsWithSeriesOverrides.([]interface{}); ok && len(thresholdsWithSeriesOverridesList) > 0 { + for _, thresholdsWithSeriesOverridesBlock := range thresholdsWithSeriesOverridesList { + if thresholdsWithSeriesOverridesBlock != nil { + thresholdsWithSeriesOverridesBlockMap := thresholdsWithSeriesOverridesBlock.(map[string]interface{}) + + // Check if thresholds block exists within thresholds_with_series_overrides + if newThresholdsData, newThresholdsOk := thresholdsWithSeriesOverridesBlockMap["thresholds"]; newThresholdsOk { + if newThresholdsList, ok := newThresholdsData.([]interface{}); ok && len(newThresholdsList) > 0 { + hasNewThresholdsBlock = true + break + } + } + } + } + } + } + + // Report conflict if both legacy warning/critical and new thresholds block are present + if hasLegacyThresholds && hasNewThresholdsBlock { + *errorsList = append(*errorsList, fmt.Sprintf( + "Conflicting threshold configurations in page %d widget_billboard %d: cannot use both legacy threshold attributes (warning/critical) and thresholds block within thresholds_with_series_overrides at the same time.", + pageIndex, widgetIndex, + )) + } + } + } + } +} diff --git a/website/docs/r/one_dashboard.html.markdown b/website/docs/r/one_dashboard.html.markdown index 9e331f9b8..ac49c914c 100644 --- a/website/docs/r/one_dashboard.html.markdown +++ b/website/docs/r/one_dashboard.html.markdown @@ -346,6 +346,7 @@ Each widget type supports an additional set of arguments: * `warning` - (Optional) Threshold above which the displayed value will be styled with a yellow color. * `billboard_settings` - (Optional) A nested block that describes billboard specific settings. See [Nested billboard\_settings blocks](#nested-billboard_settings-blocks) below for details. * `data_format` - (Optional) A nested block that describes data format. See [Nested data_format blocks](#nested-data_format-blocks) below for details. + * `thresholds_with_series_overrides` - (Optional) A nested block that describes threshold and series-overrides configuration. See [Nested thresholds\_with\_series\_overrides blocks](#nested-thresholds_with_series_overrides-blocks) below for details. * `widget_bullet` * `nrql_query` - (Required) A nested block that describes a NRQL Query. See [Nested nrql\_query blocks (for Widgets)](#nested-nrql_query-blocks-for-widgets) below for details. * `limit` - (Required) Visualization limit for the widget. @@ -505,6 +506,83 @@ The following arguments are supported: * `label` - (Optional) Grid configuration for label. * `value` - (Optional) Grid configuration for value. +### Nested `thresholds_with_series_overrides` blocks + +Nested `thresholds_with_series_overrides` blocks allow you to configure thresholds and series overrides settings for billboard widgets. + +The following arguments are supported: + + * `thresholds` - (Optional) A list of threshold configurations. See [Nested thresholds blocks](#nested-thresholds-blocks) below for details. + * `series_overrides` - (Optional) A list of threshold with series overrides configurations. See [Nested series\_overrides blocks](#nested-series_overrides-blocks) below for details. + +### Nested `thresholds` blocks + +The `thresholds` block within `thresholds_with_series_overrides` supports: + + * `from` - (Optional) The value 'from' which the threshold would need to be applied. + * `to` - (Optional) The value until which the threshold would need to be applied. + * `severity` - (Optional) The severity of the threshold, which would affect the visual appearance of the threshold (such as its color) accordingly. The value of this attribute would need to be one of the following - `warning`, `severe`, `critical`, `success`, `unavailable` which correspond to the severity labels _Warning_, _Approaching critical_, _Critical_, _Good_, _Neutral_ in the dropdown that helps specify the severity of thresholds in billboard widgets in the UI, respectively. + +### Nested `series_overrides` blocks + +The `series_overrides` block within `thresholds_with_series_overrides` supports: + + * `from` - (Optional) The value 'from' which the threshold would need to be applied. + * `to` - (Optional) The value until which the threshold would need to be applied. + * `series_name` - (Optional) The name of the series this override setting applies to. + * `severity` - (Optional) The severity of the threshold, which would affect the visual appearance of the threshold (such as its color) accordingly. The value of this attribute would need to be one of the following - `warning`, `severe`, `critical`, `success`, `unavailable` which correspond to the severity labels _Warning_, _Approaching critical_, _Critical_, _Good_, _Neutral_ in the dropdown that helps specify the severity of thresholds in billboard widgets in the UI, respectively. + +#### Example Usage + +```hcl +resource "newrelic_one_dashboard" "exampledash" { + name = "New Relic Terraform Example" + permissions = "public_read_only" + + page { + name = "New Relic Terraform Example" + + widget_billboard { + title = "Requests per minute" + row = 1 + column = 1 + width = 6 + height = 3 + + refresh_rate = 60000 // 60 seconds + + data_format { + name = "rate" + type = "recent-relative" + } + + nrql_query { + query = "FROM Transaction SELECT rate(count(*), 1 minute)" + } + + thresholds_with_series_overrides { + thresholds { + from = 0 + to = 70 + severity = "warning" + } + thresholds { + from = 70 + to = 100 + severity = "critical" + } + series_overrides { + from = 0 + to = 50 + series_name = "Rate" + severity = "warning" + } + } + } + } +} +``` + ### Nested `nrql_query` blocks (for Widgets) Nested `nrql_query` blocks in **widget** allow you to make one or more NRQL queries within a widget, against one or more specified accounts.