From b0dfeb19116183b3a4118c046e796e3921f63aed Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 9 Feb 2024 11:39:21 +0100 Subject: [PATCH 01/50] Chore: Clean up intervalv2 functions (#82074) * clean up intervalv2 functions * use roundInterval from grafana-plugin-sdk-go * use from grafana-plugin-sdk-go * have intervalv2 in publicdashboards and remove tsdb/intervalv2 * legacydata cleanup * remove unused variables * Update pkg/tsdb/legacydata/interval/interval.go Co-authored-by: Arati R. <33031346+suntala@users.noreply.github.com> --------- Co-authored-by: Arati R. <33031346+suntala@users.noreply.github.com> --- .github/CODEOWNERS | 1 - .../conditions/query_interval_test.go | 13 +- .../service/intervalv2/intervalv2.go | 77 ++++++ .../service}/intervalv2/intervalv2_test.go | 69 ------ .../publicdashboards/service/query_test.go | 2 +- .../publicdashboards/service/service.go | 3 +- .../publicdashboards/service/service_test.go | 2 +- pkg/tsdb/azuremonitor/macros/macros.go | 4 +- pkg/tsdb/azuremonitor/time/interval.go | 93 ------- pkg/tsdb/azuremonitor/time/time-grain.go | 4 +- pkg/tsdb/cloud-monitoring/time/interval.go | 192 +-------------- pkg/tsdb/cloud-monitoring/utils.go | 4 +- pkg/tsdb/influxdb/flux/macros.go | 4 +- pkg/tsdb/influxdb/models/query.go | 5 +- pkg/tsdb/intervalv2/intervalv2.go | 228 ------------------ pkg/tsdb/legacydata/interval/interval.go | 128 +--------- pkg/tsdb/legacydata/interval/interval_test.go | 20 -- pkg/tsdb/loki/parse_query.go | 6 +- pkg/tsdb/loki/step.go | 4 +- 19 files changed, 114 insertions(+), 745 deletions(-) create mode 100644 pkg/services/publicdashboards/service/intervalv2/intervalv2.go rename pkg/{tsdb => services/publicdashboards/service}/intervalv2/intervalv2_test.go (54%) delete mode 100644 pkg/tsdb/azuremonitor/time/interval.go delete mode 100644 pkg/tsdb/intervalv2/intervalv2.go diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index dd8e6988ebd..39fcdff146c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -142,7 +142,6 @@ /pkg/tests/apis/ @grafana/grafana-app-platform-squad /pkg/tests/api/correlations/ @grafana/explore-squad /pkg/tsdb/grafanads/ @grafana/backend-platform -/pkg/tsdb/intervalv2/ @grafana/backend-platform /pkg/tsdb/legacydata/ @grafana/backend-platform /pkg/tsdb/opentsdb/ @grafana/backend-platform /pkg/tsdb/sqleng/ @grafana/partner-datasources @grafana/oss-big-tent diff --git a/pkg/services/alerting/conditions/query_interval_test.go b/pkg/services/alerting/conditions/query_interval_test.go index 443b0f74d83..bb841e0f9ad 100644 --- a/pkg/services/alerting/conditions/query_interval_test.go +++ b/pkg/services/alerting/conditions/query_interval_test.go @@ -15,10 +15,11 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/validations" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) +const DefaultRes int64 = 1500 + func TestQueryInterval(t *testing.T) { t.Run("When evaluating query condition, regarding the interval value", func(t *testing.T) { t.Run("Can handle interval-calculation with no panel-min-interval and no datasource-min-interval", func(t *testing.T) { @@ -34,7 +35,7 @@ func TestQueryInterval(t *testing.T) { // 5minutes timerange = 300000milliseconds; default-resolution is 1500pixels, // so we should have 300000/1500 = 200milliseconds here require.Equal(t, int64(200), query.IntervalMS) - require.Equal(t, intervalv2.DefaultRes, query.MaxDataPoints) + require.Equal(t, DefaultRes, query.MaxDataPoints) } applyScenario(t, timeRange, dataSourceJson, queryModel, verifier) @@ -50,7 +51,7 @@ func TestQueryInterval(t *testing.T) { verifier := func(query legacydata.DataSubQuery) { require.Equal(t, int64(123000), query.IntervalMS) - require.Equal(t, intervalv2.DefaultRes, query.MaxDataPoints) + require.Equal(t, DefaultRes, query.MaxDataPoints) } applyScenario(t, timeRange, dataSourceJson, queryModel, verifier) @@ -69,7 +70,7 @@ func TestQueryInterval(t *testing.T) { verifier := func(query legacydata.DataSubQuery) { require.Equal(t, int64(71000), query.IntervalMS) - require.Equal(t, intervalv2.DefaultRes, query.MaxDataPoints) + require.Equal(t, DefaultRes, query.MaxDataPoints) } applyScenario(t, timeRange, dataSourceJson, queryModel, verifier) @@ -90,7 +91,7 @@ func TestQueryInterval(t *testing.T) { // when both panel-min-interval and datasource-min-interval exists, // panel-min-interval is used require.Equal(t, int64(19000), query.IntervalMS) - require.Equal(t, intervalv2.DefaultRes, query.MaxDataPoints) + require.Equal(t, DefaultRes, query.MaxDataPoints) } applyScenario(t, timeRange, dataSourceJson, queryModel, verifier) @@ -109,7 +110,7 @@ func TestQueryInterval(t *testing.T) { // no min-interval exists, the default-min-interval will be used, // and for such a short time-range this will cause the value to be 1millisecond. require.Equal(t, int64(1), query.IntervalMS) - require.Equal(t, intervalv2.DefaultRes, query.MaxDataPoints) + require.Equal(t, DefaultRes, query.MaxDataPoints) } applyScenario(t, timeRange, dataSourceJson, queryModel, verifier) diff --git a/pkg/services/publicdashboards/service/intervalv2/intervalv2.go b/pkg/services/publicdashboards/service/intervalv2/intervalv2.go new file mode 100644 index 00000000000..b20defc27f6 --- /dev/null +++ b/pkg/services/publicdashboards/service/intervalv2/intervalv2.go @@ -0,0 +1,77 @@ +package intervalv2 + +import ( + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" +) + +var ( + DefaultRes int64 = 1500 + defaultMinInterval = time.Millisecond * 1 +) + +type Interval struct { + Text string + Value time.Duration +} + +type intervalCalculator struct { + minInterval time.Duration +} + +type Calculator interface { + Calculate(timerange backend.TimeRange, minInterval time.Duration, maxDataPoints int64) Interval + CalculateSafeInterval(timerange backend.TimeRange, resolution int64) Interval +} + +type CalculatorOptions struct { + MinInterval time.Duration +} + +func NewCalculator(opts ...CalculatorOptions) *intervalCalculator { + calc := &intervalCalculator{} + + for _, o := range opts { + if o.MinInterval == 0 { + calc.minInterval = defaultMinInterval + } else { + calc.minInterval = o.MinInterval + } + } + + return calc +} + +func (i *Interval) Milliseconds() int64 { + return i.Value.Nanoseconds() / int64(time.Millisecond) +} + +func (ic *intervalCalculator) Calculate(timerange backend.TimeRange, minInterval time.Duration, maxDataPoints int64) Interval { + to := timerange.To.UnixNano() + from := timerange.From.UnixNano() + resolution := maxDataPoints + if resolution == 0 { + resolution = DefaultRes + } + + calculatedInterval := time.Duration((to - from) / resolution) + + if calculatedInterval < minInterval { + return Interval{Text: gtime.FormatInterval(minInterval), Value: minInterval} + } + + rounded := gtime.RoundInterval(calculatedInterval) + + return Interval{Text: gtime.FormatInterval(rounded), Value: rounded} +} + +func (ic *intervalCalculator) CalculateSafeInterval(timerange backend.TimeRange, safeRes int64) Interval { + to := timerange.To.UnixNano() + from := timerange.From.UnixNano() + safeInterval := time.Duration((to - from) / safeRes) + + rounded := gtime.RoundInterval(safeInterval) + return Interval{Text: gtime.FormatInterval(rounded), Value: rounded} +} diff --git a/pkg/tsdb/intervalv2/intervalv2_test.go b/pkg/services/publicdashboards/service/intervalv2/intervalv2_test.go similarity index 54% rename from pkg/tsdb/intervalv2/intervalv2_test.go rename to pkg/services/publicdashboards/service/intervalv2/intervalv2_test.go index 21b5a48abef..fe9900d90af 100644 --- a/pkg/tsdb/intervalv2/intervalv2_test.go +++ b/pkg/services/publicdashboards/service/intervalv2/intervalv2_test.go @@ -6,8 +6,6 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/stretchr/testify/assert" - - "github.com/grafana/grafana/pkg/services/datasources" ) func TestIntervalCalculator_Calculate(t *testing.T) { @@ -63,70 +61,3 @@ func TestIntervalCalculator_CalculateSafeInterval(t *testing.T) { }) } } - -func TestRoundInterval(t *testing.T) { - testCases := []struct { - name string - interval time.Duration - expected time.Duration - }{ - {"10ms", time.Millisecond * 10, time.Millisecond * 1}, - {"15ms", time.Millisecond * 15, time.Millisecond * 10}, - {"30ms", time.Millisecond * 30, time.Millisecond * 20}, - {"45ms", time.Millisecond * 45, time.Millisecond * 50}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expected, roundInterval(tc.interval)) - }) - } -} - -func TestFormatDuration(t *testing.T) { - testCases := []struct { - name string - duration time.Duration - expected string - }{ - {"61s", time.Second * 61, "1m"}, - {"30ms", time.Millisecond * 30, "30ms"}, - {"23h", time.Hour * 23, "23h"}, - {"24h", time.Hour * 24, "1d"}, - {"367d", time.Hour * 24 * 367, "1y"}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expected, FormatDuration(tc.duration)) - }) - } -} - -func TestGetIntervalFrom(t *testing.T) { - testCases := []struct { - name string - dsInfo *datasources.DataSource - queryInterval string - queryIntervalMs int64 - defaultInterval time.Duration - expected time.Duration - }{ - {"45s", nil, "45s", 0, time.Second * 15, time.Second * 45}, - {"45", nil, "45", 0, time.Second * 15, time.Second * 45}, - {"2m", nil, "2m", 0, time.Second * 15, time.Minute * 2}, - {"1d", nil, "1d", 0, time.Second * 15, time.Hour * 24}, - {"intervalMs", nil, "", 45000, time.Second * 15, time.Second * 45}, - {"intervalMs sub-seconds", nil, "", 45200, time.Second * 15, time.Millisecond * 45200}, - {"defaultInterval when interval empty", nil, "", 0, time.Second * 15, time.Second * 15}, - {"defaultInterval when intervalMs 0", nil, "", 0, time.Second * 15, time.Second * 15}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - actual, err := GetIntervalFrom(tc.queryInterval, "", tc.queryIntervalMs, tc.defaultInterval) - assert.Nil(t, err) - assert.Equal(t, tc.expected, actual) - }) - } -} diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index 46775f5822f..f33a3cfaf55 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -23,12 +23,12 @@ import ( "github.com/grafana/grafana/pkg/services/publicdashboards/database" "github.com/grafana/grafana/pkg/services/publicdashboards/internal" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util" diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index c352e171a3f..eb8b3fcb0ad 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -8,6 +8,7 @@ import ( "github.com/google/uuid" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" @@ -16,11 +17,11 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/publicdashboards" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util" ) diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 39f6f0af647..54f8f9cd32e 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -25,12 +25,12 @@ import ( . "github.com/grafana/grafana/pkg/services/publicdashboards" "github.com/grafana/grafana/pkg/services/publicdashboards/database" . "github.com/grafana/grafana/pkg/services/publicdashboards/models" + "github.com/grafana/grafana/pkg/services/publicdashboards/service/intervalv2" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) diff --git a/pkg/tsdb/azuremonitor/macros/macros.go b/pkg/tsdb/azuremonitor/macros/macros.go index 11304b7416d..953f5f5c914 100644 --- a/pkg/tsdb/azuremonitor/macros/macros.go +++ b/pkg/tsdb/azuremonitor/macros/macros.go @@ -8,9 +8,9 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/kinds/dataquery" - azTime "github.com/grafana/grafana/pkg/tsdb/azuremonitor/time" "github.com/grafana/grafana/pkg/tsdb/azuremonitor/types" ) @@ -126,7 +126,7 @@ func (m *kqlMacroEngine) evaluateMacro(name string, defaultTimeField string, arg if dsInterval, ok = dsInfo.JSONData["interval"].(string); !ok { dsInterval = "" } - it, err = azTime.GetIntervalFrom(dsInterval, queryInterval.Interval, queryInterval.IntervalMs, defaultInterval) + it, err = gtime.GetIntervalFrom(dsInterval, queryInterval.Interval, queryInterval.IntervalMs, defaultInterval) if err != nil { it = defaultInterval } diff --git a/pkg/tsdb/azuremonitor/time/interval.go b/pkg/tsdb/azuremonitor/time/interval.go deleted file mode 100644 index ed8a7587c8c..00000000000 --- a/pkg/tsdb/azuremonitor/time/interval.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copied from https://github.com/grafana/grafana/blob/main/pkg/tsdb/intervalv2/intervalv2.go -// We're copying this to not block ourselves from decoupling until the conversation here is resolved -// https://raintank-corp.slack.com/archives/C05QFJUHUQ6/p1700064431005089 -package time - -import ( - "fmt" - "regexp" - "strings" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" -) - -var ( - year = time.Hour * 24 * 365 - day = time.Hour * 24 -) - -// GetIntervalFrom returns the minimum interval. -// dsInterval is the string representation of data source min interval, if configured. -// queryInterval is the string representation of query interval (min interval), e.g. "10ms" or "10s". -// queryIntervalMS is a pre-calculated numeric representation of the query interval in milliseconds. -func GetIntervalFrom(dsInterval, queryInterval string, queryIntervalMS int64, defaultInterval time.Duration) (time.Duration, error) { - // Apparently we are setting default value of queryInterval to 0s now - interval := queryInterval - if interval == "0s" { - interval = "" - } - if interval == "" { - if queryIntervalMS != 0 { - return time.Duration(queryIntervalMS) * time.Millisecond, nil - } - } - if interval == "" && dsInterval != "" { - interval = dsInterval - } - if interval == "" { - return defaultInterval, nil - } - - parsedInterval, err := ParseIntervalStringToTimeDuration(interval) - if err != nil { - return time.Duration(0), err - } - - return parsedInterval, nil -} - -func ParseIntervalStringToTimeDuration(interval string) (time.Duration, error) { - formattedInterval := strings.Replace(strings.Replace(interval, "<", "", 1), ">", "", 1) - isPureNum, err := regexp.MatchString(`^\d+$`, formattedInterval) - if err != nil { - return time.Duration(0), err - } - if isPureNum { - formattedInterval += "s" - } - parsedInterval, err := gtime.ParseDuration(formattedInterval) - if err != nil { - return time.Duration(0), err - } - return parsedInterval, nil -} - -// FormatDuration converts a duration into the kbn format e.g. 1m 2h or 3d -func FormatDuration(inter time.Duration) string { - if inter >= year { - return fmt.Sprintf("%dy", inter/year) - } - - if inter >= day { - return fmt.Sprintf("%dd", inter/day) - } - - if inter >= time.Hour { - return fmt.Sprintf("%dh", inter/time.Hour) - } - - if inter >= time.Minute { - return fmt.Sprintf("%dm", inter/time.Minute) - } - - if inter >= time.Second { - return fmt.Sprintf("%ds", inter/time.Second) - } - - if inter >= time.Millisecond { - return fmt.Sprintf("%dms", inter/time.Millisecond) - } - - return "1ms" -} diff --git a/pkg/tsdb/azuremonitor/time/time-grain.go b/pkg/tsdb/azuremonitor/time/time-grain.go index f7c5b1e4d1c..dfc62d9c1ff 100644 --- a/pkg/tsdb/azuremonitor/time/time-grain.go +++ b/pkg/tsdb/azuremonitor/time/time-grain.go @@ -5,6 +5,8 @@ import ( "strconv" "strings" "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" ) // TimeGrain handles conversions between @@ -15,7 +17,7 @@ var ( ) func CreateISO8601DurationFromIntervalMS(it int64) (string, error) { - formatted := FormatDuration(time.Duration(it) * time.Millisecond) + formatted := gtime.FormatInterval(time.Duration(it) * time.Millisecond) if strings.Contains(formatted, "ms") { return "PT1M", nil diff --git a/pkg/tsdb/cloud-monitoring/time/interval.go b/pkg/tsdb/cloud-monitoring/time/interval.go index 3119d6fe624..c3a1c452f04 100644 --- a/pkg/tsdb/cloud-monitoring/time/interval.go +++ b/pkg/tsdb/cloud-monitoring/time/interval.go @@ -4,9 +4,6 @@ package time import ( - "fmt" - "regexp" - "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -16,8 +13,6 @@ import ( var ( DefaultRes int64 = 1500 defaultMinInterval = time.Millisecond * 1 - year = time.Hour * 24 * 365 - day = time.Hour * 24 ) type Interval struct { @@ -52,10 +47,6 @@ func NewCalculator(opts ...CalculatorOptions) *intervalCalculator { return calc } -func (i *Interval) Milliseconds() int64 { - return i.Value.Nanoseconds() / int64(time.Millisecond) -} - func (ic *intervalCalculator) Calculate(timerange backend.TimeRange, minInterval time.Duration, maxDataPoints int64) Interval { to := timerange.To.UnixNano() from := timerange.From.UnixNano() @@ -67,12 +58,12 @@ func (ic *intervalCalculator) Calculate(timerange backend.TimeRange, minInterval calculatedInterval := time.Duration((to - from) / resolution) if calculatedInterval < minInterval { - return Interval{Text: FormatDuration(minInterval), Value: minInterval} + return Interval{Text: gtime.FormatInterval(minInterval), Value: minInterval} } - rounded := roundInterval(calculatedInterval) + rounded := gtime.RoundInterval(calculatedInterval) - return Interval{Text: FormatDuration(rounded), Value: rounded} + return Interval{Text: gtime.FormatInterval(rounded), Value: rounded} } func (ic *intervalCalculator) CalculateSafeInterval(timerange backend.TimeRange, safeRes int64) Interval { @@ -80,179 +71,6 @@ func (ic *intervalCalculator) CalculateSafeInterval(timerange backend.TimeRange, from := timerange.From.UnixNano() safeInterval := time.Duration((to - from) / safeRes) - rounded := roundInterval(safeInterval) - return Interval{Text: FormatDuration(rounded), Value: rounded} -} - -// GetIntervalFrom returns the minimum interval. -// dsInterval is the string representation of data source min interval, if configured. -// queryInterval is the string representation of query interval (min interval), e.g. "10ms" or "10s". -// queryIntervalMS is a pre-calculated numeric representation of the query interval in milliseconds. -func GetIntervalFrom(dsInterval, queryInterval string, queryIntervalMS int64, defaultInterval time.Duration) (time.Duration, error) { - // Apparently we are setting default value of queryInterval to 0s now - interval := queryInterval - if interval == "0s" { - interval = "" - } - if interval == "" { - if queryIntervalMS != 0 { - return time.Duration(queryIntervalMS) * time.Millisecond, nil - } - } - if interval == "" && dsInterval != "" { - interval = dsInterval - } - if interval == "" { - return defaultInterval, nil - } - - parsedInterval, err := ParseIntervalStringToTimeDuration(interval) - if err != nil { - return time.Duration(0), err - } - - return parsedInterval, nil -} - -func ParseIntervalStringToTimeDuration(interval string) (time.Duration, error) { - formattedInterval := strings.Replace(strings.Replace(interval, "<", "", 1), ">", "", 1) - isPureNum, err := regexp.MatchString(`^\d+$`, formattedInterval) - if err != nil { - return time.Duration(0), err - } - if isPureNum { - formattedInterval += "s" - } - parsedInterval, err := gtime.ParseDuration(formattedInterval) - if err != nil { - return time.Duration(0), err - } - return parsedInterval, nil -} - -// FormatDuration converts a duration into the kbn format e.g. 1m 2h or 3d -func FormatDuration(inter time.Duration) string { - if inter >= year { - return fmt.Sprintf("%dy", inter/year) - } - - if inter >= day { - return fmt.Sprintf("%dd", inter/day) - } - - if inter >= time.Hour { - return fmt.Sprintf("%dh", inter/time.Hour) - } - - if inter >= time.Minute { - return fmt.Sprintf("%dm", inter/time.Minute) - } - - if inter >= time.Second { - return fmt.Sprintf("%ds", inter/time.Second) - } - - if inter >= time.Millisecond { - return fmt.Sprintf("%dms", inter/time.Millisecond) - } - - return "1ms" -} - -//nolint:gocyclo -func roundInterval(interval time.Duration) time.Duration { - switch { - // 0.01s - case interval <= 10*time.Millisecond: - return time.Millisecond * 1 // 0.001s - // 0.015s - case interval <= 15*time.Millisecond: - return time.Millisecond * 10 // 0.01s - // 0.035s - case interval <= 35*time.Millisecond: - return time.Millisecond * 20 // 0.02s - // 0.075s - case interval <= 75*time.Millisecond: - return time.Millisecond * 50 // 0.05s - // 0.15s - case interval <= 150*time.Millisecond: - return time.Millisecond * 100 // 0.1s - // 0.35s - case interval <= 350*time.Millisecond: - return time.Millisecond * 200 // 0.2s - // 0.75s - case interval <= 750*time.Millisecond: - return time.Millisecond * 500 // 0.5s - // 1.5s - case interval <= 1500*time.Millisecond: - return time.Millisecond * 1000 // 1s - // 3.5s - case interval <= 3500*time.Millisecond: - return time.Millisecond * 2000 // 2s - // 7.5s - case interval <= 7500*time.Millisecond: - return time.Millisecond * 5000 // 5s - // 12.5s - case interval <= 12500*time.Millisecond: - return time.Millisecond * 10000 // 10s - // 17.5s - case interval <= 17500*time.Millisecond: - return time.Millisecond * 15000 // 15s - // 25s - case interval <= 25000*time.Millisecond: - return time.Millisecond * 20000 // 20s - // 45s - case interval <= 45000*time.Millisecond: - return time.Millisecond * 30000 // 30s - // 1.5m - case interval <= 90000*time.Millisecond: - return time.Millisecond * 60000 // 1m - // 3.5m - case interval <= 210000*time.Millisecond: - return time.Millisecond * 120000 // 2m - // 7.5m - case interval <= 450000*time.Millisecond: - return time.Millisecond * 300000 // 5m - // 12.5m - case interval <= 750000*time.Millisecond: - return time.Millisecond * 600000 // 10m - // 17.5m - case interval <= 1050000*time.Millisecond: - return time.Millisecond * 900000 // 15m - // 25m - case interval <= 1500000*time.Millisecond: - return time.Millisecond * 1200000 // 20m - // 45m - case interval <= 2700000*time.Millisecond: - return time.Millisecond * 1800000 // 30m - // 1.5h - case interval <= 5400000*time.Millisecond: - return time.Millisecond * 3600000 // 1h - // 2.5h - case interval <= 9000000*time.Millisecond: - return time.Millisecond * 7200000 // 2h - // 4.5h - case interval <= 16200000*time.Millisecond: - return time.Millisecond * 10800000 // 3h - // 9h - case interval <= 32400000*time.Millisecond: - return time.Millisecond * 21600000 // 6h - // 24h - case interval <= 86400000*time.Millisecond: - return time.Millisecond * 43200000 // 12h - // 48h - case interval <= 172800000*time.Millisecond: - return time.Millisecond * 86400000 // 24h - // 1w - case interval <= 604800000*time.Millisecond: - return time.Millisecond * 86400000 // 24h - // 3w - case interval <= 1814400000*time.Millisecond: - return time.Millisecond * 604800000 // 1w - // 2y - case interval < 3628800000*time.Millisecond: - return time.Millisecond * 2592000000 // 30d - default: - return time.Millisecond * 31536000000 // 1y - } + rounded := gtime.RoundInterval(safeInterval) + return Interval{Text: gtime.FormatInterval(rounded), Value: rounded} } diff --git a/pkg/tsdb/cloud-monitoring/utils.go b/pkg/tsdb/cloud-monitoring/utils.go index aa03c7fc226..3b719ff2c7e 100644 --- a/pkg/tsdb/cloud-monitoring/utils.go +++ b/pkg/tsdb/cloud-monitoring/utils.go @@ -14,17 +14,17 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/backend/log" "github.com/grafana/grafana-plugin-sdk-go/backend/tracing" "github.com/grafana/grafana-plugin-sdk-go/data" - gcmTime "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring/time" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" ) func addInterval(period string, field *data.Field) error { period = strings.TrimPrefix(period, "+") - p, err := gcmTime.ParseIntervalStringToTimeDuration(period) + p, err := gtime.ParseIntervalStringToTimeDuration(period) if err != nil { return err } diff --git a/pkg/tsdb/influxdb/flux/macros.go b/pkg/tsdb/influxdb/flux/macros.go index ed3a34d2d20..4dce9d76822 100644 --- a/pkg/tsdb/influxdb/flux/macros.go +++ b/pkg/tsdb/influxdb/flux/macros.go @@ -6,7 +6,7 @@ import ( "strings" "time" - "github.com/grafana/grafana/pkg/tsdb/intervalv2" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" ) // $__interval_ms is the exact value in milliseconds @@ -15,7 +15,7 @@ import ( func interpolateInterval(flux string, interval time.Duration) string { intervalMs := int64(interval / time.Millisecond) - intervalText := intervalv2.FormatDuration(interval) + intervalText := gtime.FormatInterval(interval) flux = strings.ReplaceAll(flux, "$__interval_ms", strconv.FormatInt(intervalMs, 10)) flux = strings.ReplaceAll(flux, "$__interval", intervalText) diff --git a/pkg/tsdb/influxdb/models/query.go b/pkg/tsdb/influxdb/models/query.go index 64f3331a37f..fc6024fe074 100644 --- a/pkg/tsdb/influxdb/models/query.go +++ b/pkg/tsdb/influxdb/models/query.go @@ -8,8 +8,7 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" - - "github.com/grafana/grafana/pkg/tsdb/intervalv2" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" ) var ( @@ -36,7 +35,7 @@ func (query *Query) Build(queryContext *backend.QueryDataRequest) (string, error res += query.renderTz() } - intervalText := intervalv2.FormatDuration(query.Interval) + intervalText := gtime.FormatInterval(query.Interval) intervalMs := int64(query.Interval / time.Millisecond) res = strings.ReplaceAll(res, "$timeFilter", query.renderTimeFilter(queryContext)) diff --git a/pkg/tsdb/intervalv2/intervalv2.go b/pkg/tsdb/intervalv2/intervalv2.go deleted file mode 100644 index 9aadc011802..00000000000 --- a/pkg/tsdb/intervalv2/intervalv2.go +++ /dev/null @@ -1,228 +0,0 @@ -package intervalv2 - -import ( - "regexp" - "strings" - "time" - - "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" -) - -var ( - DefaultRes int64 = 1500 - defaultMinInterval = time.Millisecond * 1 -) - -type Interval struct { - Text string - Value time.Duration -} - -type intervalCalculator struct { - minInterval time.Duration -} - -type Calculator interface { - Calculate(timerange backend.TimeRange, minInterval time.Duration, maxDataPoints int64) Interval - CalculateSafeInterval(timerange backend.TimeRange, resolution int64) Interval -} - -type CalculatorOptions struct { - MinInterval time.Duration -} - -func NewCalculator(opts ...CalculatorOptions) *intervalCalculator { - calc := &intervalCalculator{} - - for _, o := range opts { - if o.MinInterval == 0 { - calc.minInterval = defaultMinInterval - } else { - calc.minInterval = o.MinInterval - } - } - - return calc -} - -func (i *Interval) Milliseconds() int64 { - return i.Value.Nanoseconds() / int64(time.Millisecond) -} - -func (ic *intervalCalculator) Calculate(timerange backend.TimeRange, minInterval time.Duration, maxDataPoints int64) Interval { - to := timerange.To.UnixNano() - from := timerange.From.UnixNano() - resolution := maxDataPoints - if resolution == 0 { - resolution = DefaultRes - } - - calculatedInterval := time.Duration((to - from) / resolution) - - if calculatedInterval < minInterval { - return Interval{Text: FormatDuration(minInterval), Value: minInterval} - } - - rounded := roundInterval(calculatedInterval) - - return Interval{Text: FormatDuration(rounded), Value: rounded} -} - -func (ic *intervalCalculator) CalculateSafeInterval(timerange backend.TimeRange, safeRes int64) Interval { - to := timerange.To.UnixNano() - from := timerange.From.UnixNano() - safeInterval := time.Duration((to - from) / safeRes) - - rounded := roundInterval(safeInterval) - return Interval{Text: FormatDuration(rounded), Value: rounded} -} - -// GetIntervalFrom returns the minimum interval. -// dsInterval is the string representation of data source min interval, if configured. -// queryInterval is the string representation of query interval (min interval), e.g. "10ms" or "10s". -// queryIntervalMS is a pre-calculated numeric representation of the query interval in milliseconds. -func GetIntervalFrom(dsInterval, queryInterval string, queryIntervalMS int64, defaultInterval time.Duration) (time.Duration, error) { - // Apparently we are setting default value of queryInterval to 0s now - interval := queryInterval - if interval == "0s" { - interval = "" - } - if interval == "" { - if queryIntervalMS != 0 { - return time.Duration(queryIntervalMS) * time.Millisecond, nil - } - } - if interval == "" && dsInterval != "" { - interval = dsInterval - } - if interval == "" { - return defaultInterval, nil - } - - parsedInterval, err := ParseIntervalStringToTimeDuration(interval) - if err != nil { - return time.Duration(0), err - } - - return parsedInterval, nil -} - -func ParseIntervalStringToTimeDuration(interval string) (time.Duration, error) { - formattedInterval := strings.Replace(strings.Replace(interval, "<", "", 1), ">", "", 1) - isPureNum, err := regexp.MatchString(`^\d+$`, formattedInterval) - if err != nil { - return time.Duration(0), err - } - if isPureNum { - formattedInterval += "s" - } - parsedInterval, err := gtime.ParseDuration(formattedInterval) - if err != nil { - return time.Duration(0), err - } - return parsedInterval, nil -} - -// FormatDuration converts a duration into the kbn format e.g. 1m 2h or 3d -func FormatDuration(inter time.Duration) string { - return gtime.FormatInterval(inter) -} - -//nolint:gocyclo -func roundInterval(interval time.Duration) time.Duration { - switch { - // 0.01s - case interval <= 10*time.Millisecond: - return time.Millisecond * 1 // 0.001s - // 0.015s - case interval <= 15*time.Millisecond: - return time.Millisecond * 10 // 0.01s - // 0.035s - case interval <= 35*time.Millisecond: - return time.Millisecond * 20 // 0.02s - // 0.075s - case interval <= 75*time.Millisecond: - return time.Millisecond * 50 // 0.05s - // 0.15s - case interval <= 150*time.Millisecond: - return time.Millisecond * 100 // 0.1s - // 0.35s - case interval <= 350*time.Millisecond: - return time.Millisecond * 200 // 0.2s - // 0.75s - case interval <= 750*time.Millisecond: - return time.Millisecond * 500 // 0.5s - // 1.5s - case interval <= 1500*time.Millisecond: - return time.Millisecond * 1000 // 1s - // 3.5s - case interval <= 3500*time.Millisecond: - return time.Millisecond * 2000 // 2s - // 7.5s - case interval <= 7500*time.Millisecond: - return time.Millisecond * 5000 // 5s - // 12.5s - case interval <= 12500*time.Millisecond: - return time.Millisecond * 10000 // 10s - // 17.5s - case interval <= 17500*time.Millisecond: - return time.Millisecond * 15000 // 15s - // 25s - case interval <= 25000*time.Millisecond: - return time.Millisecond * 20000 // 20s - // 45s - case interval <= 45000*time.Millisecond: - return time.Millisecond * 30000 // 30s - // 1.5m - case interval <= 90000*time.Millisecond: - return time.Millisecond * 60000 // 1m - // 3.5m - case interval <= 210000*time.Millisecond: - return time.Millisecond * 120000 // 2m - // 7.5m - case interval <= 450000*time.Millisecond: - return time.Millisecond * 300000 // 5m - // 12.5m - case interval <= 750000*time.Millisecond: - return time.Millisecond * 600000 // 10m - // 17.5m - case interval <= 1050000*time.Millisecond: - return time.Millisecond * 900000 // 15m - // 25m - case interval <= 1500000*time.Millisecond: - return time.Millisecond * 1200000 // 20m - // 45m - case interval <= 2700000*time.Millisecond: - return time.Millisecond * 1800000 // 30m - // 1.5h - case interval <= 5400000*time.Millisecond: - return time.Millisecond * 3600000 // 1h - // 2.5h - case interval <= 9000000*time.Millisecond: - return time.Millisecond * 7200000 // 2h - // 4.5h - case interval <= 16200000*time.Millisecond: - return time.Millisecond * 10800000 // 3h - // 9h - case interval <= 32400000*time.Millisecond: - return time.Millisecond * 21600000 // 6h - // 24h - case interval <= 86400000*time.Millisecond: - return time.Millisecond * 43200000 // 12h - // 48h - case interval <= 172800000*time.Millisecond: - return time.Millisecond * 86400000 // 24h - // 1w - case interval <= 604800000*time.Millisecond: - return time.Millisecond * 86400000 // 24h - // 3w - case interval <= 1814400000*time.Millisecond: - return time.Millisecond * 604800000 // 1w - // 2y - case interval < 3628800000*time.Millisecond: - return time.Millisecond * 2592000000 // 30d - default: - return time.Millisecond * 31536000000 // 1y - } -} diff --git a/pkg/tsdb/legacydata/interval/interval.go b/pkg/tsdb/legacydata/interval/interval.go index 2ab255a3c5f..7f9769f4717 100644 --- a/pkg/tsdb/legacydata/interval/interval.go +++ b/pkg/tsdb/legacydata/interval/interval.go @@ -1,7 +1,6 @@ package interval import ( - "fmt" "regexp" "strings" "time" @@ -16,8 +15,6 @@ import ( var ( DefaultRes int64 = 1500 defaultMinInterval = time.Millisecond * 1 - year = time.Hour * 24 * 365 - day = time.Hour * 24 ) type Interval struct { @@ -62,11 +59,11 @@ func (ic *intervalCalculator) Calculate(timerange legacydata.DataTimeRange, minI calculatedInterval := time.Duration((to - from) / DefaultRes) if calculatedInterval < minInterval { - return Interval{Text: FormatDuration(minInterval), Value: minInterval} + return Interval{Text: gtime.FormatInterval(minInterval), Value: minInterval} } rounded := roundInterval(calculatedInterval) - return Interval{Text: FormatDuration(rounded), Value: rounded} + return Interval{Text: gtime.FormatInterval(rounded), Value: rounded} } func (ic *intervalCalculator) CalculateSafeInterval(timerange legacydata.DataTimeRange, safeRes int64) Interval { @@ -75,7 +72,7 @@ func (ic *intervalCalculator) CalculateSafeInterval(timerange legacydata.DataTim safeInterval := time.Duration((to - from) / safeRes) rounded := roundInterval(safeInterval) - return Interval{Text: FormatDuration(rounded), Value: rounded} + return Interval{Text: gtime.FormatInterval(rounded), Value: rounded} } func GetIntervalFrom(dsInfo *datasources.DataSource, queryModel *simplejson.Json, defaultInterval time.Duration) (time.Duration, error) { @@ -117,126 +114,11 @@ func GetIntervalFrom(dsInfo *datasources.DataSource, queryModel *simplejson.Json return parsedInterval, nil } -// FormatDuration converts a duration into the kbn format e.g. 1m 2h or 3d -func FormatDuration(inter time.Duration) string { - if inter >= year { - return fmt.Sprintf("%dy", inter/year) - } - - if inter >= day { - return fmt.Sprintf("%dd", inter/day) - } - - if inter >= time.Hour { - return fmt.Sprintf("%dh", inter/time.Hour) - } - - if inter >= time.Minute { - return fmt.Sprintf("%dm", inter/time.Minute) - } - - if inter >= time.Second { - return fmt.Sprintf("%ds", inter/time.Second) - } - - if inter >= time.Millisecond { - return fmt.Sprintf("%dms", inter/time.Millisecond) - } - - return "1ms" -} - //nolint:gocyclo func roundInterval(interval time.Duration) time.Duration { - switch { // 0.015s - case interval <= 15*time.Millisecond: + if interval <= 15*time.Millisecond { return time.Millisecond * 10 // 0.01s - // 0.035s - case interval <= 35*time.Millisecond: - return time.Millisecond * 20 // 0.02s - // 0.075s - case interval <= 75*time.Millisecond: - return time.Millisecond * 50 // 0.05s - // 0.15s - case interval <= 150*time.Millisecond: - return time.Millisecond * 100 // 0.1s - // 0.35s - case interval <= 350*time.Millisecond: - return time.Millisecond * 200 // 0.2s - // 0.75s - case interval <= 750*time.Millisecond: - return time.Millisecond * 500 // 0.5s - // 1.5s - case interval <= 1500*time.Millisecond: - return time.Millisecond * 1000 // 1s - // 3.5s - case interval <= 3500*time.Millisecond: - return time.Millisecond * 2000 // 2s - // 7.5s - case interval <= 7500*time.Millisecond: - return time.Millisecond * 5000 // 5s - // 12.5s - case interval <= 12500*time.Millisecond: - return time.Millisecond * 10000 // 10s - // 17.5s - case interval <= 17500*time.Millisecond: - return time.Millisecond * 15000 // 15s - // 25s - case interval <= 25000*time.Millisecond: - return time.Millisecond * 20000 // 20s - // 45s - case interval <= 45000*time.Millisecond: - return time.Millisecond * 30000 // 30s - // 1.5m - case interval <= 90000*time.Millisecond: - return time.Millisecond * 60000 // 1m - // 3.5m - case interval <= 210000*time.Millisecond: - return time.Millisecond * 120000 // 2m - // 7.5m - case interval <= 450000*time.Millisecond: - return time.Millisecond * 300000 // 5m - // 12.5m - case interval <= 750000*time.Millisecond: - return time.Millisecond * 600000 // 10m - // 12.5m - case interval <= 1050000*time.Millisecond: - return time.Millisecond * 900000 // 15m - // 25m - case interval <= 1500000*time.Millisecond: - return time.Millisecond * 1200000 // 20m - // 45m - case interval <= 2700000*time.Millisecond: - return time.Millisecond * 1800000 // 30m - // 1.5h - case interval <= 5400000*time.Millisecond: - return time.Millisecond * 3600000 // 1h - // 2.5h - case interval <= 9000000*time.Millisecond: - return time.Millisecond * 7200000 // 2h - // 4.5h - case interval <= 16200000*time.Millisecond: - return time.Millisecond * 10800000 // 3h - // 9h - case interval <= 32400000*time.Millisecond: - return time.Millisecond * 21600000 // 6h - // 24h - case interval <= 86400000*time.Millisecond: - return time.Millisecond * 43200000 // 12h - // 48h - case interval <= 172800000*time.Millisecond: - return time.Millisecond * 86400000 // 24h - // 1w - case interval <= 604800000*time.Millisecond: - return time.Millisecond * 86400000 // 24h - // 3w - case interval <= 1814400000*time.Millisecond: - return time.Millisecond * 604800000 // 1w - // 2y - case interval < 3628800000*time.Millisecond: - return time.Millisecond * 2592000000 // 30d - default: - return time.Millisecond * 31536000000 // 1y } + return gtime.RoundInterval(interval) } diff --git a/pkg/tsdb/legacydata/interval/interval_test.go b/pkg/tsdb/legacydata/interval/interval_test.go index fdd5deb2519..b76c36a6774 100644 --- a/pkg/tsdb/legacydata/interval/interval_test.go +++ b/pkg/tsdb/legacydata/interval/interval_test.go @@ -74,26 +74,6 @@ func TestRoundInterval(t *testing.T) { } } -func TestFormatDuration(t *testing.T) { - testCases := []struct { - name string - duration time.Duration - expected string - }{ - {"61s", time.Second * 61, "1m"}, - {"30ms", time.Millisecond * 30, "30ms"}, - {"23h", time.Hour * 23, "23h"}, - {"24h", time.Hour * 24, "1d"}, - {"367d", time.Hour * 24 * 367, "1y"}, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - assert.Equal(t, tc.expected, FormatDuration(tc.duration)) - }) - } -} - func TestGetIntervalFrom(t *testing.T) { dsJSON, err := simplejson.NewJson([]byte(`{"timeInterval": "60s"}`)) require.NoError(t, err) diff --git a/pkg/tsdb/loki/parse_query.go b/pkg/tsdb/loki/parse_query.go index 5e1c8e44d0b..60ace2430c8 100644 --- a/pkg/tsdb/loki/parse_query.go +++ b/pkg/tsdb/loki/parse_query.go @@ -8,8 +8,8 @@ import ( "time" "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" - "github.com/grafana/grafana/pkg/tsdb/intervalv2" "github.com/grafana/grafana/pkg/tsdb/loki/kinds/dataquery" ) @@ -32,8 +32,8 @@ const ( ) func interpolateVariables(expr string, interval time.Duration, timeRange time.Duration, queryType dataquery.LokiQueryType, step time.Duration) string { - intervalText := intervalv2.FormatDuration(interval) - stepText := intervalv2.FormatDuration(step) + intervalText := gtime.FormatInterval(interval) + stepText := gtime.FormatInterval(step) intervalMsText := strconv.FormatInt(int64(interval/time.Millisecond), 10) rangeMs := timeRange.Milliseconds() diff --git a/pkg/tsdb/loki/step.go b/pkg/tsdb/loki/step.go index df66ebef466..4023abfe1db 100644 --- a/pkg/tsdb/loki/step.go +++ b/pkg/tsdb/loki/step.go @@ -4,7 +4,7 @@ import ( "math" "time" - "github.com/grafana/grafana/pkg/tsdb/intervalv2" + "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" ) // round the duration to the nearest millisecond larger-or-equal-to the duration @@ -31,7 +31,7 @@ func calculateStep(interval time.Duration, timeRange time.Duration, resolution i return ceilMs(chosenStep), nil } - step, err := intervalv2.ParseIntervalStringToTimeDuration(*queryStep) + step, err := gtime.ParseIntervalStringToTimeDuration(*queryStep) if err != nil { return step, err } From fc498f53756ed7ad5850c683b0b26745caa8ad61 Mon Sep 17 00:00:00 2001 From: marybelvargas <107340764+marybelvargas@users.noreply.github.com> Date: Fri, 9 Feb 2024 04:44:29 -0600 Subject: [PATCH 02/50] Update RBAC role name: fixed:datasources.id:reader (#82186) --- .../access-control/rbac-fixed-basic-role-definitions/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md index 8161b297f99..0957733aeb1 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/rbac-fixed-basic-role-definitions/index.md @@ -28,7 +28,7 @@ The following tables list permissions associated with basic and fixed roles. | Grafana Admin | `fixed:roles:reader`
`fixed:roles:writer`
`fixed:users:reader`
`fixed:users:writer`
`fixed:org.users:reader`
`fixed:org.users:writer`
`fixed:ldap:reader`
`fixed:ldap:writer`
`fixed:stats:reader`
`fixed:settings:reader`
`fixed:settings:writer`
`fixed:provisioning:writer`
`fixed:organization:reader`
`fixed:organization:maintainer`
`fixed:licensing:reader`
`fixed:licensing:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:maintainer`
`fixed:authentication.config:writer` | Default [Grafana server administrator]({{< relref "../../#grafana-server-administrators" >}}) assignments. | | Admin | `fixed:reports:reader`
`fixed:reports:writer`
`fixed:datasources:reader`
`fixed:datasources:writer`
`fixed:organization:writer`
`fixed:datasources.permissions:reader`
`fixed:datasources.permissions:writer`
`fixed:teams:writer`
`fixed:dashboards:reader`
`fixed:dashboards:writer`
`fixed:dashboards.permissions:reader`
`fixed:dashboards.permissions:writer`
`fixed:dashboards.public:writer`
`fixed:folders:reader`
`fixed:folders:writer`
`fixed:folders.permissions:reader`
`fixed:folders.permissions:writer`
`fixed:alerting:writer`
`fixed:apikeys:reader`
`fixed:apikeys:writer`
`fixed:alerting.provisioning.secrets:reader`
`fixed:alerting.provisioning:writer`
`fixed:datasources.caching:reader`
`fixed:datasources.caching:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader`
`fixed:plugins:writer` | Default [Grafana organization administrator]({{< relref "../#basic-roles" >}}) assignments. | | Editor | `fixed:datasources:explorer`
`fixed:dashboards:creator`
`fixed:folders:creator`
`fixed:annotations:writer`
`fixed:teams:creator` if the `editors_can_admin` configuration flag is enabled
`fixed:alerting:writer`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Editor]({{< relref "../#basic-roles" >}}) assignments. | -| Viewer | `fixed:datasources:id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#basic-roles" >}}) assignments. | +| Viewer | `fixed:datasources.id:reader`
`fixed:organization:reader`
`fixed:annotations:reader`
`fixed:annotations.dashboard:writer`
`fixed:alerting:reader`
`fixed:plugins.app:reader`
`fixed:dashboards.insights:reader`
`fixed:datasources.insights:reader` | Default [Viewer]({{< relref "../#basic-roles" >}}) assignments. | | No Basic Role | | Default [No Basic Role]({{< relref "../#basic-roles" >}}) | ## Fixed role definitions @@ -61,7 +61,7 @@ The following tables list permissions associated with basic and fixed roles. | `fixed:datasources.caching:reader` | `datasources.caching:read` | Read data source query caching settings. | | `fixed:datasources.caching:writer` | `datasources.caching:read`
`datasources.caching:write` | Enable, disable, or update query caching settings. | | `fixed:datasources:explorer` | `datasources:explore` | Enable the Explore feature. Data source permissions still apply, you can only query data sources for which you have query permissions. | -| `fixed:datasources:id:reader` | `datasources.id:read` | Read the ID of a data source based on its name. | +| `fixed:datasources.id:reader` | `datasources.id:read` | Read the ID of a data source based on its name. | | `fixed:datasources.insights:reader` | `datasources.insights:read` | Read data source insights data. | | `fixed:datasources.permissions:reader` | `datasources.permissions:read` | Read data source permissions. | | `fixed:datasources.permissions:writer` | All permissions from `fixed:datasources.permissions:reader` and
`datasources.permissions:write` | Create, read, or delete permissions of a data source. | From ed9e26122ad19b05aab250a4aeaba6ca6b9b4552 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Feb 2024 10:16:44 +0000 Subject: [PATCH 03/50] Update dependency @types/node to v20.11.17 --- package.json | 2 +- packages/grafana-data/package.json | 2 +- packages/grafana-e2e-selectors/package.json | 2 +- packages/grafana-prometheus/package.json | 2 +- packages/grafana-ui/package.json | 2 +- .../datasource/azuremonitor/package.json | 2 +- .../datasource/cloud-monitoring/package.json | 2 +- .../grafana-testdata-datasource/package.json | 2 +- .../app/plugins/datasource/tempo/package.json | 2 +- yarn.lock | 26 +++++++++---------- 10 files changed, 22 insertions(+), 22 deletions(-) diff --git a/package.json b/package.json index 21374db5bfc..95c29e024d9 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ "@types/lucene": "^2", "@types/marked": "5.0.2", "@types/mousetrap": "1.6.15", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "@types/node-forge": "^1", "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4", "@types/papaparse": "5.3.14", diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index f0b713ebdf5..cd1105d0fe3 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -76,7 +76,7 @@ "@types/jquery": "3.5.29", "@types/lodash": "4.14.202", "@types/marked": "5.0.2", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "@types/papaparse": "5.3.14", "@types/react": "18.2.55", "@types/react-dom": "18.2.19", diff --git a/packages/grafana-e2e-selectors/package.json b/packages/grafana-e2e-selectors/package.json index b515b6d6e09..25883821bec 100644 --- a/packages/grafana-e2e-selectors/package.json +++ b/packages/grafana-e2e-selectors/package.json @@ -41,7 +41,7 @@ "devDependencies": { "@rollup/plugin-commonjs": "25.0.7", "@rollup/plugin-node-resolve": "15.2.3", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "esbuild": "0.18.12", "rimraf": "5.0.5", "rollup": "2.79.1", diff --git a/packages/grafana-prometheus/package.json b/packages/grafana-prometheus/package.json index e1bc89e5334..7c701a97efe 100644 --- a/packages/grafana-prometheus/package.json +++ b/packages/grafana-prometheus/package.json @@ -82,7 +82,7 @@ "@types/jquery": "3.5.29", "@types/lodash": "4.14.202", "@types/marked": "5.0.2", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "@types/pluralize": "^0.0.33", "@types/prismjs": "1.26.3", "@types/react": "18.2.55", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index 4dda55d993d..7aa780ff848 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -140,7 +140,7 @@ "@types/jquery": "3.5.29", "@types/lodash": "4.14.202", "@types/mock-raf": "1.0.6", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "@types/prismjs": "1.26.3", "@types/react": "18.2.55", "@types/react-beautiful-dnd": "13.1.8", diff --git a/public/app/plugins/datasource/azuremonitor/package.json b/public/app/plugins/datasource/azuremonitor/package.json index 1c4388ff2e9..1aed00090cd 100644 --- a/public/app/plugins/datasource/azuremonitor/package.json +++ b/public/app/plugins/datasource/azuremonitor/package.json @@ -29,7 +29,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "@types/prismjs": "1.26.3", "@types/react": "18.2.55", "@types/testing-library__jest-dom": "5.14.9", diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 5af90024bfe..045a67c3c72 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -32,7 +32,7 @@ "@types/debounce-promise": "3.1.9", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "@types/prismjs": "1.26.3", "@types/react": "18.2.55", "@types/react-test-renderer": "18.0.7", diff --git a/public/app/plugins/datasource/grafana-testdata-datasource/package.json b/public/app/plugins/datasource/grafana-testdata-datasource/package.json index 11d0b7aa775..5595a328d48 100644 --- a/public/app/plugins/datasource/grafana-testdata-datasource/package.json +++ b/public/app/plugins/datasource/grafana-testdata-datasource/package.json @@ -27,7 +27,7 @@ "@types/d3-random": "^3.0.2", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "@types/react": "18.2.55", "@types/testing-library__jest-dom": "5.14.9", "@types/uuid": "9.0.8", diff --git a/public/app/plugins/datasource/tempo/package.json b/public/app/plugins/datasource/tempo/package.json index 5e14de8ac28..41dc8b8ba05 100644 --- a/public/app/plugins/datasource/tempo/package.json +++ b/public/app/plugins/datasource/tempo/package.json @@ -47,7 +47,7 @@ "@testing-library/user-event": "14.5.2", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.16", + "@types/node": "20.11.17", "@types/prismjs": "1.26.3", "@types/react": "18.2.55", "@types/react-dom": "18.2.19", diff --git a/yarn.lock b/yarn.lock index e44294cdd20..e2b0cba33eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3213,7 +3213,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.55" "@types/testing-library__jest-dom": "npm:5.14.9" @@ -3255,7 +3255,7 @@ __metadata: "@types/debounce-promise": "npm:3.1.9" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.55" "@types/react-test-renderer": "npm:18.0.7" @@ -3337,7 +3337,7 @@ __metadata: "@types/d3-random": "npm:^3.0.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" "@types/react": "npm:18.2.55" "@types/testing-library__jest-dom": "npm:5.14.9" "@types/uuid": "npm:9.0.8" @@ -3433,7 +3433,7 @@ __metadata: "@testing-library/user-event": "npm:14.5.2" "@types/jest": "npm:29.5.12" "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.55" "@types/react-dom": "npm:18.2.19" @@ -3507,7 +3507,7 @@ __metadata: "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.14.202" "@types/marked": "npm:5.0.2" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" "@types/papaparse": "npm:5.3.14" "@types/react": "npm:18.2.55" "@types/react-dom": "npm:18.2.19" @@ -3569,7 +3569,7 @@ __metadata: "@grafana/tsconfig": "npm:^1.2.0-rc1" "@rollup/plugin-commonjs": "npm:25.0.7" "@rollup/plugin-node-resolve": "npm:15.2.3" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" esbuild: "npm:0.18.12" rimraf: "npm:5.0.5" rollup: "npm:2.79.1" @@ -3893,7 +3893,7 @@ __metadata: "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.14.202" "@types/marked": "npm:5.0.2" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" "@types/pluralize": "npm:^0.0.33" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.55" @@ -4151,7 +4151,7 @@ __metadata: "@types/jquery": "npm:3.5.29" "@types/lodash": "npm:4.14.202" "@types/mock-raf": "npm:1.0.6" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" "@types/prismjs": "npm:1.26.3" "@types/react": "npm:18.2.55" "@types/react-beautiful-dnd": "npm:13.1.8" @@ -9438,12 +9438,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:*, @types/node@npm:20.11.16, @types/node@npm:>=13.7.0": - version: 20.11.16 - resolution: "@types/node@npm:20.11.16" +"@types/node@npm:*, @types/node@npm:20.11.17, @types/node@npm:>=13.7.0": + version: 20.11.17 + resolution: "@types/node@npm:20.11.17" dependencies: undici-types: "npm:~5.26.4" - checksum: 10/751f50ec5c9332b11515e82fe37c71479ac4449b711280aa3c7910edf67b1e3f5ac00041512add543f9a892096a68356406998bf02a2c809a73d176c44c28414 + checksum: 10/3342df87258d1c56154bcd4b85180f48675427b235971e6e6e2e037353f5a2ae9aaa05ba5df0fe1e2d2f1022c8d856fd39056b9d7f50ea30c0ca3214137cae1d languageName: node linkType: hard @@ -18125,7 +18125,7 @@ __metadata: "@types/lucene": "npm:^2" "@types/marked": "npm:5.0.2" "@types/mousetrap": "npm:1.6.15" - "@types/node": "npm:20.11.16" + "@types/node": "npm:20.11.17" "@types/node-forge": "npm:^1" "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.2.4" "@types/papaparse": "npm:5.3.14" From 3e93a0991f6ae1e77a7dc72fd6b7e4429dff28a2 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:13:37 +0100 Subject: [PATCH 04/50] Alerting: Use new readonly permission endpoints for getting contact points and mute timings (#82132) * use new read only contact points list endpoint in simplified routing section * Dont use alertmanager endpoint to get groupby defaults * Use the new read only endpoint for mute timings in route settings * review suggestions * Rename hook * Use options in params for useContactPointsWithStatus hook * Refactor useContactPointsWithStatus * second part of the enhanceContactPointsWithMetadata refactor --- .../alerting/unified/api/alertmanagerApi.ts | 9 ++++ .../contact-points/useContactPoints.tsx | 46 ++++++++++++++++--- .../components/contact-points/utils.ts | 29 ++++++++---- .../simplifiedRouting/AlertManagerRouting.tsx | 7 ++- .../route-settings/MuteTimingFields.tsx | 24 +++++++++- .../route-settings/RouteSettings.tsx | 25 ++++------ 6 files changed, 104 insertions(+), 36 deletions(-) diff --git a/public/app/features/alerting/unified/api/alertmanagerApi.ts b/public/app/features/alerting/unified/api/alertmanagerApi.ts index e97588813de..c0c71092210 100644 --- a/public/app/features/alerting/unified/api/alertmanagerApi.ts +++ b/public/app/features/alerting/unified/api/alertmanagerApi.ts @@ -11,7 +11,9 @@ import { ExternalAlertmanagerConfig, ExternalAlertmanagers, ExternalAlertmanagersResponse, + GrafanaManagedContactPoint, Matcher, + MuteTimeInterval, } from '../../../../plugins/datasource/alertmanager/types'; import { NotifierDTO } from '../../../../types'; import { withPerformanceLogging } from '../Analytics'; @@ -257,5 +259,12 @@ export const alertmanagerApi = alertingApi.injectEndpoints({ })); }, }), + // Grafana Managed Alertmanager only + getContactPointsList: build.query({ + query: () => ({ url: '/api/v1/notifications/receivers' }), + }), + getMuteTimingList: build.query({ + query: () => ({ url: '/api/v1/notifications/time-intervals' }), + }), }), }); diff --git a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx index d5b3a841ed6..5ab23e150e3 100644 --- a/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx +++ b/public/app/features/alerting/unified/components/contact-points/useContactPoints.tsx @@ -27,7 +27,13 @@ const RECEIVER_STATUS_POLLING_INTERVAL = 10 * 1000; // 10 seconds * 3. (if available) additional metadata about Grafana Managed contact points * 4. (if available) the OnCall plugin metadata */ -export function useContactPointsWithStatus() { +interface UseContactPointsWithStatusOptions { + includePoliciesCount: boolean; +} + +export function useContactPointsWithStatus( + { includePoliciesCount }: UseContactPointsWithStatusOptions = { includePoliciesCount: true } +) { const { selectedAlertmanager, isGrafanaAlertmanager } = useAlertmanager(); const { installed: onCallPluginInstalled, loading: onCallPluginStatusLoading } = usePluginBridge( SupportedPlugin.OnCall @@ -64,6 +70,7 @@ export function useContactPointsWithStatus() { } // fetch the latest config from the Alertmanager + // we use this endpoint only when we need to get the number of policies const fetchAlertmanagerConfiguration = alertmanagerApi.endpoints.getAlertmanagerConfiguration.useQuery( selectedAlertmanager!, { @@ -73,31 +80,56 @@ export function useContactPointsWithStatus() { ...result, contactPoints: result.data ? enhanceContactPointsWithMetadata( - result.data, fetchContactPointsStatus.data, fetchReceiverMetadata.data, - onCallMetadata + onCallMetadata, + result.data.alertmanager_config.receivers ?? [], + result.data ) : [], }), + skip: !includePoliciesCount, } ); + // for Grafana Managed Alertmanager, we use the new read-only endpoint for getting the list of contact points + const fetchGrafanaContactPoints = alertmanagerApi.endpoints.getContactPointsList.useQuery(undefined, { + refetchOnFocus: true, + refetchOnReconnect: true, + selectFromResult: (result) => ({ + ...result, + contactPoints: result.data + ? enhanceContactPointsWithMetadata( + fetchContactPointsStatus.data, + fetchReceiverMetadata.data, + onCallMetadata, + result.data, // contact points from the new readonly endpoint + undefined //no config data + ) + : [], + }), + skip: includePoliciesCount || !isGrafanaAlertmanager, + }); + // we will fail silently for fetching OnCall plugin status and integrations - const error = fetchAlertmanagerConfiguration.error ?? fetchContactPointsStatus.error; + const error = + fetchAlertmanagerConfiguration.error || fetchGrafanaContactPoints.error || fetchContactPointsStatus.error; const isLoading = fetchAlertmanagerConfiguration.isLoading || + fetchGrafanaContactPoints.isLoading || fetchContactPointsStatus.isLoading || onCallPluginStatusLoading || onCallPluginIntegrationsLoading; - const contactPoints = fetchAlertmanagerConfiguration.contactPoints.sort((a, b) => a.name.localeCompare(b.name)); - + const unsortedContactPoints = includePoliciesCount + ? fetchAlertmanagerConfiguration.contactPoints + : fetchGrafanaContactPoints.contactPoints; + const contactPoints = unsortedContactPoints.sort((a, b) => a.name.localeCompare(b.name)); return { error, isLoading, contactPoints, - refetchReceivers: fetchAlertmanagerConfiguration.refetch, + refetchReceivers: fetchGrafanaContactPoints.refetch, }; } diff --git a/public/app/features/alerting/unified/components/contact-points/utils.ts b/public/app/features/alerting/unified/components/contact-points/utils.ts index 38a69fd3b72..201351e1451 100644 --- a/public/app/features/alerting/unified/components/contact-points/utils.ts +++ b/public/app/features/alerting/unified/components/contact-points/utils.ts @@ -7,6 +7,7 @@ import { GrafanaManagedContactPoint, GrafanaManagedReceiverConfig, MatcherOperator, + Receiver, Route, } from 'app/plugins/datasource/alertmanager/types'; import { NotifierDTO, NotifierStatus, ReceiversStateDTO } from 'app/types'; @@ -30,6 +31,9 @@ export function isProvisioned(contactPoint: GrafanaManagedContactPoint) { // TODO we should really add some type information to these receiver settings... export function getReceiverDescription(receiver: ReceiverConfigWithMetadata): ReactNode | undefined { + if (!receiver.settings) { + return undefined; + } switch (receiver.type) { case 'email': { const hasEmailAddresses = 'addresses' in receiver.settings; // when dealing with alertmanager email_configs we don't normalize the settings @@ -87,7 +91,7 @@ export interface ReceiverConfigWithMetadata extends GrafanaManagedReceiverConfig } export interface ContactPointWithMetadata extends GrafanaManagedContactPoint { - numberOfPolicies: number; + numberOfPolicies?: number; // now is optional as we don't have the data from the read-only endpoint grafana_managed_receiver_configs: ReceiverConfigWithMetadata[]; } @@ -95,30 +99,36 @@ export interface ContactPointWithMetadata extends GrafanaManagedContactPoint { * This function adds the status information for each of the integrations (contact point types) in a contact point * 1. we iterate over all contact points * 2. for each contact point we "enhance" it with the status or "undefined" for vanilla Alertmanager + * contactPoints: list of contact points + * alertmanagerConfiguration: optional as is passed when we need to get number of policies for each contact point + * and we prefer using the data from the read-only endpoint. */ export function enhanceContactPointsWithMetadata( - result: AlertManagerCortexConfig, status: ReceiversStateDTO[] = [], notifiers: NotifierDTO[] = [], - onCallIntegrations: OnCallIntegrationDTO[] | undefined | null + onCallIntegrations: OnCallIntegrationDTO[] | undefined | null, + contactPoints: Receiver[], + alertmanagerConfiguration?: AlertManagerCortexConfig ): ContactPointWithMetadata[] { - const contactPoints = result.alertmanager_config.receivers ?? []; - // compute the entire inherited tree before finding what notification policies are using a particular contact point - const fullyInheritedTree = computeInheritedTree(result?.alertmanager_config?.route ?? {}); + const fullyInheritedTree = computeInheritedTree(alertmanagerConfiguration?.alertmanager_config?.route ?? {}); const usedContactPoints = getUsedContactPoints(fullyInheritedTree); const usedContactPointsByName = countBy(usedContactPoints); - return contactPoints.map((contactPoint) => { + const contactPointsList = alertmanagerConfiguration + ? alertmanagerConfiguration?.alertmanager_config.receivers ?? [] + : contactPoints ?? []; + + return contactPointsList.map((contactPoint) => { const receivers = extractReceivers(contactPoint); const statusForReceiver = status.find((status) => status.name === contactPoint.name); return { ...contactPoint, - numberOfPolicies: usedContactPointsByName[contactPoint.name] ?? 0, + numberOfPolicies: + alertmanagerConfiguration && usedContactPointsByName && (usedContactPointsByName[contactPoint.name] ?? 0), grafana_managed_receiver_configs: receivers.map((receiver, index) => { const isOnCallReceiver = receiver.type === ReceiverTypes.OnCall; - return { ...receiver, [RECEIVER_STATUS_KEY]: statusForReceiver?.integrations[index], @@ -130,6 +140,7 @@ export function enhanceContactPointsWithMetadata( }; }); } + export function isAutoGeneratedPolicy(route: Route) { const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; if (!simplifiedRoutingToggleEnabled) { diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx index 4ec14cd359a..06ab125cba9 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx @@ -22,7 +22,12 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo const styles = useStyles2(getStyles); const alertManagerName = alertManager.name; - const { isLoading, error: errorInContactPointStatus, contactPoints, refetchReceivers } = useContactPointsWithStatus(); + const { + isLoading, + error: errorInContactPointStatus, + contactPoints, + refetchReceivers, + } = useContactPointsWithStatus({ includePoliciesCount: false }); const [selectedContactPointWithMetadata, setSelectedContactPointWithMetadata] = useState< ContactPointWithMetadata | undefined >(); diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx index 70728a041d7..ddb93051902 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/MuteTimingFields.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { useFormContext } from 'react-hook-form'; +import { SelectableValue } from '@grafana/data'; import { Field, InputControl, MultiSelect, useStyles2 } from '@grafana/ui'; -import { useMuteTimingOptions } from 'app/features/alerting/unified/hooks/useMuteTimingOptions'; +import { alertmanagerApi } from 'app/features/alerting/unified/api/alertmanagerApi'; import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; +import { timeIntervalToString } from 'app/features/alerting/unified/utils/alertmanager'; import { mapMultiSelectValueToStrings } from 'app/features/alerting/unified/utils/amroutes'; import { getFormStyles } from '../../../../notification-policies/formStyles'; @@ -19,7 +21,7 @@ export function MuteTimingFields({ alertManager }: MuteTimingFieldsProps) { formState: { errors }, } = useFormContext(); - const muteTimingOptions = useMuteTimingOptions(); + const muteTimingOptions = useSelectableMuteTimings(); return ( ); } + +function useSelectableMuteTimings(): Array> { + const fetchGrafanaMuteTimings = alertmanagerApi.endpoints.getMuteTimingList.useQuery(undefined, { + refetchOnFocus: true, + refetchOnReconnect: true, + selectFromResult: (result) => ({ + ...result, + mutetimings: result.data + ? result.data.map((value) => ({ + value: value.name, + label: value.name, + description: value.time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '), + })) + : [], + }), + }); + return fetchGrafanaMuteTimings.mutetimings; +} diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx index 298239dc15d..d676664f2f1 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/RouteSettings.tsx @@ -14,8 +14,6 @@ import { Text, useStyles2, } from '@grafana/ui'; -import { useAlertmanagerConfig } from 'app/features/alerting/unified/hooks/useAlertmanagerConfig'; -import { useAlertmanager } from 'app/features/alerting/unified/state/AlertmanagerContext'; import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; import { commonGroupByOptions, @@ -42,7 +40,7 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => { formState: { errors }, } = useFormContext(); const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues([])); - const { groupBy, groupIntervalValue, groupWaitValue, repeatIntervalValue } = useGetDefaultsForRoutingSettings(); + const { groupIntervalValue, groupWaitValue, repeatIntervalValue } = getDefaultsForRoutingSettings(); const overrideGrouping = watch(`contactPoints.${alertManager}.overrideGrouping`); const overrideTimings = watch(`contactPoints.${alertManager}.overrideTimings`); const requiredFieldsInGroupBy = ['grafana_folder', 'alertname']; @@ -56,7 +54,7 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => { {!overrideGrouping && ( - Grouping: {groupBy.join(', ')} + Grouping: {requiredFieldsInGroupBy.join(', ')} )} @@ -131,20 +129,13 @@ export const RoutingSettings = ({ alertManager }: RoutingSettingsProps) => { ); }; -function useGetDefaultsForRoutingSettings() { - const { selectedAlertmanager } = useAlertmanager(); - const { currentData } = useAlertmanagerConfig(selectedAlertmanager); - const config = currentData?.alertmanager_config; - return React.useMemo(() => { - return { - groupWaitValue: TIMING_OPTIONS_DEFAULTS.group_wait, - groupIntervalValue: TIMING_OPTIONS_DEFAULTS.group_interval, - repeatIntervalValue: TIMING_OPTIONS_DEFAULTS.repeat_interval, - groupBy: config?.route?.group_by ?? [], - }; - }, [config]); +function getDefaultsForRoutingSettings() { + return { + groupWaitValue: TIMING_OPTIONS_DEFAULTS.group_wait, + groupIntervalValue: TIMING_OPTIONS_DEFAULTS.group_interval, + repeatIntervalValue: TIMING_OPTIONS_DEFAULTS.repeat_interval, + }; } - const getStyles = (theme: GrafanaTheme2) => ({ switchElement: css({ flexFlow: 'row-reverse', From beca6a08b030405b084eda0c831a26b95cb9b1e3 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:16:28 +0100 Subject: [PATCH 05/50] Alerting: defaults for simplified routing (#82050) * Expand route settings by default when alert rule has values in these fields * Default to manual routing option if FF is enabled and local storage is not set to false * Add test for getDefautManualRouting function * Update seting local storage item to false in case of policy routing * Only save to local storage when creating a new alert rule --- .../alert-rule-form/AlertRuleForm.tsx | 17 +++++++--- .../simplifiedRouting/AlertManagerRouting.tsx | 10 +++++- .../alerting/unified/utils/rule-form.test.ts | 33 +++++++++++++++++++ .../alerting/unified/utils/rule-form.ts | 18 ++++++++-- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx index a7a5ef32199..e1f56127f28 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/AlertRuleForm.tsx @@ -1,11 +1,11 @@ import { css } from '@emotion/css'; import React, { useEffect, useMemo, useState } from 'react'; -import { FormProvider, SubmitErrorHandler, useForm, UseFormWatch } from 'react-hook-form'; +import { FormProvider, SubmitErrorHandler, UseFormWatch, useForm } from 'react-hook-form'; import { Link, useParams } from 'react-router-dom'; import { GrafanaTheme2 } from '@grafana/data'; import { config } from '@grafana/runtime'; -import { Button, ConfirmModal, CustomScrollbar, HorizontalGroup, Spinner, useStyles2, Stack } from '@grafana/ui'; +import { Button, ConfirmModal, CustomScrollbar, HorizontalGroup, Spinner, Stack, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { useAppNotification } from 'app/core/copy/appNotification'; import { contextSrv } from 'app/core/core'; @@ -14,17 +14,18 @@ import { useQueryParams } from 'app/core/hooks/useQueryParams'; import { useDispatch } from 'app/types'; import { RuleWithLocation } from 'app/types/unified-alerting'; -import { logInfo, LogMessages, trackNewAlerRuleFormError } from '../../../Analytics'; +import { LogMessages, logInfo, trackNewAlerRuleFormError } from '../../../Analytics'; import { useUnifiedAlertingSelector } from '../../../hooks/useUnifiedAlertingSelector'; import { deleteRuleAction, saveRuleFormAction } from '../../../state/actions'; import { RuleFormType, RuleFormValues } from '../../../types/rule-form'; import { initialAsyncRequestState } from '../../../utils/redux'; import { + MANUAL_ROUTING_KEY, + MINUTE, formValuesFromExistingRule, getDefaultFormValues, getDefaultQueries, ignoreHiddenQueries, - MINUTE, normalizeDefaultAnnotations, } from '../../../utils/rule-form'; import * as ruleId from '../../../utils/rule-id'; @@ -108,6 +109,14 @@ export const AlertRuleForm = ({ existing, prefill }: Props) => { notifyApp.error(conditionErrorMsg); return; } + // when creating a new rule, we save the manual routing setting in local storage + if (!existing) { + if (values.manualRouting) { + localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); + } else { + localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); + } + } dispatch( saveRuleFormAction({ diff --git a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx index 06ab125cba9..94ee0b3be3f 100644 --- a/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx @@ -1,8 +1,10 @@ import { css } from '@emotion/css'; import React, { useState } from 'react'; +import { useFormContext } from 'react-hook-form'; import { GrafanaTheme2 } from '@grafana/data'; import { Alert, CollapsableSection, LoadingPlaceholder, Stack, useStyles2 } from '@grafana/ui'; +import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form'; import { AlertManagerDataSource } from 'app/features/alerting/unified/utils/datasource'; import { ContactPointReceiverSummary } from '../../../contact-points/ContactPoints'; @@ -36,6 +38,12 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo setSelectedContactPointWithMetadata(contactPoint); }; + const { watch } = useFormContext(); + const hasRouteSettings = + watch(`contactPoints.${alertManagerName}.overrideGrouping`) || + watch(`contactPoints.${alertManagerName}.overrideTimings`) || + watch(`contactPoints.${alertManagerName}.muteTimeIntervals`)?.length > 0; + const options = contactPoints.map((receiver) => { const integrations = receiver?.grafana_managed_receiver_configs; const description = ; @@ -74,7 +82,7 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo
diff --git a/public/app/features/alerting/unified/utils/rule-form.test.ts b/public/app/features/alerting/unified/utils/rule-form.test.ts index 48911f86b6a..735a8f7087b 100644 --- a/public/app/features/alerting/unified/utils/rule-form.test.ts +++ b/public/app/features/alerting/unified/utils/rule-form.test.ts @@ -1,3 +1,4 @@ +import { config } from '@grafana/runtime'; import { PromQuery } from 'app/plugins/datasource/prometheus/types'; import { GrafanaAlertStateDecision, GrafanaRuleDefinition, RulerAlertingRuleDTO } from 'app/types/unified-alerting-dto'; @@ -5,11 +6,13 @@ import { AlertManagerManualRouting, RuleFormType, RuleFormValues } from '../type import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { + MANUAL_ROUTING_KEY, alertingRulerRuleToRuleForm, formValuesToRulerGrafanaRuleDTO, formValuesToRulerRuleDTO, getContactPointsFromDTO, getDefaultFormValues, + getDefautManualRouting, getNotificationSettingsForDTO, } from './rule-form'; @@ -207,3 +210,33 @@ describe('getNotificationSettingsForDTO', () => { }); }); }); + +describe('getDefautManualRouting', () => { + afterEach(() => { + window.localStorage.clear(); + }); + + it('returns false if the feature toggle is not enabled', () => { + config.featureToggles.alertingSimplifiedRouting = false; + expect(getDefautManualRouting()).toBe(false); + }); + + it('returns true if the feature toggle is enabled and localStorage is not set', () => { + config.featureToggles.alertingSimplifiedRouting = true; + expect(getDefautManualRouting()).toBe(true); + }); + + it('returns false if the feature toggle is enabled and localStorage is set to "false"', () => { + config.featureToggles.alertingSimplifiedRouting = true; + localStorage.setItem(MANUAL_ROUTING_KEY, 'false'); + expect(getDefautManualRouting()).toBe(false); + }); + + it('returns true if the feature toggle is enabled and localStorage is set to any value other than "false"', () => { + config.featureToggles.alertingSimplifiedRouting = true; + localStorage.setItem(MANUAL_ROUTING_KEY, 'true'); + expect(getDefautManualRouting()).toBe(true); + localStorage.removeItem(MANUAL_ROUTING_KEY); + expect(getDefautManualRouting()).toBe(true); + }); +}); diff --git a/public/app/features/alerting/unified/utils/rule-form.ts b/public/app/features/alerting/unified/utils/rule-form.ts index cd4d6abdaad..f7dd7d6c437 100644 --- a/public/app/features/alerting/unified/utils/rule-form.ts +++ b/public/app/features/alerting/unified/utils/rule-form.ts @@ -11,7 +11,7 @@ import { ScopedVars, TimeRange, } from '@grafana/data'; -import { getDataSourceSrv } from '@grafana/runtime'; +import { config, getDataSourceSrv } from '@grafana/runtime'; import { ExpressionDatasourceRef } from '@grafana/runtime/src/utils/DataSourceWithBackend'; import { sceneGraph, VizPanel } from '@grafana/scenes'; import { DataSourceJsonData } from '@grafana/schema'; @@ -54,6 +54,8 @@ export type PromOrLokiQuery = PromQuery | LokiQuery; export const MINUTE = '1m'; +export const MANUAL_ROUTING_KEY = 'grafana.alerting.manualRouting'; + export const getDefaultFormValues = (): RuleFormValues => { const { canCreateGrafanaRules, canCreateCloudRules } = getRulesAccess(); @@ -75,7 +77,7 @@ export const getDefaultFormValues = (): RuleFormValues => { execErrState: GrafanaAlertStateDecision.Error, evaluateFor: '5m', evaluateEvery: MINUTE, - manualRouting: false, // let's decide this later + manualRouting: getDefautManualRouting(), // we default to true if the feature toggle is enabled and the user hasn't set local storage to false contactPoints: {}, overrideGrouping: false, overrideTimings: false, @@ -89,6 +91,18 @@ export const getDefaultFormValues = (): RuleFormValues => { }); }; +export const getDefautManualRouting = () => { + // first check if feature toggle for simplified routing is enabled + const simplifiedRoutingToggleEnabled = config.featureToggles.alertingSimplifiedRouting ?? false; + if (!simplifiedRoutingToggleEnabled) { + return false; + } + //then, check in local storage if the user has enabled simplified routing + // if it's not set, we'll default to true + const manualRouting = localStorage.getItem(MANUAL_ROUTING_KEY); + return manualRouting !== 'false'; +}; + export function formValuesToRulerRuleDTO(values: RuleFormValues): RulerRuleDTO { const { name, expression, forTime, forTimeUnit, keepFiringForTime, keepFiringForTimeUnit, type } = values; if (type === RuleFormType.cloudAlerting) { From 8beff981426b2f78083e7090cba1d4336324b7f9 Mon Sep 17 00:00:00 2001 From: Fabrizio <135109076+fabrizio-grafana@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:16:40 +0100 Subject: [PATCH 06/50] Update Prettier checks to parse also JSON files (#82046) --- .prettierignore | 4 +- package.json | 6 +- .../fixtures/exemplars-query-response.json | 103 +++--------------- .../grafana-o11y-ds-frontend/tsconfig.json | 4 +- packages/grafana-sql/tsconfig.json | 4 +- .../datasource/azuremonitor/tsconfig.json | 2 +- .../datasource/cloud-monitoring/tsconfig.json | 2 +- .../tsconfig.json | 2 +- .../plugins/datasource/parca/tsconfig.json | 2 +- .../plugins/datasource/tempo/tsconfig.json | 4 +- tsconfig.json | 8 +- 11 files changed, 35 insertions(+), 106 deletions(-) diff --git a/.prettierignore b/.prettierignore index 9f95507f7b3..524e01209cd 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,6 +12,7 @@ node_modules pkg public/lib/monaco public/sass/*.generated.scss +scripts/cli/bettererIssueTemplate.md scripts/grafana-server/tmp vendor @@ -37,4 +38,5 @@ kinds/report.json # Generated schema docs docs/sources/developers/kinds/ -scripts/cli/bettererIssueTemplate.md +# Crowdin files +public/locales/**/*.json diff --git a/package.json b/package.json index 95c29e024d9..5b61c7f3aed 100644 --- a/package.json +++ b/package.json @@ -31,9 +31,9 @@ "packages:prepare": "lerna version --no-push --no-git-tag-version --force-publish --exact", "packages:pack": "mkdir -p ./npm-artifacts && lerna exec --no-private -- yarn pack --out \"../../npm-artifacts/%s-%v.tgz\"", "packages:typecheck": "lerna run typecheck", - "prettier:check": "prettier --check --list-different=false --log-level=warn \"**/*.{ts,tsx,scss,md,mdx}\"", - "prettier:checkDocs": "prettier --check --list-different=false --log-level=warn \"docs/**/*.md\" \"*.md\" \"packages/**/*.{ts,tsx,scss,md,mdx}\"", - "prettier:write": "prettier --list-different \"**/*.{js,ts,tsx,scss,md,mdx}\" --write", + "prettier:check": "prettier --check --list-different=false --log-level=warn \"**/*.{ts,tsx,scss,md,mdx,json}\"", + "prettier:checkDocs": "prettier --check --list-different=false --log-level=warn \"docs/**/*.md\" \"*.md\" \"packages/**/*.{ts,tsx,scss,md,mdx,json}\"", + "prettier:write": "prettier --list-different \"**/*.{js,ts,tsx,scss,md,mdx,json}\" --write", "start": "yarn themes:generate && yarn dev --watch", "start:noTsCheck": "yarn start --env noTsCheck=1", "start:noLint": "yarn start --env noTsCheck=1 --env noLint=1", diff --git a/packages/grafana-e2e/cypress/fixtures/exemplars-query-response.json b/packages/grafana-e2e/cypress/fixtures/exemplars-query-response.json index c90d2c8457e..6c26a9fbf7d 100644 --- a/packages/grafana-e2e/cypress/fixtures/exemplars-query-response.json +++ b/packages/grafana-e2e/cypress/fixtures/exemplars-query-response.json @@ -23,50 +23,16 @@ "data": { "values": [ [ - 1633619595000, - 1633619610000, - 1633619625000, - 1633619640000, - 1633619655000, - 1633619670000, - 1633619685000, - 1633619700000, - 1633619715000, - 1633619730000, - 1633619745000, - 1633619760000, - 1633619775000, - 1633619790000, - 1633619805000, - 1633619820000, - 1633619835000, - 1633619850000, - 1633619865000, - 1633619880000, - 1633619895000 + 1633619595000, 1633619610000, 1633619625000, 1633619640000, 1633619655000, 1633619670000, 1633619685000, + 1633619700000, 1633619715000, 1633619730000, 1633619745000, 1633619760000, 1633619775000, 1633619790000, + 1633619805000, 1633619820000, 1633619835000, 1633619850000, 1633619865000, 1633619880000, 1633619895000 ], [ - 0.07245212135073513, - 0.07253198890830721, - 0.07247862573797707, - 0.07238248338231042, - 0.07221687487740913, - 0.07223291298743946, - 0.07225427016727755, - 0.024531677091864545, - 0.02317081920915543, - 0.07548902139580993, - 0.0777721702857508, - 0.07768649905047344, - 0.07782257603228229, - 0.07788810213200052, - 0.07791835055437593, - 0.07798387201529966, - 0.07790826751849372, - 0.07794858648610933, - 0.07778729925797964, - 0.07769657495236215, - 0.077550401329267 + 0.07245212135073513, 0.07253198890830721, 0.07247862573797707, 0.07238248338231042, 0.07221687487740913, + 0.07223291298743946, 0.07225427016727755, 0.024531677091864545, 0.02317081920915543, + 0.07548902139580993, 0.0777721702857508, 0.07768649905047344, 0.07782257603228229, 0.07788810213200052, + 0.07791835055437593, 0.07798387201529966, 0.07790826751849372, 0.07794858648610933, 0.07778729925797964, + 0.07769657495236215, 0.077550401329267 ] ] } @@ -113,54 +79,15 @@ "data": { "values": [ [ - 1633619598000, - 1633619622000, - 1633619625000, - 1633619646000, - 1633619658000, - 1633619682000, - 1633619695000, - 1633619712000, - 1633619712000, - 1633619724000, - 1633619717000, - 1633619742000, - 1633619757000, - 1633619771000, - 1633619784000, - 1633619801000, - 1633619806000, - 1633619833000, - 1633619833000, - 1633619845000, - 1633619862000, - 1633619877000, - 1633619889000 + 1633619598000, 1633619622000, 1633619625000, 1633619646000, 1633619658000, 1633619682000, 1633619695000, + 1633619712000, 1633619712000, 1633619724000, 1633619717000, 1633619742000, 1633619757000, 1633619771000, + 1633619784000, 1633619801000, 1633619806000, 1633619833000, 1633619833000, 1633619845000, 1633619862000, + 1633619877000, 1633619889000 ], [ - 0.0146153, - 0.0118506, - 0.0473847, - 0.026997, - 0.0164318, - 0.0113532, - 0.0105197, - 0.162789, - 0.0556026, - 0.148856, - 0.0433809, - 0.0117758, - 0.0114496, - 0.0114099, - 0.0421927, - 0.0134148, - 0.0152827, - 0.6975967, - 0.0394788, - 0.0137441, - 0.0110939, - 0.0104496, - 0.0101284 + 0.0146153, 0.0118506, 0.0473847, 0.026997, 0.0164318, 0.0113532, 0.0105197, 0.162789, 0.0556026, + 0.148856, 0.0433809, 0.0117758, 0.0114496, 0.0114099, 0.0421927, 0.0134148, 0.0152827, 0.6975967, + 0.0394788, 0.0137441, 0.0110939, 0.0104496, 0.0101284 ], [ "app:80", diff --git a/packages/grafana-o11y-ds-frontend/tsconfig.json b/packages/grafana-o11y-ds-frontend/tsconfig.json index 8cafcb9c417..03aa83f469f 100644 --- a/packages/grafana-o11y-ds-frontend/tsconfig.json +++ b/packages/grafana-o11y-ds-frontend/tsconfig.json @@ -4,9 +4,9 @@ "declarationDir": "./compiled", "emitDeclarationOnly": true, "isolatedModules": true, - "rootDirs": ["."], + "rootDirs": ["."] }, "exclude": ["dist/**/*"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"], + "include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"] } diff --git a/packages/grafana-sql/tsconfig.json b/packages/grafana-sql/tsconfig.json index 5840ddbcdb5..a2f6548df35 100644 --- a/packages/grafana-sql/tsconfig.json +++ b/packages/grafana-sql/tsconfig.json @@ -5,9 +5,9 @@ "emitDeclarationOnly": true, "isolatedModules": true, "strict": true, - "rootDirs": ["."], + "rootDirs": ["."] }, "exclude": ["dist/**/*"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"], + "include": ["src/**/*.ts*", "../../public/app/types/*.d.ts", "../grafana-ui/src/types/*.d.ts"] } diff --git a/public/app/plugins/datasource/azuremonitor/tsconfig.json b/public/app/plugins/datasource/azuremonitor/tsconfig.json index 9ace7d78dbf..7daf2ee8aba 100644 --- a/public/app/plugins/datasource/azuremonitor/tsconfig.json +++ b/public/app/plugins/datasource/azuremonitor/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@grafana/plugin-configs/tsconfig.json", - "include": ["."], + "include": ["."] } diff --git a/public/app/plugins/datasource/cloud-monitoring/tsconfig.json b/public/app/plugins/datasource/cloud-monitoring/tsconfig.json index 9ace7d78dbf..7daf2ee8aba 100644 --- a/public/app/plugins/datasource/cloud-monitoring/tsconfig.json +++ b/public/app/plugins/datasource/cloud-monitoring/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@grafana/plugin-configs/tsconfig.json", - "include": ["."], + "include": ["."] } diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/tsconfig.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/tsconfig.json index 9ace7d78dbf..7daf2ee8aba 100644 --- a/public/app/plugins/datasource/grafana-pyroscope-datasource/tsconfig.json +++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@grafana/plugin-configs/tsconfig.json", - "include": ["."], + "include": ["."] } diff --git a/public/app/plugins/datasource/parca/tsconfig.json b/public/app/plugins/datasource/parca/tsconfig.json index 9ace7d78dbf..7daf2ee8aba 100644 --- a/public/app/plugins/datasource/parca/tsconfig.json +++ b/public/app/plugins/datasource/parca/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "@grafana/plugin-configs/tsconfig.json", - "include": ["."], + "include": ["."] } diff --git a/public/app/plugins/datasource/tempo/tsconfig.json b/public/app/plugins/datasource/tempo/tsconfig.json index 334e3949dfb..6dc8a770cba 100644 --- a/public/app/plugins/datasource/tempo/tsconfig.json +++ b/public/app/plugins/datasource/tempo/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "types": ["node", "jest", "@testing-library/jest-dom"], + "types": ["node", "jest", "@testing-library/jest-dom"] }, "extends": "@grafana/plugin-configs/tsconfig.json", - "include": ["."], + "include": ["."] } diff --git a/tsconfig.json b/tsconfig.json index 5f95b68a673..4949f581e7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,8 +11,8 @@ "incremental": true, "tsBuildInfoFile": "./tsconfig.tsbuildinfo", "paths": { - "@grafana/schema/dist/esm/*": ["../packages/grafana-schema/src/*"], - }, + "@grafana/schema/dist/esm/*": ["../packages/grafana-schema/src/*"] + } }, "extends": "@grafana/tsconfig/base.json", "include": [ @@ -21,7 +21,7 @@ "public/test/**/*.ts", "public/vendor/**/*.ts", "packages/grafana-data/typings", - "packages/grafana-ui/src/types", + "packages/grafana-ui/src/types" ], - "exclude": ["public/app/**/webpack.config.ts"], + "exclude": ["public/app/**/webpack.config.ts"] } From e0bff6247c014b83e258cca77ddfd1836d85d033 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 9 Feb 2024 11:46:23 +0000 Subject: [PATCH 07/50] Chore: ignore `loader-utils` update (#82236) ignore loader-utils --- .github/renovate.json5 | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 065b17028d8..7ebf4542543 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -6,6 +6,7 @@ "ignoreDeps": [ "history", // we should bump this together with react-router-dom (see https://github.com/grafana/grafana/issues/76744) "react-router-dom", // we should bump this together with history (see https://github.com/grafana/grafana/issues/76744) + "loader-utils", // v3 requires upstream changes in ngtemplate-loader. ignore, and remove when we remove angular. "monaco-editor", // due to us exposing this via @grafana/ui/CodeEditor's props bumping can break plugins "@fingerprintjs/fingerprintjs", // we don't want to bump to v4 due to licensing changes ], From 5a5520b5dafa08d139f60a3dda852f246e0d2791 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:01:58 +0000 Subject: [PATCH 08/50] Dashboards: add delete variable flow to `VariableEditorForm` (#82149) * add delete variable flow to VariableEditorForm * adjust modal logic and replace HorizontalGroup with Stack * revert onDelete prop name --- .../settings/VariablesEditView.tsx | 5 +- .../settings/variables/VariableEditorForm.tsx | 155 ++++++++++-------- 2 files changed, 94 insertions(+), 66 deletions(-) diff --git a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx index 2f79d02a8df..d574cbdd5c5 100644 --- a/public/app/features/dashboard-scene/settings/VariablesEditView.tsx +++ b/public/app/features/dashboard-scene/settings/VariablesEditView.tsx @@ -207,6 +207,7 @@ function VariableEditorSettingsListView({ model }: SceneComponentProps ); } @@ -234,6 +235,7 @@ interface VariableEditorSettingsEditViewProps { dashboard: DashboardScene; onTypeChange: (variableType: EditableVariableType) => void; onGoBack: () => void; + onDelete: (variableName: string) => void; } function VariableEditorSettingsView({ @@ -243,6 +245,7 @@ function VariableEditorSettingsView({ dashboard, onTypeChange, onGoBack, + onDelete, }: VariableEditorSettingsEditViewProps) { const parentTab = pageNav.children!.find((p) => p.active)!; parentTab.parentItem = pageNav; @@ -255,7 +258,7 @@ function VariableEditorSettingsView({ return ( - + ); } diff --git a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx index 3038cb6fe04..4f0ab76a584 100644 --- a/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx +++ b/public/app/features/dashboard-scene/settings/variables/VariableEditorForm.tsx @@ -1,18 +1,19 @@ +import { css } from '@emotion/css'; import React, { FormEvent } from 'react'; import { useAsyncFn } from 'react-use'; import { lastValueFrom } from 'rxjs'; -import { SelectableValue } from '@grafana/data'; +import { GrafanaTheme2, SelectableValue } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; +import { reportInteraction } from '@grafana/runtime'; import { SceneVariable } from '@grafana/scenes'; import { VariableHide, defaultVariableModel } from '@grafana/schema'; -import { HorizontalGroup, Button, LoadingPlaceholder } from '@grafana/ui'; +import { Button, LoadingPlaceholder, ConfirmModal, ModalsController, Stack, useStyles2 } from '@grafana/ui'; import { VariableHideSelect } from 'app/features/dashboard-scene/settings/variables/components/VariableHideSelect'; import { VariableLegend } from 'app/features/dashboard-scene/settings/variables/components/VariableLegend'; import { VariableTextAreaField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextAreaField'; import { VariableTextField } from 'app/features/dashboard-scene/settings/variables/components/VariableTextField'; import { VariableValuesPreview } from 'app/features/dashboard-scene/settings/variables/components/VariableValuesPreview'; -import { ConfirmDeleteModal } from 'app/features/variables/editor/ConfirmDeleteModal'; import { VariableNameConstraints } from 'app/features/variables/editor/types'; import { VariableTypeSelect } from './components/VariableTypeSelect'; @@ -22,9 +23,11 @@ interface VariableEditorFormProps { variable: SceneVariable; onTypeChange: (type: EditableVariableType) => void; onGoBack: () => void; + onDelete: (variableName: string) => void; } -export function VariableEditorForm({ variable, onTypeChange, onGoBack }: VariableEditorFormProps) { +export function VariableEditorForm({ variable, onTypeChange, onGoBack, onDelete }: VariableEditorFormProps) { + const styles = useStyles2(getStyles); const { name, type, label, description, hide } = variable.useState(); const EditorToRender = isEditableVariableType(type) ? getVariableEditor(type) : undefined; const [runQueryState, onRunQuery] = useAsyncFn(async () => { @@ -42,78 +45,100 @@ export function VariableEditorForm({ variable, onTypeChange, onGoBack }: Variabl const onDescriptionBlur = (e: FormEvent) => variable.setState({ description: e.currentTarget.value }); const onHideChange = (hide: VariableHide) => variable.setState({ hide }); + const isHasVariableOptions = hasVariableOptions(variable); + const onDeleteVariable = (hideModal: () => void) => () => { + reportInteraction('Delete variable'); + onDelete(name); + hideModal(); + }; + return ( - <> -
- + + - General - - - + General + + + - + - {EditorToRender && } + {EditorToRender && } - {isHasVariableOptions && } + {isHasVariableOptions && } -
- - {/* */} - - - {isHasVariableOptions && ( +
+ + + {({ showModal, hideModal }) => ( )} - -
- - console.log('needs implementation')} - onDismiss={() => console.log('needs implementation')} - /> - + + + + {isHasVariableOptions && ( + + )} + +
+ ); } + +const getStyles = (theme: GrafanaTheme2) => ({ + buttonContainer: css({ + marginTop: theme.spacing(2), + }), +}); From b1dc505a2b8706a062496397992adb6c02a8c190 Mon Sep 17 00:00:00 2001 From: Misi Date: Fri, 9 Feb 2024 13:10:23 +0100 Subject: [PATCH 09/50] Auth: Validate admin assignment in SSO Settings (#82233) * Add validation for allowAssignGrafanaAdmin * Update default values * Do not render hidden fields * Change error message * Improve tests --------- Co-authored-by: Clarity-89 --- pkg/login/social/connectors/azuread_oauth.go | 5 +-- .../social/connectors/azuread_oauth_test.go | 27 ++++++++++++--- pkg/login/social/connectors/generic_oauth.go | 5 +-- .../social/connectors/generic_oauth_test.go | 32 +++++++++++++++--- pkg/login/social/connectors/github_oauth.go | 5 +-- .../social/connectors/github_oauth_test.go | 33 ++++++++++++++++--- pkg/login/social/connectors/gitlab_oauth.go | 5 +-- .../social/connectors/gitlab_oauth_test.go | 33 ++++++++++++++++--- pkg/login/social/connectors/google_oauth.go | 5 +-- .../social/connectors/google_oauth_test.go | 33 ++++++++++++++++--- .../social/connectors/grafana_com_oauth.go | 5 +-- .../connectors/grafana_com_oauth_test.go | 27 +++++++++++++-- pkg/login/social/connectors/okta_oauth.go | 5 +-- .../social/connectors/okta_oauth_test.go | 32 +++++++++++++++--- pkg/login/social/connectors/social_base.go | 9 +++-- pkg/services/ssosettings/api/api.go | 2 +- pkg/services/ssosettings/api/api_test.go | 14 ++++---- pkg/services/ssosettings/ssosettings.go | 5 +-- .../ssosettings/ssosettingsimpl/service.go | 5 +-- .../ssosettingsimpl/service_test.go | 31 ++++++++--------- .../ssosettingstests/reloadable_mock.go | 14 ++++---- .../ssosettingstests/service_mock.go | 14 ++++---- .../features/auth-config/FieldRenderer.tsx | 6 +++- public/app/features/auth-config/fields.tsx | 4 ++- public/app/features/auth-config/types.ts | 3 +- 25 files changed, 271 insertions(+), 88 deletions(-) diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index 712d0f145ba..435f1c209ff 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/ssosettings" @@ -185,13 +186,13 @@ func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettin return nil } -func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSettings) error { +func (s *SocialAzureAD) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error { info, err := CreateOAuthInfoFromKeyValues(settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - err = validateInfo(info) + err = validateInfo(info, requester) if err != nil { return err } diff --git a/pkg/login/social/connectors/azuread_oauth_test.go b/pkg/login/social/connectors/azuread_oauth_test.go index e59803135b2..39da9b7f76e 100644 --- a/pkg/login/social/connectors/azuread_oauth_test.go +++ b/pkg/login/social/connectors/azuread_oauth_test.go @@ -18,10 +18,12 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -992,9 +994,10 @@ func TestSocialAzureAD_InitializeExtraFields(t *testing.T) { func TestSocialAzureAD_Validate(t *testing.T) { testCases := []struct { - name string - settings ssoModels.SSOSettings - wantErr error + name string + settings ssoModels.SSOSettings + requester identity.Requester + wantErr error }{ { name: "SSOSettings is valid", @@ -1052,13 +1055,29 @@ func TestSocialAzureAD_Validate(t *testing.T) { }, wantErr: ssosettings.ErrBaseInvalidOAuthConfig, }, + { + name: "fails if the user is not allowed to update allow assign grafana admin", + requester: &user.SignedInUser{ + IsGrafanaAdmin: false, + }, + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "allow_assign_grafana_admin": "true", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewAzureADProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures(), nil) - err := s.Validate(context.Background(), tc.settings) + if tc.requester == nil { + tc.requester = &user.SignedInUser{IsGrafanaAdmin: false} + } + err := s.Validate(context.Background(), tc.settings, tc.requester) if tc.wantErr != nil { require.ErrorIs(t, err, tc.wantErr) return diff --git a/pkg/login/social/connectors/generic_oauth.go b/pkg/login/social/connectors/generic_oauth.go index 3e1c3cecb38..a001aef73d4 100644 --- a/pkg/login/social/connectors/generic_oauth.go +++ b/pkg/login/social/connectors/generic_oauth.go @@ -13,6 +13,7 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" @@ -67,13 +68,13 @@ func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettin return provider } -func (s *SocialGenericOAuth) Validate(ctx context.Context, settings ssoModels.SSOSettings) error { +func (s *SocialGenericOAuth) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error { info, err := CreateOAuthInfoFromKeyValues(settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - err = validateInfo(info) + err = validateInfo(info, requester) if err != nil { return err } diff --git a/pkg/login/social/connectors/generic_oauth_test.go b/pkg/login/social/connectors/generic_oauth_test.go index edcc5eb76e3..bf991f18712 100644 --- a/pkg/login/social/connectors/generic_oauth_test.go +++ b/pkg/login/social/connectors/generic_oauth_test.go @@ -13,11 +13,13 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -920,17 +922,20 @@ func TestSocialGenericOAuth_InitializeExtraFields(t *testing.T) { func TestSocialGenericOAuth_Validate(t *testing.T) { testCases := []struct { - name string - settings ssoModels.SSOSettings - wantErr error + name string + settings ssoModels.SSOSettings + requester identity.Requester + wantErr error }{ { name: "SSOSettings is valid", settings: ssoModels.SSOSettings{ Settings: map[string]any{ - "client_id": "client-id", + "client_id": "client-id", + "allow_assign_grafana_admin": "true", }, }, + requester: &user.SignedInUser{IsGrafanaAdmin: true}, }, { name: "fails if settings map contains an invalid field", @@ -969,13 +974,30 @@ func TestSocialGenericOAuth_Validate(t *testing.T) { }, wantErr: ssosettings.ErrBaseInvalidOAuthConfig, }, + { + name: "fails if the user is not allowed to update allow assign grafana admin", + requester: &user.SignedInUser{ + IsGrafanaAdmin: false, + }, + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "allow_assign_grafana_admin": "true", + "skip_org_role_sync": "true", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewGenericOAuthProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - err := s.Validate(context.Background(), tc.settings) + if tc.requester == nil { + tc.requester = &user.SignedInUser{IsGrafanaAdmin: false} + } + err := s.Validate(context.Background(), tc.settings, tc.requester) if tc.wantErr != nil { require.ErrorIs(t, err, tc.wantErr) return diff --git a/pkg/login/social/connectors/github_oauth.go b/pkg/login/social/connectors/github_oauth.go index de7fc6def39..8e9bddfc316 100644 --- a/pkg/login/social/connectors/github_oauth.go +++ b/pkg/login/social/connectors/github_oauth.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" @@ -74,13 +75,13 @@ func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings sso return provider } -func (s *SocialGithub) Validate(ctx context.Context, settings ssoModels.SSOSettings) error { +func (s *SocialGithub) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error { info, err := CreateOAuthInfoFromKeyValues(settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - err = validateInfo(info) + err = validateInfo(info, requester) if err != nil { return err } diff --git a/pkg/login/social/connectors/github_oauth_test.go b/pkg/login/social/connectors/github_oauth_test.go index f0e09a85e07..9b27703da7d 100644 --- a/pkg/login/social/connectors/github_oauth_test.go +++ b/pkg/login/social/connectors/github_oauth_test.go @@ -12,10 +12,12 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -346,17 +348,20 @@ func TestSocialGitHub_InitializeExtraFields(t *testing.T) { func TestSocialGitHub_Validate(t *testing.T) { testCases := []struct { - name string - settings ssoModels.SSOSettings - wantErr error + name string + settings ssoModels.SSOSettings + requester identity.Requester + wantErr error }{ { name: "SSOSettings is valid", settings: ssoModels.SSOSettings{ Settings: map[string]any{ - "client_id": "client-id", + "client_id": "client-id", + "allow_assign_grafana_admin": "true", }, }, + requester: &user.SignedInUser{IsGrafanaAdmin: true}, }, { name: "fails if settings map contains an invalid field", @@ -405,13 +410,31 @@ func TestSocialGitHub_Validate(t *testing.T) { }, wantErr: ssosettings.ErrBaseInvalidOAuthConfig, }, + { + name: "fails if the user is not allowed to update allow assign grafana admin", + requester: &user.SignedInUser{ + IsGrafanaAdmin: false, + }, + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "allow_assign_grafana_admin": "true", + "skip_org_role_sync": "true", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewGitHubProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - err := s.Validate(context.Background(), tc.settings) + if tc.requester == nil { + tc.requester = &user.SignedInUser{IsGrafanaAdmin: false} + } + + err := s.Validate(context.Background(), tc.settings, tc.requester) if tc.wantErr != nil { require.ErrorIs(t, err, tc.wantErr) return diff --git a/pkg/login/social/connectors/gitlab_oauth.go b/pkg/login/social/connectors/gitlab_oauth.go index 4025c26d1bf..329b4d58c11 100644 --- a/pkg/login/social/connectors/gitlab_oauth.go +++ b/pkg/login/social/connectors/gitlab_oauth.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" @@ -64,13 +65,13 @@ func NewGitLabProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings sso return provider } -func (s *SocialGitlab) Validate(ctx context.Context, settings ssoModels.SSOSettings) error { +func (s *SocialGitlab) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error { info, err := CreateOAuthInfoFromKeyValues(settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - err = validateInfo(info) + err = validateInfo(info, requester) if err != nil { return err } diff --git a/pkg/login/social/connectors/gitlab_oauth_test.go b/pkg/login/social/connectors/gitlab_oauth_test.go index f2bf015da03..926bccaae9f 100644 --- a/pkg/login/social/connectors/gitlab_oauth_test.go +++ b/pkg/login/social/connectors/gitlab_oauth_test.go @@ -16,11 +16,13 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -464,17 +466,20 @@ func TestSocialGitlab_GetGroupsNextPage(t *testing.T) { func TestSocialGitlab_Validate(t *testing.T) { testCases := []struct { - name string - settings ssoModels.SSOSettings - wantErr error + name string + settings ssoModels.SSOSettings + requester identity.Requester + wantErr error }{ { name: "SSOSettings is valid", settings: ssoModels.SSOSettings{ Settings: map[string]any{ - "client_id": "client-id", + "client_id": "client-id", + "allow_assign_grafana_admin": "true", }, }, + requester: &user.SignedInUser{IsGrafanaAdmin: true}, }, { name: "fails if settings map contains an invalid field", @@ -513,13 +518,31 @@ func TestSocialGitlab_Validate(t *testing.T) { }, wantErr: ssosettings.ErrBaseInvalidOAuthConfig, }, + { + name: "fails if the user is not allowed to update allow assign grafana admin", + requester: &user.SignedInUser{ + IsGrafanaAdmin: false, + }, + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "allow_assign_grafana_admin": "true", + "skip_org_role_sync": "true", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewGitLabProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - err := s.Validate(context.Background(), tc.settings) + if tc.requester == nil { + tc.requester = &user.SignedInUser{IsGrafanaAdmin: false} + } + + err := s.Validate(context.Background(), tc.settings, tc.requester) if tc.wantErr != nil { require.ErrorIs(t, err, tc.wantErr) return diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index 1398f045397..6964a21dd6c 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -11,6 +11,7 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" @@ -54,13 +55,13 @@ func NewGoogleProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings sso return provider } -func (s *SocialGoogle) Validate(ctx context.Context, settings ssoModels.SSOSettings) error { +func (s *SocialGoogle) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error { info, err := CreateOAuthInfoFromKeyValues(settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - err = validateInfo(info) + err = validateInfo(info, requester) if err != nil { return err } diff --git a/pkg/login/social/connectors/google_oauth_test.go b/pkg/login/social/connectors/google_oauth_test.go index df66caa0d7c..055f416952f 100644 --- a/pkg/login/social/connectors/google_oauth_test.go +++ b/pkg/login/social/connectors/google_oauth_test.go @@ -16,10 +16,12 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -669,17 +671,20 @@ func TestSocialGoogle_UserInfo(t *testing.T) { func TestSocialGoogle_Validate(t *testing.T) { testCases := []struct { - name string - settings ssoModels.SSOSettings - wantErr error + name string + settings ssoModels.SSOSettings + requester identity.Requester + wantErr error }{ { name: "SSOSettings is valid", settings: ssoModels.SSOSettings{ Settings: map[string]any{ - "client_id": "client-id", + "client_id": "client-id", + "allow_assign_grafana_admin": "true", }, }, + requester: &user.SignedInUser{IsGrafanaAdmin: true}, }, { name: "fails if settings map contains an invalid field", @@ -718,13 +723,31 @@ func TestSocialGoogle_Validate(t *testing.T) { }, wantErr: ssosettings.ErrBaseInvalidOAuthConfig, }, + { + name: "fails if the user is not allowed to update allow assign grafana admin", + requester: &user.SignedInUser{ + IsGrafanaAdmin: false, + }, + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "allow_assign_grafana_admin": "true", + "skip_org_role_sync": "true", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewGoogleProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - err := s.Validate(context.Background(), tc.settings) + if tc.requester == nil { + tc.requester = &user.SignedInUser{IsGrafanaAdmin: false} + } + + err := s.Validate(context.Background(), tc.settings, tc.requester) if tc.wantErr != nil { require.ErrorIs(t, err, tc.wantErr) return diff --git a/pkg/login/social/connectors/grafana_com_oauth.go b/pkg/login/social/connectors/grafana_com_oauth.go index 60c3d23e51e..1db16e06c29 100644 --- a/pkg/login/social/connectors/grafana_com_oauth.go +++ b/pkg/login/social/connectors/grafana_com_oauth.go @@ -10,6 +10,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/ssosettings" @@ -52,13 +53,13 @@ func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings return provider } -func (s *SocialGrafanaCom) Validate(ctx context.Context, settings ssoModels.SSOSettings) error { +func (s *SocialGrafanaCom) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error { info, err := CreateOAuthInfoFromKeyValues(settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - err = validateInfo(info) + err = validateInfo(info, requester) if err != nil { return err } diff --git a/pkg/login/social/connectors/grafana_com_oauth_test.go b/pkg/login/social/connectors/grafana_com_oauth_test.go index bdcf15e081b..dc800775f89 100644 --- a/pkg/login/social/connectors/grafana_com_oauth_test.go +++ b/pkg/login/social/connectors/grafana_com_oauth_test.go @@ -10,9 +10,11 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -139,15 +141,18 @@ func TestSocialGrafanaCom_Validate(t *testing.T) { testCases := []struct { name string settings ssoModels.SSOSettings + requester identity.Requester expectError bool }{ { name: "SSOSettings is valid", settings: ssoModels.SSOSettings{ Settings: map[string]any{ - "client_id": "client-id", + "client_id": "client-id", + "allow_assign_grafana_admin": "true", }, }, + requester: &user.SignedInUser{IsGrafanaAdmin: true}, expectError: false, }, { @@ -176,13 +181,31 @@ func TestSocialGrafanaCom_Validate(t *testing.T) { }, expectError: true, }, + { + name: "fails if the user is not allowed to update allow assign grafana admin", + requester: &user.SignedInUser{ + IsGrafanaAdmin: false, + }, + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "allow_assign_grafana_admin": "true", + "skip_org_role_sync": "true", + }, + }, + expectError: true, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewGrafanaComProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - err := s.Validate(context.Background(), tc.settings) + if tc.requester == nil { + tc.requester = &user.SignedInUser{IsGrafanaAdmin: false} + } + + err := s.Validate(context.Background(), tc.settings, tc.requester) if tc.expectError { require.Error(t, err) } else { diff --git a/pkg/login/social/connectors/okta_oauth.go b/pkg/login/social/connectors/okta_oauth.go index 39eb41a1bc6..8354ab1a20f 100644 --- a/pkg/login/social/connectors/okta_oauth.go +++ b/pkg/login/social/connectors/okta_oauth.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" @@ -60,13 +61,13 @@ func NewOktaProvider(info *social.OAuthInfo, cfg *setting.Cfg, ssoSettings ssose return provider } -func (s *SocialOkta) Validate(ctx context.Context, settings ssoModels.SSOSettings) error { +func (s *SocialOkta) Validate(ctx context.Context, settings ssoModels.SSOSettings, requester identity.Requester) error { info, err := CreateOAuthInfoFromKeyValues(settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - err = validateInfo(info) + err = validateInfo(info, requester) if err != nil { return err } diff --git a/pkg/login/social/connectors/okta_oauth_test.go b/pkg/login/social/connectors/okta_oauth_test.go index 2bddad48984..e390680a522 100644 --- a/pkg/login/social/connectors/okta_oauth_test.go +++ b/pkg/login/social/connectors/okta_oauth_test.go @@ -14,10 +14,12 @@ import ( "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/models/roletype" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" ssoModels "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -137,17 +139,20 @@ func TestSocialOkta_UserInfo(t *testing.T) { func TestSocialOkta_Validate(t *testing.T) { testCases := []struct { - name string - settings ssoModels.SSOSettings - wantErr error + name string + settings ssoModels.SSOSettings + requester identity.Requester + wantErr error }{ { name: "SSOSettings is valid", settings: ssoModels.SSOSettings{ Settings: map[string]any{ - "client_id": "client-id", + "client_id": "client-id", + "allow_assign_grafana_admin": "true", }, }, + requester: &user.SignedInUser{IsGrafanaAdmin: true}, }, { name: "fails if settings map contains an invalid field", @@ -186,13 +191,30 @@ func TestSocialOkta_Validate(t *testing.T) { }, wantErr: ssosettings.ErrBaseInvalidOAuthConfig, }, + { + name: "fails if the user is not allowed to update allow assign grafana admin", + requester: &user.SignedInUser{ + IsGrafanaAdmin: false, + }, + settings: ssoModels.SSOSettings{ + Settings: map[string]any{ + "client_id": "client-id", + "allow_assign_grafana_admin": "true", + "skip_org_role_sync": "true", + }, + }, + wantErr: ssosettings.ErrBaseInvalidOAuthConfig, + }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { s := NewOktaProvider(&social.OAuthInfo{}, &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - err := s.Validate(context.Background(), tc.settings) + if tc.requester == nil { + tc.requester = &user.SignedInUser{IsGrafanaAdmin: false} + } + err := s.Validate(context.Background(), tc.settings, tc.requester) if tc.wantErr != nil { require.ErrorIs(t, err, tc.wantErr) return diff --git a/pkg/login/social/connectors/social_base.go b/pkg/login/social/connectors/social_base.go index 31f8163cbba..b57d3c0dd5e 100644 --- a/pkg/login/social/connectors/social_base.go +++ b/pkg/login/social/connectors/social_base.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/ssosettings" @@ -220,9 +221,13 @@ func getRoleFromSearch(role string) (org.RoleType, bool) { return org.RoleType(cases.Title(language.Und).String(role)), false } -func validateInfo(info *social.OAuthInfo) error { +func validateInfo(info *social.OAuthInfo, requester identity.Requester) error { if info.ClientId == "" { - return ssosettings.ErrInvalidOAuthConfig("ClientId is empty") + return ssosettings.ErrInvalidOAuthConfig("Client Id is empty.") + } + + if info.AllowAssignGrafanaAdmin && !requester.GetIsGrafanaAdmin() { + return ssosettings.ErrInvalidOAuthConfig("Allow assign Grafana Admin can only be updated by Grafana Server Admins.") } if info.AllowAssignGrafanaAdmin && info.SkipOrgRoleSync { diff --git a/pkg/services/ssosettings/api/api.go b/pkg/services/ssosettings/api/api.go index 4ebc206a137..d45c2d0db57 100644 --- a/pkg/services/ssosettings/api/api.go +++ b/pkg/services/ssosettings/api/api.go @@ -178,7 +178,7 @@ func (api *Api) updateProviderSettings(c *contextmodel.ReqContext) response.Resp settings.Provider = key - err := api.SSOSettingsService.Upsert(c.Req.Context(), &settings) + err := api.SSOSettingsService.Upsert(c.Req.Context(), &settings, c.SignedInUser) if err != nil { return response.ErrOrFallback(http.StatusInternalServerError, "Failed to update provider settings", err) } diff --git a/pkg/services/ssosettings/api/api_test.go b/pkg/services/ssosettings/api/api_test.go index 3421c4e5dcb..2530f3cf0aa 100644 --- a/pkg/services/ssosettings/api/api_test.go +++ b/pkg/services/ssosettings/api/api_test.go @@ -132,19 +132,21 @@ func TestSSOSettingsAPI_Update(t *testing.T) { Settings: input.Settings, } + signedInUser := &user.SignedInUser{ + OrgRole: org.RoleAdmin, + OrgID: 1, + Permissions: getPermissionsForActionAndScope(tt.action, tt.scope), + } + service := ssosettingstests.NewMockService(t) if tt.expectedServiceCall { - service.On("Upsert", mock.Anything, &settings).Return(tt.expectedError).Once() + service.On("Upsert", mock.Anything, &settings, signedInUser).Return(tt.expectedError).Once() } server := setupTests(t, service) path := fmt.Sprintf("/api/v1/sso-settings/%s", tt.key) req := server.NewRequest(http.MethodPut, path, bytes.NewBufferString(tt.body)) - webtest.RequestWithSignedInUser(req, &user.SignedInUser{ - OrgRole: org.RoleEditor, - OrgID: 1, - Permissions: getPermissionsForActionAndScope(tt.action, tt.scope), - }) + webtest.RequestWithSignedInUser(req, signedInUser) res, err := server.SendJSON(req) require.NoError(t, err) diff --git a/pkg/services/ssosettings/ssosettings.go b/pkg/services/ssosettings/ssosettings.go index f31d30b4fde..be0bdaac718 100644 --- a/pkg/services/ssosettings/ssosettings.go +++ b/pkg/services/ssosettings/ssosettings.go @@ -4,6 +4,7 @@ import ( "context" "github.com/grafana/grafana/pkg/login/social" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/ssosettings/models" ) @@ -28,7 +29,7 @@ type Service interface { // GetForProviderWithRedactedSecrets returns the SSO settings for a given provider (DB or config file) with secret values redacted GetForProviderWithRedactedSecrets(ctx context.Context, provider string) (*models.SSOSettings, error) // Upsert creates or updates the SSO settings for a given provider - Upsert(ctx context.Context, settings *models.SSOSettings) error + Upsert(ctx context.Context, settings *models.SSOSettings, requester identity.Requester) error // Delete deletes the SSO settings for a given provider (soft delete) Delete(ctx context.Context, provider string) error // Patch updates the specified SSO settings (key-value pairs) for a given provider @@ -44,7 +45,7 @@ type Service interface { //go:generate mockery --name Reloadable --structname MockReloadable --outpkg ssosettingstests --filename reloadable_mock.go --output ./ssosettingstests/ type Reloadable interface { Reload(ctx context.Context, settings models.SSOSettings) error - Validate(ctx context.Context, settings models.SSOSettings) error + Validate(ctx context.Context, settings models.SSOSettings, requester identity.Requester) error } // FallbackStrategy is an interface that can be implemented to allow a provider to load settings from a different source diff --git a/pkg/services/ssosettings/ssosettingsimpl/service.go b/pkg/services/ssosettings/ssosettingsimpl/service.go index 02b1319dd79..9466848d7ce 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/ssosettings" @@ -164,7 +165,7 @@ func (s *Service) ListWithRedactedSecrets(ctx context.Context) ([]*models.SSOSet return configurableSettings, nil } -func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings) error { +func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings, requester identity.Requester) error { if !s.isProviderConfigurable(settings.Provider) { return ssosettings.ErrNotConfigurable } @@ -174,7 +175,7 @@ func (s *Service) Upsert(ctx context.Context, settings *models.SSOSettings) erro return ssosettings.ErrInvalidProvider.Errorf("provider %s not found in reloadables", settings.Provider) } - err := social.Validate(ctx, *settings) + err := social.Validate(ctx, *settings, requester) if err != nil { return err } diff --git a/pkg/services/ssosettings/ssosettingsimpl/service_test.go b/pkg/services/ssosettings/ssosettingsimpl/service_test.go index 333efa33a04..6f736c5479e 100644 --- a/pkg/services/ssosettings/ssosettingsimpl/service_test.go +++ b/pkg/services/ssosettings/ssosettingsimpl/service_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/services/ssosettings/models" "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingstests" + "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" ) @@ -796,7 +797,7 @@ func TestService_Upsert(t *testing.T) { wg.Add(1) reloadable := ssosettingstests.NewMockReloadable(t) - reloadable.On("Validate", mock.Anything, settings).Return(nil) + reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil) reloadable.On("Reload", mock.Anything, mock.MatchedBy(func(settings models.SSOSettings) bool { defer wg.Done() return settings.Provider == provider && @@ -830,7 +831,7 @@ func TestService_Upsert(t *testing.T) { }, }, nil } - err := env.service.Upsert(context.Background(), &settings) + err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) require.NoError(t, err) // Wait for the goroutine first to assert the Reload call @@ -859,7 +860,7 @@ func TestService_Upsert(t *testing.T) { reloadable := ssosettingstests.NewMockReloadable(t) env.reloadables[provider] = reloadable - err := env.service.Upsert(context.Background(), settings) + err := env.service.Upsert(context.Background(), settings, &user.SignedInUser{}) require.Error(t, err) }) @@ -883,7 +884,7 @@ func TestService_Upsert(t *testing.T) { // the reloadable is available for other provider env.reloadables["github"] = reloadable - err := env.service.Upsert(context.Background(), settings) + err := env.service.Upsert(context.Background(), settings, &user.SignedInUser{}) require.Error(t, err) }) @@ -904,10 +905,10 @@ func TestService_Upsert(t *testing.T) { } reloadable := ssosettingstests.NewMockReloadable(t) - reloadable.On("Validate", mock.Anything, settings).Return(errors.New("validation failed")) + reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(errors.New("validation failed")) env.reloadables[provider] = reloadable - err := env.service.Upsert(context.Background(), &settings) + err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) require.Error(t, err) }) @@ -928,7 +929,7 @@ func TestService_Upsert(t *testing.T) { env.fallbackStrategy.ExpectedIsMatch = false - err := env.service.Upsert(context.Background(), settings) + err := env.service.Upsert(context.Background(), settings, &user.SignedInUser{}) require.Error(t, err) }) @@ -949,11 +950,11 @@ func TestService_Upsert(t *testing.T) { } reloadable := ssosettingstests.NewMockReloadable(t) - reloadable.On("Validate", mock.Anything, settings).Return(nil) + reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil) env.reloadables[provider] = reloadable env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return(nil, errors.New("encryption failed")).Once() - err := env.service.Upsert(context.Background(), &settings) + err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) require.Error(t, err) }) @@ -981,13 +982,13 @@ func TestService_Upsert(t *testing.T) { } reloadable := ssosettingstests.NewMockReloadable(t) - reloadable.On("Validate", mock.Anything, settings).Return(nil) + reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil) reloadable.On("Reload", mock.Anything, mock.Anything).Return(nil).Maybe() env.reloadables[provider] = reloadable env.secrets.On("Decrypt", mock.Anything, []byte("current-client-secret"), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once() env.secrets.On("Encrypt", mock.Anything, []byte("encrypted-client-secret"), mock.Anything).Return([]byte("current-client-secret"), nil).Once() - err := env.service.Upsert(context.Background(), &settings) + err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) require.NoError(t, err) settings.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("current-client-secret")) @@ -1011,7 +1012,7 @@ func TestService_Upsert(t *testing.T) { } reloadable := ssosettingstests.NewMockReloadable(t) - reloadable.On("Validate", mock.Anything, settings).Return(nil) + reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil) env.reloadables[provider] = reloadable env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once() env.store.GetFn = func(ctx context.Context, provider string) (*models.SSOSettings, error) { @@ -1022,7 +1023,7 @@ func TestService_Upsert(t *testing.T) { return errors.New("failed to upsert settings") } - err := env.service.Upsert(context.Background(), &settings) + err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) require.Error(t, err) }) @@ -1043,12 +1044,12 @@ func TestService_Upsert(t *testing.T) { } reloadable := ssosettingstests.NewMockReloadable(t) - reloadable.On("Validate", mock.Anything, settings).Return(nil) + reloadable.On("Validate", mock.Anything, settings, mock.Anything).Return(nil) reloadable.On("Reload", mock.Anything, mock.Anything).Return(errors.New("failed reloading new settings")).Maybe() env.reloadables[provider] = reloadable env.secrets.On("Encrypt", mock.Anything, []byte(settings.Settings["client_secret"].(string)), mock.Anything).Return([]byte("encrypted-client-secret"), nil).Once() - err := env.service.Upsert(context.Background(), &settings) + err := env.service.Upsert(context.Background(), &settings, &user.SignedInUser{}) require.NoError(t, err) settings.Settings["client_secret"] = base64.RawStdEncoding.EncodeToString([]byte("encrypted-client-secret")) diff --git a/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go b/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go index a6c0526cfa4..599fe1c5c19 100644 --- a/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go +++ b/pkg/services/ssosettings/ssosettingstests/reloadable_mock.go @@ -5,8 +5,10 @@ package ssosettingstests import ( context "context" - models "github.com/grafana/grafana/pkg/services/ssosettings/models" + identity "github.com/grafana/grafana/pkg/services/auth/identity" mock "github.com/stretchr/testify/mock" + + models "github.com/grafana/grafana/pkg/services/ssosettings/models" ) // MockReloadable is an autogenerated mock type for the Reloadable type @@ -32,17 +34,17 @@ func (_m *MockReloadable) Reload(ctx context.Context, settings models.SSOSetting return r0 } -// Validate provides a mock function with given fields: ctx, settings -func (_m *MockReloadable) Validate(ctx context.Context, settings models.SSOSettings) error { - ret := _m.Called(ctx, settings) +// Validate provides a mock function with given fields: ctx, settings, requester +func (_m *MockReloadable) Validate(ctx context.Context, settings models.SSOSettings, requester identity.Requester) error { + ret := _m.Called(ctx, settings, requester) if len(ret) == 0 { panic("no return value specified for Validate") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, models.SSOSettings) error); ok { - r0 = rf(ctx, settings) + if rf, ok := ret.Get(0).(func(context.Context, models.SSOSettings, identity.Requester) error); ok { + r0 = rf(ctx, settings, requester) } else { r0 = ret.Error(0) } diff --git a/pkg/services/ssosettings/ssosettingstests/service_mock.go b/pkg/services/ssosettings/ssosettingstests/service_mock.go index 42ef1091fb9..02837917856 100644 --- a/pkg/services/ssosettings/ssosettingstests/service_mock.go +++ b/pkg/services/ssosettings/ssosettingstests/service_mock.go @@ -5,9 +5,11 @@ package ssosettingstests import ( context "context" - models "github.com/grafana/grafana/pkg/services/ssosettings/models" + identity "github.com/grafana/grafana/pkg/services/auth/identity" mock "github.com/stretchr/testify/mock" + models "github.com/grafana/grafana/pkg/services/ssosettings/models" + ssosettings "github.com/grafana/grafana/pkg/services/ssosettings" ) @@ -182,17 +184,17 @@ func (_m *MockService) Reload(ctx context.Context, provider string) { _m.Called(ctx, provider) } -// Upsert provides a mock function with given fields: ctx, settings -func (_m *MockService) Upsert(ctx context.Context, settings *models.SSOSettings) error { - ret := _m.Called(ctx, settings) +// Upsert provides a mock function with given fields: ctx, settings, requester +func (_m *MockService) Upsert(ctx context.Context, settings *models.SSOSettings, requester identity.Requester) error { + ret := _m.Called(ctx, settings, requester) if len(ret) == 0 { panic("no return value specified for Upsert") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, *models.SSOSettings) error); ok { - r0 = rf(ctx, settings) + if rf, ok := ret.Get(0).(func(context.Context, *models.SSOSettings, identity.Requester) error); ok { + r0 = rf(ctx, settings, requester) } else { r0 = ret.Error(0) } diff --git a/public/app/features/auth-config/FieldRenderer.tsx b/public/app/features/auth-config/FieldRenderer.tsx index 4c74ff5c0c8..856ac56b252 100644 --- a/public/app/features/auth-config/FieldRenderer.tsx +++ b/public/app/features/auth-config/FieldRenderer.tsx @@ -47,6 +47,10 @@ export const FieldRenderer = ({ return null; } + if (!!fieldData.hidden) { + return null; + } + // Dependant field means the field depends on another field's value and shouldn't be rendered if the parent field is false if (isDependantField) { const parentValue = watch(field.dependsOn); @@ -61,7 +65,7 @@ export const FieldRenderer = ({ error: fieldData.validation?.message, key: name, description: fieldData.description, - defaultValue: fieldData.defaultValue, + defaultValue: fieldData.defaultValue?.value, }; switch (fieldData.type) { diff --git a/public/app/features/auth-config/fields.tsx b/public/app/features/auth-config/fields.tsx index 813523ffd6b..1d174f59ed0 100644 --- a/public/app/features/auth-config/fields.tsx +++ b/public/app/features/auth-config/fields.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { validate as uuidValidate } from 'uuid'; import { TextLink } from '@grafana/ui'; +import { contextSrv } from 'app/core/core'; import { FieldData, SSOProvider, SSOSettingsField } from './types'; import { isSelectableValue } from './utils/guards'; @@ -151,7 +152,7 @@ export function fieldMap(provider: string): Record { { value: 'InParams', label: 'InParams' }, { value: 'InHeader', label: 'InHeader' }, ], - defaultValue: 'AutoDetect', + defaultValue: { value: 'AutoDetect', label: 'AutoDetect' }, }, tokenUrl: { label: 'Token URL', @@ -280,6 +281,7 @@ export function fieldMap(provider: string): Record { label: 'Allow assign Grafana admin', description: 'If enabled, it will automatically sync the Grafana server administrator role.', type: 'switch', + hidden: !contextSrv.isGrafanaAdmin, }, skipOrgRoleSync: { label: 'Skip organization role sync', diff --git a/public/app/features/auth-config/types.ts b/public/app/features/auth-config/types.ts index eb385fa7183..015101b2882 100644 --- a/public/app/features/auth-config/types.ts +++ b/public/app/features/auth-config/types.ts @@ -113,7 +113,8 @@ export type FieldData = { allowCustomValue?: boolean; options?: Array>; placeholder?: string; - defaultValue?: string; + defaultValue?: SelectableValue; + hidden?: boolean; }; export type SSOSettingsField = From 48b4ca82283dce0f159c5a3da26528cb578c73fb Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Fri, 9 Feb 2024 13:11:08 +0100 Subject: [PATCH 10/50] Elasticsearch: Decouple frontend dependencies from core (#82179) * Elasticsearch: Decouple frontend dependencies from core * Remove not needed code change --- .eslintrc | 4 +- .../elasticsearch/ElasticResponse.test.ts | 4 +- .../elasticsearch/ElasticResponse.ts | 5 +- .../elasticsearch/IndexPattern.test.ts | 2 - .../DateHistogramSettingsEditor.test.tsx | 9 +- .../DateHistogramSettingsEditor.tsx | 2 +- .../TermsSettingsEditor.test.tsx | 3 +- .../SettingsEditor/useDescription.ts | 3 +- .../state/reducer.test.ts | 9 +- .../state/reducer.test.ts | 3 +- .../SettingsEditor/SettingField.tsx | 2 +- .../state/reducer.test.ts | 7 +- .../components/QueryEditor/state.test.ts | 3 +- .../elasticsearch/components/reducerTester.ts | 109 ++++++++++++++++++ .../configuration/ConfigEditor.test.tsx | 2 +- .../configuration/ConfigEditor.tsx | 2 +- .../elasticsearch/configuration/DataLink.tsx | 2 +- .../configuration/ElasticDetails.test.tsx | 2 +- .../configuration/LogsConfig.test.tsx | 2 +- .../configuration/__mocks__/configOptions.ts | 17 +++ .../elasticsearch/configuration/mocks.ts | 20 ---- .../elasticsearch/datasource.test.ts | 59 ++++++---- .../datasource/elasticsearch/datasource.ts | 5 +- .../datasource/elasticsearch/tracking.ts | 2 +- .../datasource/elasticsearch/utils.test.ts | 38 +++++- .../plugins/datasource/elasticsearch/utils.ts | 50 ++++++++ 26 files changed, 282 insertions(+), 84 deletions(-) create mode 100644 public/app/plugins/datasource/elasticsearch/components/reducerTester.ts create mode 100644 public/app/plugins/datasource/elasticsearch/configuration/__mocks__/configOptions.ts delete mode 100644 public/app/plugins/datasource/elasticsearch/configuration/mocks.ts diff --git a/.eslintrc b/.eslintrc index 0ba27fd484e..e8cfd522609 100644 --- a/.eslintrc +++ b/.eslintrc @@ -113,7 +113,9 @@ "public/app/plugins/datasource/tempo/*.{ts,tsx}", "public/app/plugins/datasource/tempo/**/*.{ts,tsx}", "public/app/plugins/datasource/loki/*.{ts,tsx}", - "public/app/plugins/datasource/loki/**/*.{ts,tsx}" + "public/app/plugins/datasource/loki/**/*.{ts,tsx}", + "public/app/plugins/datasource/elasticsearch/*.{ts,tsx}", + "public/app/plugins/datasource/elasticsearch/**/*.{ts,tsx}" ], "settings": { "import/resolver": { diff --git a/public/app/plugins/datasource/elasticsearch/ElasticResponse.test.ts b/public/app/plugins/datasource/elasticsearch/ElasticResponse.test.ts index e72e6b87a8c..0f8a9aa059d 100644 --- a/public/app/plugins/datasource/elasticsearch/ElasticResponse.test.ts +++ b/public/app/plugins/datasource/elasticsearch/ElasticResponse.test.ts @@ -1,9 +1,9 @@ import { DataFrame, DataFrameView, Field, FieldCache, FieldType, KeyValue, MutableDataFrame } from '@grafana/data'; -import flatten from 'app/core/utils/flatten'; import { ElasticResponse } from './ElasticResponse'; import { highlightTags } from './queryDef'; import { ElasticsearchQuery } from './types'; +import { flattenObject } from './utils'; function getTimeField(frame: DataFrame): Field { const field = frame.fields[0]; @@ -1445,7 +1445,7 @@ describe('ElasticResponse', () => { expect(r._id).toEqual(response.responses[0].hits.hits[i]._id); expect(r._type).toEqual(response.responses[0].hits.hits[i]._type); expect(r._index).toEqual(response.responses[0].hits.hits[i]._index); - expect(r._source).toEqual(flatten(response.responses[0].hits.hits[i]._source)); + expect(r._source).toEqual(flattenObject(response.responses[0].hits.hits[i]._source)); } // Make a map from the histogram results diff --git a/public/app/plugins/datasource/elasticsearch/ElasticResponse.ts b/public/app/plugins/datasource/elasticsearch/ElasticResponse.ts index 46994b11ba6..07ef4a2645a 100644 --- a/public/app/plugins/datasource/elasticsearch/ElasticResponse.ts +++ b/public/app/plugins/datasource/elasticsearch/ElasticResponse.ts @@ -10,13 +10,12 @@ import { } from '@grafana/data'; import { convertFieldType } from '@grafana/data/src/transformations/transformers/convertFieldType'; import TableModel from 'app/core/TableModel'; -import flatten from 'app/core/utils/flatten'; import { isMetricAggregationWithField } from './components/QueryEditor/MetricAggregationsEditor/aggregations'; import { metricAggregationConfig } from './components/QueryEditor/MetricAggregationsEditor/utils'; import * as queryDef from './queryDef'; import { ElasticsearchAggregation, ElasticsearchQuery, TopMetrics, ExtendedStatMetaType } from './types'; -import { describeMetric, getScriptValue } from './utils'; +import { describeMetric, flattenObject, getScriptValue } from './utils'; const HIGHLIGHT_TAGS_EXP = `${queryDef.highlightTags.pre}([^@]+)${queryDef.highlightTags.post}`; type TopMetricMetric = Record; @@ -678,7 +677,7 @@ const flattenHits = (hits: Doc[]): { docs: Array>; propNames let propNames: string[] = []; for (const hit of hits) { - const flattened = hit._source ? flatten(hit._source) : {}; + const flattened = hit._source ? flattenObject(hit._source) : {}; const doc = { _id: hit._id, _type: hit._type, diff --git a/public/app/plugins/datasource/elasticsearch/IndexPattern.test.ts b/public/app/plugins/datasource/elasticsearch/IndexPattern.test.ts index 38ecf5c07fa..30ee68f7be3 100644 --- a/public/app/plugins/datasource/elasticsearch/IndexPattern.test.ts +++ b/public/app/plugins/datasource/elasticsearch/IndexPattern.test.ts @@ -1,5 +1,3 @@ -/// - import { toUtc, getLocale, setLocale, dateTime } from '@grafana/data'; import { IndexPattern } from './IndexPattern'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx index 9389f779bf7..52b45832fbf 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.test.tsx @@ -1,10 +1,9 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; -import { selectOptionInTest } from 'test/helpers/selectOptionInTest'; - -import { DateHistogram } from 'app/plugins/datasource/elasticsearch/types'; +import { select } from 'react-select-event'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; +import { DateHistogram } from '../../../../types'; import { DateHistogramSettingsEditor } from './DateHistogramSettingsEditor'; @@ -63,7 +62,7 @@ describe('DateHistogramSettingsEditor', () => { expect(await screen.findByText('Calendar interval')).toBeInTheDocument(); expect(await screen.findByText('1w')).toBeInTheDocument(); - await selectOptionInTest(screen.getByLabelText('Calendar interval'), '10s'); + await select(screen.getByLabelText('Calendar interval'), '10s', { container: document.body }); expect(dispatch).toHaveBeenCalledTimes(1); }); @@ -79,7 +78,7 @@ describe('DateHistogramSettingsEditor', () => { expect(await screen.findByText('Fixed interval')).toBeInTheDocument(); expect(await screen.findByText('1m')).toBeInTheDocument(); - await selectOptionInTest(screen.getByLabelText('Fixed interval'), '1q'); + await select(screen.getByLabelText('Fixed interval'), '1q', { container: document.body }); expect(dispatch).toHaveBeenCalledTimes(1); }); diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx index 7fd36121783..52a3081ac19 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/DateHistogramSettingsEditor.tsx @@ -4,8 +4,8 @@ import { GroupBase, OptionsOrGroups } from 'react-select'; import { InternalTimeZones, SelectableValue } from '@grafana/data'; import { InlineField, Input, Select, TimeZonePicker } from '@grafana/ui'; -import { calendarIntervals } from 'app/plugins/datasource/elasticsearch/QueryBuilder'; +import { calendarIntervals } from '../../../../QueryBuilder'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { DateHistogram } from '../../../../types'; import { useCreatableSelectPersistedBehaviour } from '../../../hooks/useCreatableSelectPersistedBehaviour'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx index 87dedea1f6d..97442a66f35 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/TermsSettingsEditor.test.tsx @@ -2,10 +2,9 @@ import { screen } from '@testing-library/react'; import React from 'react'; import selectEvent from 'react-select-event'; -import { describeMetric } from 'app/plugins/datasource/elasticsearch/utils'; - import { renderWithESProvider } from '../../../../test-helpers/render'; import { ElasticsearchQuery, Terms, Average, Derivative, TopMetrics } from '../../../../types'; +import { describeMetric } from '../../../../utils'; import { TermsSettingsEditor } from './TermsSettingsEditor'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts index 5def15faa55..282df98b144 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/SettingsEditor/useDescription.ts @@ -1,5 +1,4 @@ -import { defaultGeoHashPrecisionString } from 'app/plugins/datasource/elasticsearch/queryDef'; - +import { defaultGeoHashPrecisionString } from '../../../../queryDef'; import { BucketAggregation } from '../../../../types'; import { describeMetric, convertOrderByToMetricId } from '../../../../utils'; import { useQuery } from '../../ElasticsearchQueryContext'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts index b39166c1093..bdb9616b91d 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/BucketAggregationsEditor/state/reducer.test.ts @@ -1,9 +1,6 @@ -import { reducerTester } from 'test/core/redux/reducerTester'; - -import { defaultBucketAgg } from 'app/plugins/datasource/elasticsearch/queryDef'; -import { ElasticsearchQuery } from 'app/plugins/datasource/elasticsearch/types'; - -import { BucketAggregation, DateHistogram } from '../../../../types'; +import { defaultBucketAgg } from '../../../../queryDef'; +import { BucketAggregation, DateHistogram, ElasticsearchQuery } from '../../../../types'; +import { reducerTester } from '../../../reducerTester'; import { changeMetricType } from '../../MetricAggregationsEditor/state/actions'; import { initQuery } from '../../state'; import { bucketAggregationConfig } from '../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts index f21388ffc7f..b9ecf0749e7 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/BucketScriptSettingsEditor/state/reducer.test.ts @@ -1,6 +1,5 @@ -import { reducerTester } from 'test/core/redux/reducerTester'; - import { PipelineVariable } from '../../../../../../types'; +import { reducerTester } from '../../../../../reducerTester'; import { addPipelineVariable, diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx index 8d58b2960d1..5a0d48924c1 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/SettingsEditor/SettingField.tsx @@ -2,10 +2,10 @@ import { uniqueId } from 'lodash'; import React, { ComponentProps, useState } from 'react'; import { InlineField, Input } from '@grafana/ui'; -import { getScriptValue } from 'app/plugins/datasource/elasticsearch/utils'; import { useDispatch } from '../../../../hooks/useStatelessReducer'; import { MetricAggregationWithInlineScript, MetricAggregationWithSettings } from '../../../../types'; +import { getScriptValue } from '../../../../utils'; import { SettingKeyOf } from '../../../types'; import { changeMetricSetting } from '../state/actions'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts index 246642b7b5e..0719b2024c8 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/MetricAggregationsEditor/state/reducer.test.ts @@ -1,9 +1,6 @@ -import { reducerTester } from 'test/core/redux/reducerTester'; - -import { ElasticsearchQuery } from 'app/plugins/datasource/elasticsearch/types'; - import { defaultMetricAgg } from '../../../../queryDef'; -import { Derivative, ExtendedStats, MetricAggregation } from '../../../../types'; +import { Derivative, ElasticsearchQuery, ExtendedStats, MetricAggregation } from '../../../../types'; +import { reducerTester } from '../../../reducerTester'; import { initQuery } from '../../state'; import { metricAggregationConfig } from '../utils'; diff --git a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts index bb7ea5a161f..987551baf51 100644 --- a/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts +++ b/public/app/plugins/datasource/elasticsearch/components/QueryEditor/state.test.ts @@ -1,6 +1,5 @@ -import { reducerTester } from 'test/core/redux/reducerTester'; - import { ElasticsearchQuery } from '../../types'; +import { reducerTester } from '../reducerTester'; import { aliasPatternReducer, changeAliasPattern, changeQuery, initQuery, queryReducer } from './state'; diff --git a/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts new file mode 100644 index 00000000000..38c466afa2b --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/components/reducerTester.ts @@ -0,0 +1,109 @@ +import { AnyAction } from '@reduxjs/toolkit'; +import { cloneDeep } from 'lodash'; +import { Action } from 'redux'; + +import { StoreState } from 'app/types'; + +type GrafanaReducer = (state: S, action: A) => S; + +export interface Given { + givenReducer: ( + reducer: GrafanaReducer, + state: State, + showDebugOutput?: boolean, + disableDeepFreeze?: boolean + ) => When; +} + +export interface When { + whenActionIsDispatched: (action: AnyAction) => Then; +} + +export interface Then { + thenStateShouldEqual: (state: State) => When; + thenStatePredicateShouldEqual: (predicate: (resultingState: State) => boolean) => When; + whenActionIsDispatched: (action: AnyAction) => Then; +} + +const isNotException = (object: unknown, propertyName: string) => + typeof object === 'function' + ? propertyName !== 'caller' && propertyName !== 'callee' && propertyName !== 'arguments' + : true; + +export const deepFreeze = (obj: T): T => { + if (typeof obj === 'object') { + for (const key in obj) { + const prop = obj[key]; + + if ( + prop && + Object.hasOwn(obj, key) && + isNotException(obj, key) && + (typeof prop === 'object' || typeof prop === 'function') && + !Object.isFrozen(prop) + ) { + deepFreeze(prop); + } + } + } + + return Object.freeze(obj); +}; + +interface ReducerTester extends Given, When, Then {} + +export const reducerTester = (): Given => { + let reducerUnderTest: GrafanaReducer; + let resultingState: State; + let initialState: State; + let showDebugOutput = false; + + const givenReducer = ( + reducer: GrafanaReducer, + state: State, + debug = false, + disableDeepFreeze = false + ): When => { + reducerUnderTest = reducer; + initialState = cloneDeep(state); + if (!disableDeepFreeze && (typeof state === 'object' || typeof state === 'function')) { + deepFreeze(initialState); + } + showDebugOutput = debug; + + return instance; + }; + + const whenActionIsDispatched = (action: AnyAction): Then => { + resultingState = reducerUnderTest(resultingState || initialState, action); + + return instance; + }; + + const thenStateShouldEqual = (state: State): When => { + if (showDebugOutput) { + console.log(JSON.stringify(resultingState, null, 2)); + } + expect(resultingState).toEqual(state); + + return instance; + }; + + const thenStatePredicateShouldEqual = (predicate: (resultingState: State) => boolean): When => { + if (showDebugOutput) { + console.log(JSON.stringify(resultingState, null, 2)); + } + expect(predicate(resultingState)).toBe(true); + + return instance; + }; + + const instance: ReducerTester = { + thenStateShouldEqual, + thenStatePredicateShouldEqual, + givenReducer, + whenActionIsDispatched, + }; + + return instance; +}; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx index 850cdb470df..8438017aa40 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.test.tsx @@ -2,7 +2,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; import { ConfigEditor } from './ConfigEditor'; -import { createDefaultConfigOptions } from './mocks'; +import { createDefaultConfigOptions } from './__mocks__/configOptions'; describe('ConfigEditor', () => { it('should render without error', () => { diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx index 296a4e24b28..1dc4aa84780 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ConfigEditor.tsx @@ -11,8 +11,8 @@ import { convertLegacyAuthProps, DataSourceDescription, } from '@grafana/experimental'; +import { config } from '@grafana/runtime'; import { Alert, SecureSocksProxySettings, Divider, Stack } from '@grafana/ui'; -import { config } from 'app/core/config'; import { ElasticsearchOptions } from '../types'; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/DataLink.tsx b/public/app/plugins/datasource/elasticsearch/configuration/DataLink.tsx index 87835825539..da2c231ad13 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/DataLink.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/DataLink.tsx @@ -3,6 +3,7 @@ import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { usePrevious } from 'react-use'; import { DataSourceInstanceSettings, VariableSuggestion } from '@grafana/data'; +import { DataSourcePicker } from '@grafana/runtime'; import { Button, DataLinkInput, @@ -13,7 +14,6 @@ import { Input, useStyles2, } from '@grafana/ui'; -import { DataSourcePicker } from 'app/features/datasources/components/picker/DataSourcePicker'; import { DataLinkConfig } from '../types'; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx index 42c69cd9cd9..0b7af77201d 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/ElasticDetails.test.tsx @@ -3,7 +3,7 @@ import React from 'react'; import selectEvent from 'react-select-event'; import { ElasticDetails } from './ElasticDetails'; -import { createDefaultConfigOptions } from './mocks'; +import { createDefaultConfigOptions } from './__mocks__/configOptions'; describe('ElasticDetails', () => { describe('Max concurrent Shard Requests', () => { diff --git a/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx index 861ed7cdfc1..97d3d130398 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/LogsConfig.test.tsx @@ -2,7 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react'; import React from 'react'; import { LogsConfig } from './LogsConfig'; -import { createDefaultConfigOptions } from './mocks'; +import { createDefaultConfigOptions } from './__mocks__/configOptions'; describe('ElasticDetails', () => { it('should pass correct data to onChange', () => { diff --git a/public/app/plugins/datasource/elasticsearch/configuration/__mocks__/configOptions.ts b/public/app/plugins/datasource/elasticsearch/configuration/__mocks__/configOptions.ts new file mode 100644 index 00000000000..8f7e7963ceb --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/configuration/__mocks__/configOptions.ts @@ -0,0 +1,17 @@ +import { DataSourceSettings } from '@grafana/data'; + +import { ElasticsearchOptions } from '../../types'; + +export function createDefaultConfigOptions(): DataSourceSettings { + return { + jsonData: { + timeField: '@time', + interval: 'Hourly', + timeInterval: '10s', + maxConcurrentShardRequests: 300, + logMessageField: 'test.message', + logLevelField: 'test.level', + }, + secureJsonFields: {}, + } as DataSourceSettings; +} diff --git a/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts b/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts deleted file mode 100644 index c716a4d03ab..00000000000 --- a/public/app/plugins/datasource/elasticsearch/configuration/mocks.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { DataSourceSettings } from '@grafana/data'; -import { getMockDataSource } from 'app/features/datasources/__mocks__'; - -import { ElasticsearchOptions } from '../types'; - -export function createDefaultConfigOptions( - options?: Partial -): DataSourceSettings { - return getMockDataSource({ - jsonData: { - timeField: '@time', - interval: 'Hourly', - timeInterval: '10s', - maxConcurrentShardRequests: 300, - logMessageField: 'test.message', - logLevelField: 'test.level', - ...options, - }, - }); -} diff --git a/public/app/plugins/datasource/elasticsearch/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/datasource.test.ts index 0a7d8a57469..7fd3446dcfb 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.test.ts @@ -1,6 +1,5 @@ import { map } from 'lodash'; import { Observable, of, throwError } from 'rxjs'; -import { getQueryOptions } from 'test/helpers/getQueryOptions'; import { CoreApp, @@ -18,9 +17,6 @@ import { toUtc, } from '@grafana/data'; import { BackendSrvRequest, FetchResponse, reportInteraction, config } from '@grafana/runtime'; -import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__ - -import { createFetchResponse } from '../../../../test/helpers/createFetchResponse'; import { enhanceDataFrame } from './LegacyQueryRunner'; import { ElasticDatasource } from './datasource'; @@ -30,7 +26,9 @@ import { Filters, ElasticsearchOptions, ElasticsearchQuery } from './types'; const ELASTICSEARCH_MOCK_URL = 'http://elasticsearch.local'; const originalConsoleError = console.error; - +const backendSrv = { + fetch: jest.fn(), +}; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, @@ -44,6 +42,15 @@ jest.mock('@grafana/runtime', () => ({ }, })); +const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({ + from, + to, + raw: { + from, + to, + }, +}); + const TIME_START = [2022, 8, 21, 6, 10, 10]; const TIME_END = [2022, 8, 24, 6, 10, 21]; const DATAQUERY_BASE = { @@ -56,16 +63,22 @@ const DATAQUERY_BASE = { timezone: '', app: 'test', startTime: 0, + range: createTimeRange(toUtc(TIME_START), toUtc(TIME_END)), }; -const createTimeRange = (from: DateTime, to: DateTime): TimeRange => ({ - from, - to, - raw: { - from, - to, - }, -}); +function createFetchResponse(data: T): FetchResponse { + return { + data, + status: 200, + url: 'http://localhost:3000/api/ds/query', + config: { url: 'http://localhost:3000/api/ds/query' }, + type: 'basic', + statusText: 'Ok', + redirected: false, + headers: {} as unknown as Headers, + ok: true, + }; +} interface TestContext { data?: Data; @@ -977,27 +990,29 @@ describe('ElasticDatasource', () => { }); it('does not create a logs sample provider for non time series query', () => { - const options = getQueryOptions({ + const options: DataQueryRequest = { + ...DATAQUERY_BASE, targets: [ { refId: 'A', metrics: [{ type: 'logs', id: '1', settings: { limit: '100' } }], }, ], - }); + }; expect(ds.getSupplementaryRequest(SupplementaryQueryType.LogsSample, options)).not.toBeDefined(); }); it('does create a logs sample provider for time series query', () => { - const options = getQueryOptions({ + const options: DataQueryRequest = { + ...DATAQUERY_BASE, targets: [ { refId: 'A', bucketAggs: [{ type: 'date_histogram', id: '1' }], }, ], - }); + }; expect(ds.getSupplementaryRequest(SupplementaryQueryType.LogsSample, options)).toBeDefined(); }); @@ -1010,27 +1025,29 @@ describe('ElasticDatasource', () => { }); it("doesn't return a logs sample provider given a non time series query", () => { - const request = getQueryOptions({ + const request: DataQueryRequest = { + ...DATAQUERY_BASE, targets: [ { refId: 'A', metrics: [{ type: 'logs', id: '1', settings: { limit: '100' } }], }, ], - }); + }; expect(ds.getSupplementaryRequest(SupplementaryQueryType.LogsSample, request)).not.toBeDefined(); }); it('returns a logs sample provider given a time series query', () => { - const request = getQueryOptions({ + const request: DataQueryRequest = { + ...DATAQUERY_BASE, targets: [ { refId: 'A', bucketAggs: [{ type: 'date_histogram', id: '1' }], }, ], - }); + }; expect(ds.getSupplementaryRequest(SupplementaryQueryType.LogsSample, request)).toBeDefined(); }); diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 52169626b2a..7fab51613c9 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -38,6 +38,7 @@ import { AdHocVariableFilter, DataSourceWithQueryModificationSupport, AdHocVariableModel, + TypedVariableModel, } from '@grafana/data'; import { DataSourceWithBackend, @@ -47,7 +48,6 @@ import { TemplateSrv, getTemplateSrv, } from '@grafana/runtime'; -import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { IndexPattern, intervalMap } from './IndexPattern'; import LanguageProvider from './LanguageProvider'; @@ -264,7 +264,8 @@ export class ElasticDatasource private prepareAnnotationRequest(options: { annotation: ElasticsearchAnnotationQuery; - dashboard: DashboardModel; + // Should be DashboardModel but cannot import that here from the main app. This is a temporary solution as we need to move from deprecated annotations. + dashboard: { getVariables: () => TypedVariableModel[] }; range: TimeRange; }) { const annotation = options.annotation; diff --git a/public/app/plugins/datasource/elasticsearch/tracking.ts b/public/app/plugins/datasource/elasticsearch/tracking.ts index a4d25158ec7..01e3c106dfc 100644 --- a/public/app/plugins/datasource/elasticsearch/tracking.ts +++ b/public/app/plugins/datasource/elasticsearch/tracking.ts @@ -1,10 +1,10 @@ import { CoreApp, DashboardLoadedEvent, DataQueryRequest, DataQueryResponse } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; -import { variableRegex } from 'app/features/variables/utils'; import { REF_ID_STARTER_LOG_VOLUME } from './datasource'; import pluginJson from './plugin.json'; import { ElasticsearchAnnotationQuery, ElasticsearchQuery } from './types'; +import { variableRegex } from './utils'; type ElasticSearchOnDashboardLoadedTrackingEvent = { grafana_version?: string; diff --git a/public/app/plugins/datasource/elasticsearch/utils.test.ts b/public/app/plugins/datasource/elasticsearch/utils.test.ts index dcf10900c64..731efa9aefe 100644 --- a/public/app/plugins/datasource/elasticsearch/utils.test.ts +++ b/public/app/plugins/datasource/elasticsearch/utils.test.ts @@ -1,5 +1,5 @@ import { ElasticsearchQuery } from './types'; -import { isTimeSeriesQuery, removeEmpty } from './utils'; +import { flattenObject, isTimeSeriesQuery, removeEmpty } from './utils'; describe('removeEmpty', () => { it('Should remove all empty', () => { @@ -79,3 +79,39 @@ describe('isTimeSeriesQuery', () => { expect(isTimeSeriesQuery(query)).toBe(true); }); }); + +describe('flattenObject', () => { + it('flattens objects of arbitrary depth', () => { + const nestedObject = { + a: { + b: { + c: 1, + d: { + e: 2, + f: 3, + }, + }, + g: 4, + }, + h: 5, + }; + + expect(flattenObject(nestedObject)).toEqual({ + 'a.b.c': 1, + 'a.b.d.e': 2, + 'a.b.d.f': 3, + 'a.g': 4, + h: 5, + }); + }); + + it('does not alter other objects', () => { + const nestedObject = { + a: 'uno', + b: 'dos', + c: 3, + }; + + expect(flattenObject(nestedObject)).toEqual(nestedObject); + }); +}); diff --git a/public/app/plugins/datasource/elasticsearch/utils.ts b/public/app/plugins/datasource/elasticsearch/utils.ts index 63e3ac25feb..1ca679734cf 100644 --- a/public/app/plugins/datasource/elasticsearch/utils.ts +++ b/public/app/plugins/datasource/elasticsearch/utils.ts @@ -106,3 +106,53 @@ export const unsupportedVersionMessage = export const isTimeSeriesQuery = (query: ElasticsearchQuery): boolean => { return query?.bucketAggs?.slice(-1)[0]?.type === 'date_histogram'; }; + +/* + * This regex matches 3 types of variable reference with an optional format specifier + * There are 6 capture groups that replace will return + * \$(\w+) $var1 + * \[\[(\w+?)(?::(\w+))?\]\] [[var2]] or [[var2:fmt2]] + * \${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?} ${var3} or ${var3.fieldPath} or ${var3:fmt3} (or ${var3.fieldPath:fmt3} but that is not a separate capture group) + */ +export const variableRegex = /\$(\w+)|\[\[(\w+?)(?::(\w+))?\]\]|\${(\w+)(?:\.([^:^\}]+))?(?::([^\}]+))?}/g; + +// Copyright (c) 2014, Hugh Kennedy +// Based on code from https://github.com/hughsk/flat/blob/master/index.js +// +export function flattenObject( + target: Record, + opts?: { delimiter?: string; maxDepth?: number; safe?: boolean } +): Record { + opts = opts || {}; + + const delimiter = opts.delimiter || '.'; + let maxDepth = opts.maxDepth || 3; + let currentDepth = 1; + const output: Record = {}; + + function step(object: Record, prev: string | null) { + Object.keys(object).forEach((key) => { + const value = object[key]; + const isarray = opts?.safe && Array.isArray(value); + const type = Object.prototype.toString.call(value); + const isobject = type === '[object Object]'; + + const newKey = prev ? prev + delimiter + key : key; + + if (!opts?.maxDepth) { + maxDepth = currentDepth + 1; + } + + if (!isarray && isobject && value && Object.keys(value).length && currentDepth < maxDepth) { + ++currentDepth; + return step({ ...value }, newKey); + } + + output[newKey] = value; + }); + } + + step(target, null); + + return output; +} From bc83d8263b231154566b18786f26479cb62a9df3 Mon Sep 17 00:00:00 2001 From: Joao Silva <100691367+JoaoSilvaGrafana@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:37:28 +0000 Subject: [PATCH 11/50] Card: Add `isCompact` prop and `Overline` sub-component (#82077) --- .../grafana-ui/src/components/Card/Card.mdx | 18 ++++++++ .../src/components/Card/Card.story.tsx | 42 ++++++++++++------- .../grafana-ui/src/components/Card/Card.tsx | 40 +++++++++++++++++- .../src/components/Card/CardContainer.tsx | 8 ++-- 4 files changed, 89 insertions(+), 19 deletions(-) diff --git a/packages/grafana-ui/src/components/Card/Card.mdx b/packages/grafana-ui/src/components/Card/Card.mdx index 2e604d527f7..b6b1445de7f 100644 --- a/packages/grafana-ui/src/components/Card/Card.mdx +++ b/packages/grafana-ui/src/components/Card/Card.mdx @@ -445,6 +445,24 @@ Card can have a disabled state, effectively making it and its actions non-clicka +### With overline + +```jsx + + Filter option + Filter by name + Filter data by query. + +``` + + + + Filter option + Filter by name + Filter data by query. + + + ### Props diff --git a/packages/grafana-ui/src/components/Card/Card.story.tsx b/packages/grafana-ui/src/components/Card/Card.story.tsx index f6904421394..4d4f8367b17 100644 --- a/packages/grafana-ui/src/components/Card/Card.story.tsx +++ b/packages/grafana-ui/src/components/Card/Card.story.tsx @@ -24,9 +24,9 @@ const meta: Meta = { }, }; -export const Basic: StoryFn = ({ disabled }) => { +export const Basic: StoryFn = (args) => { return ( - + Filter by name Filter data by query. This is useful if you are sharing the results from a different panel that has many queries @@ -36,24 +36,24 @@ export const Basic: StoryFn = ({ disabled }) => { ); }; -export const AsLink: StoryFn = ({ disabled }) => { +export const AsLink: StoryFn = (args) => { return ( - + Filter by name Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel. - + Filter by name2 Filter data by query. This is useful if you are sharing the results from a different panel that has many queries and you want to only visualize a subset of that in this panel. - + Production system overview Meta tags @@ -61,9 +61,9 @@ export const AsLink: StoryFn = ({ disabled }) => { ); }; -export const WithTags: StoryFn = ({ disabled }) => { +export const WithTags: StoryFn = (args) => { return ( - + Elasticsearch – Custom Templated Query Elastic Search @@ -73,9 +73,9 @@ export const WithTags: StoryFn = ({ disabled }) => { ); }; -export const WithMedia: StoryFn = ({ disabled }) => { +export const WithMedia: StoryFn = (args) => { return ( - + 1-ops-tools1-fallback Prometheus @@ -89,9 +89,9 @@ export const WithMedia: StoryFn = ({ disabled }) => { ); }; -export const WithActions: StoryFn = ({ disabled }) => { +export const WithActions: StoryFn = (args) => { return ( - + 1-ops-tools1-fallback Prometheus @@ -118,9 +118,9 @@ export const WithActions: StoryFn = ({ disabled }) => { ); }; -export const Full: StoryFn = ({ disabled }) => { +export const Full: StoryFn = (args) => { return ( - + Card title Description, body text. Greetings! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod @@ -179,4 +179,18 @@ export const NotSelected: StoryFn = () => { ); }; +export const WithOverline: StoryFn = (args) => { + return ( + + Overline text above the title + Card title + + Description, body text. Greetings! Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco + laboris nisi ut aliquip ex ea commodo consequat. + + + ); +}; + export default meta; diff --git a/packages/grafana-ui/src/components/Card/Card.tsx b/packages/grafana-ui/src/components/Card/Card.tsx index 4ebb077f1fa..be178a25c29 100644 --- a/packages/grafana-ui/src/components/Card/Card.tsx +++ b/packages/grafana-ui/src/components/Card/Card.tsx @@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '../../themes'; import { getFocusStyles } from '../../themes/mixins'; +import { Text } from '../Text/Text'; import { CardContainer, CardContainerProps, getCardContainerStyles } from './CardContainer'; @@ -23,9 +24,12 @@ export interface Props extends Omit { + Overline: typeof Overline; Heading: typeof Heading; Tags: typeof Tags; Figure: typeof Figure; @@ -47,7 +51,16 @@ const CardContext = React.createContext<{ * * @public */ -export const Card: CardInterface = ({ disabled, href, onClick, children, isSelected, className, ...htmlProps }) => { +export const Card: CardInterface = ({ + disabled, + href, + onClick, + children, + isSelected, + isCompact, + className, + ...htmlProps +}) => { const hasHeadingComponent = useMemo( () => React.Children.toArray(children).some((c) => React.isValidElement(c) && c.type === Heading), [children] @@ -55,7 +68,7 @@ export const Card: CardInterface = ({ disabled, href, onClick, children, isSelec const disableHover = disabled || (!onClick && !href); const onCardClick = onClick && !disabled ? onClick : undefined; - const styles = useStyles2(getCardContainerStyles, disabled, disableHover, isSelected); + const styles = useStyles2(getCardContainerStyles, disabled, disableHover, isSelected, isCompact); return ( ({ }), }); +/** Card text to be displayed above title */ +const Overline = ({ children, className }: ChildProps) => { + const styles = useStyles2(getOverlineStyles); + return ( +
+ {children && ( + + {children} + + )} +
+ ); +}; +Overline.displayName = 'Overline'; + +const getOverlineStyles = (theme: GrafanaTheme2) => ({ + overline: css({ + gridArea: 'Overline', + marginBottom: theme.spacing(0.5), + }), +}); + const Tags = ({ children, className }: ChildProps) => { const styles = useStyles2(getTagStyles); return
{children}
; @@ -349,6 +384,7 @@ export const getCardStyles = (theme: GrafanaTheme2) => { }; }; +Card.Overline = Overline; Card.Heading = Heading; Card.Tags = Tags; Card.Figure = Figure; diff --git a/packages/grafana-ui/src/components/Card/CardContainer.tsx b/packages/grafana-ui/src/components/Card/CardContainer.tsx index e4a52c9f1f2..430634f9ba7 100644 --- a/packages/grafana-ui/src/components/Card/CardContainer.tsx +++ b/packages/grafana-ui/src/components/Card/CardContainer.tsx @@ -70,7 +70,8 @@ export const getCardContainerStyles = ( theme: GrafanaTheme2, disabled = false, disableHover = false, - isSelected?: boolean + isSelected?: boolean, + isCompact?: boolean ) => { const isSelectable = isSelected !== undefined; @@ -79,16 +80,17 @@ export const getCardContainerStyles = ( display: 'grid', position: 'relative', gridTemplateColumns: 'auto 1fr auto', - gridTemplateRows: '1fr auto auto auto', + gridTemplateRows: 'auto 1fr auto auto auto', gridAutoColumns: '1fr', gridAutoFlow: 'row', gridTemplateAreas: ` + "Figure Overline Tags" "Figure Heading Tags" "Figure Meta Tags" "Figure Description Tags" "Figure Actions Secondary"`, width: '100%', - padding: theme.spacing(2), + padding: theme.spacing(isCompact ? 1 : 2), background: theme.colors.background.secondary, borderRadius: theme.shape.radius.default, marginBottom: '8px', From 02d61857abc39b623b154a139d26312ff8c8d0f0 Mon Sep 17 00:00:00 2001 From: Leon Sorokin Date: Fri, 9 Feb 2024 06:55:09 -0600 Subject: [PATCH 12/50] Annotations: Fix axis markers rendering in wrong (stale) positions (#82219) --- .../panel/timeseries/plugins/AnnotationsPlugin2.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx index 32b6da49ca8..c1e616aeb22 100644 --- a/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx +++ b/public/app/plugins/panel/timeseries/plugins/AnnotationsPlugin2.tsx @@ -1,5 +1,5 @@ import { css } from '@emotion/css'; -import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useReducer } from 'react'; import { createPortal } from 'react-dom'; import tinycolor from 'tinycolor2'; import uPlot from 'uplot'; @@ -68,6 +68,8 @@ export const AnnotationsPlugin2 = ({ const styles = useStyles2(getStyles); const getColorByName = useTheme2().visualization.getColorByName; + const [_, forceUpdate] = useReducer((x) => x + 1, 0); + const annos = useMemo(() => { let annos = annotations.filter( (frame) => frame.name !== 'exemplar' && frame.length > 0 && frame.fields.some((f) => f.name === 'time') @@ -165,6 +167,13 @@ export const AnnotationsPlugin2 = ({ useEffect(() => { if (plot) { plot.redraw(); + + // this forces a second redraw after uPlot is updated (in the Plot.tsx didUpdate) with new data/scales + // and ensures the anno marker positions in the dom are re-rendered in correct places + // (this is temp fix until uPlot integrtion is refactored) + setTimeout(() => { + forceUpdate(); + }, 0); } }, [annos, plot]); From fc5f2286755d497cd458fb44e2aab6661fbe0588 Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Fri, 9 Feb 2024 13:30:50 +0000 Subject: [PATCH 13/50] Revert "Update dependency lerna to v8 (#82196)" (#82254) This reverts commit 00fd023fda6afa7ceb85e08a76b73845b8971f65. --- package.json | 2 +- yarn.lock | 868 +++++++++++++++++++++------------------------------ 2 files changed, 355 insertions(+), 515 deletions(-) diff --git a/package.json b/package.json index 5b61c7f3aed..2954485b2ff 100644 --- a/package.json +++ b/package.json @@ -181,7 +181,7 @@ "jest-fail-on-console": "3.1.2", "jest-junit": "16.0.0", "jest-matcher-utils": "29.7.0", - "lerna": "8.1.2", + "lerna": "7.4.1", "mini-css-extract-plugin": "2.8.0", "msw": "1.3.2", "mutationobserver-shim": "0.3.7", diff --git a/yarn.lock b/yarn.lock index e2b0cba33eb..e56cafb4d1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3172,7 +3172,7 @@ __metadata: languageName: node linkType: hard -"@gar/promisify@npm:^1.0.1": +"@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" checksum: 10/052dd232140fa60e81588000cbe729a40146579b361f1070bce63e2a761388a22a16d00beeffc504bd3601cb8e055c57b21a185448b3ed550cf50716f4fd442e @@ -4743,12 +4743,24 @@ __metadata: languageName: node linkType: hard -"@lerna/create@npm:8.1.2": - version: 8.1.2 - resolution: "@lerna/create@npm:8.1.2" +"@lerna/child-process@npm:7.4.1": + version: 7.4.1 + resolution: "@lerna/child-process@npm:7.4.1" dependencies: - "@npmcli/run-script": "npm:7.0.2" - "@nx/devkit": "npm:>=17.1.2 < 19" + chalk: "npm:^4.1.0" + execa: "npm:^5.0.0" + strong-log-transformer: "npm:^2.1.0" + checksum: 10/6be434a3d8aaf41e290dd0169133417cdb3b33ffd59fe77c7a927f28302fb8712a0be63fd261cf1b9c601000ed4dba1f86f8c0a8c3fa97fc665cd4e3458fc1ba + languageName: node + linkType: hard + +"@lerna/create@npm:7.4.1": + version: 7.4.1 + resolution: "@lerna/create@npm:7.4.1" + dependencies: + "@lerna/child-process": "npm:7.4.1" + "@npmcli/run-script": "npm:6.0.2" + "@nx/devkit": "npm:>=16.5.1 < 17" "@octokit/plugin-enterprise-rest": "npm:6.0.1" "@octokit/rest": "npm:19.0.11" byte-size: "npm:8.1.1" @@ -4785,12 +4797,12 @@ __metadata: npm-packlist: "npm:5.1.1" npm-registry-fetch: "npm:^14.0.5" npmlog: "npm:^6.0.2" - nx: "npm:>=17.1.2 < 19" + nx: "npm:>=16.5.1 < 17" p-map: "npm:4.0.0" p-map-series: "npm:2.1.0" p-queue: "npm:6.6.2" p-reduce: "npm:^2.1.0" - pacote: "npm:^17.0.5" + pacote: "npm:^15.2.0" pify: "npm:5.0.0" read-cmd-shim: "npm:4.0.0" read-package-json: "npm:6.0.4" @@ -4809,9 +4821,9 @@ __metadata: validate-npm-package-name: "npm:5.0.0" write-file-atomic: "npm:5.0.1" write-pkg: "npm:4.0.0" - yargs: "npm:17.7.2" - yargs-parser: "npm:21.1.1" - checksum: 10/d12b378cec4396d01f05127f9921dba83aae5f9c682d9cc01a15092bcd806625ad97126cd7b0dcc3cbfa851a9886c398d7562106ce9972603e00d02ddf6a6c61 + yargs: "npm:16.2.0" + yargs-parser: "npm:20.2.4" + checksum: 10/b475e41761a77c519d84711d80799db97c2844d1d100c91e0a7c9d8c500c3f34654d4915d7bbb08cd1c95d703a8b2b8cc92257ee14aaa64474ce8662977a4bcd languageName: node linkType: hard @@ -5183,19 +5195,6 @@ __metadata: languageName: node linkType: hard -"@npmcli/agent@npm:^2.0.0": - version: 2.2.1 - resolution: "@npmcli/agent@npm:2.2.1" - dependencies: - agent-base: "npm:^7.1.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.1" - lru-cache: "npm:^10.0.1" - socks-proxy-agent: "npm:^8.0.1" - checksum: 10/d4a48128f61e47f2f5c89315a5350e265dc619987e635bd62b52b29c7ed93536e724e721418c0ce352ceece86c13043c67aba1b70c3f5cc72fce6bb746706162 - languageName: node - linkType: hard - "@npmcli/fs@npm:^1.0.0": version: 1.0.0 resolution: "@npmcli/fs@npm:1.0.0" @@ -5206,6 +5205,16 @@ __metadata: languageName: node linkType: hard +"@npmcli/fs@npm:^2.1.0": + version: 2.1.0 + resolution: "@npmcli/fs@npm:2.1.0" + dependencies: + "@gar/promisify": "npm:^1.1.3" + semver: "npm:^7.3.5" + checksum: 10/1fe97efb5c1250c5986b46b6c8256b1eab8159a6d50fc8ace9f90937b3195541272faf77f18bdbf5eeb89bab68332c7846ac5ab9337e6099e63c6007388ebe84 + languageName: node + linkType: hard + "@npmcli/fs@npm:^3.1.0": version: 3.1.0 resolution: "@npmcli/fs@npm:3.1.0" @@ -5215,19 +5224,19 @@ __metadata: languageName: node linkType: hard -"@npmcli/git@npm:^5.0.0": - version: 5.0.4 - resolution: "@npmcli/git@npm:5.0.4" +"@npmcli/git@npm:^4.0.0": + version: 4.1.0 + resolution: "@npmcli/git@npm:4.1.0" dependencies: - "@npmcli/promise-spawn": "npm:^7.0.0" - lru-cache: "npm:^10.0.1" - npm-pick-manifest: "npm:^9.0.0" + "@npmcli/promise-spawn": "npm:^6.0.0" + lru-cache: "npm:^7.4.4" + npm-pick-manifest: "npm:^8.0.0" proc-log: "npm:^3.0.0" promise-inflight: "npm:^1.0.1" promise-retry: "npm:^2.0.1" semver: "npm:^7.3.5" - which: "npm:^4.0.0" - checksum: 10/136e71f4de73ef315285ebaf172b4681d1d22aff4c87ec526af1e57ab88ad7c864272523382009a2e3fab00f932bea204ed90cbdf187c7b7bd3d5c6e3d6c6d1a + which: "npm:^3.0.0" + checksum: 10/33512ce12758d67c0322eca25019c4d5ef03e83f5829e09a05389af485bab216cc4df408b8eba98f2d12c119c6dff84f0d8ff25a1ac5d8a46184e55ae8f53754 languageName: node linkType: hard @@ -5253,6 +5262,16 @@ __metadata: languageName: node linkType: hard +"@npmcli/move-file@npm:^2.0.0": + version: 2.0.0 + resolution: "@npmcli/move-file@npm:2.0.0" + dependencies: + mkdirp: "npm:^1.0.4" + rimraf: "npm:^3.0.2" + checksum: 10/1388777b507b0c592d53f41b9d182e1a8de7763bc625fc07999b8edbc22325f074e5b3ec90af79c89d6987fdb2325bc66d59f483258543c14a43661621f841b0 + languageName: node + linkType: hard + "@npmcli/node-gyp@npm:^3.0.0": version: 3.0.0 resolution: "@npmcli/node-gyp@npm:3.0.0" @@ -5260,161 +5279,132 @@ __metadata: languageName: node linkType: hard -"@npmcli/package-json@npm:^5.0.0": - version: 5.0.0 - resolution: "@npmcli/package-json@npm:5.0.0" +"@npmcli/promise-spawn@npm:^6.0.0, @npmcli/promise-spawn@npm:^6.0.1": + version: 6.0.2 + resolution: "@npmcli/promise-spawn@npm:6.0.2" dependencies: - "@npmcli/git": "npm:^5.0.0" - glob: "npm:^10.2.2" - hosted-git-info: "npm:^7.0.0" - json-parse-even-better-errors: "npm:^3.0.0" - normalize-package-data: "npm:^6.0.0" - proc-log: "npm:^3.0.0" - semver: "npm:^7.5.3" - checksum: 10/bb907e934e96dae3d3aa26aa45cbaa87b318cb64c4aaaacfa3596b1ca5147ad1b51c3281eb529df12116a163d33ca99f48c4593b0c168e38412dfbf2c5cced72 + which: "npm:^3.0.0" + checksum: 10/cc94a83ff1626ad93d42c2ea583dba1fb2d24cdab49caf0af77a3a0ff9bdbba34e09048b6821d4060ea7a58d4a41d49bece4ae3716929e2077c2fff0f5e94d94 languageName: node linkType: hard -"@npmcli/promise-spawn@npm:^7.0.0": - version: 7.0.1 - resolution: "@npmcli/promise-spawn@npm:7.0.1" - dependencies: - which: "npm:^4.0.0" - checksum: 10/7cbfc3c5e0bcad28e362dc34418b7507afea4fa82d692b802d9b8999ebdc99ceb2686f5959b5b9890e424983cee801401d3e972638f6942f75a2976a2c61774c - languageName: node - linkType: hard - -"@npmcli/run-script@npm:7.0.2": - version: 7.0.2 - resolution: "@npmcli/run-script@npm:7.0.2" +"@npmcli/run-script@npm:6.0.2, @npmcli/run-script@npm:^6.0.0": + version: 6.0.2 + resolution: "@npmcli/run-script@npm:6.0.2" dependencies: "@npmcli/node-gyp": "npm:^3.0.0" - "@npmcli/promise-spawn": "npm:^7.0.0" - node-gyp: "npm:^10.0.0" + "@npmcli/promise-spawn": "npm:^6.0.0" + node-gyp: "npm:^9.0.0" read-package-json-fast: "npm:^3.0.0" - which: "npm:^4.0.0" - checksum: 10/4549311f3b937ca81d147b72fbfd41aa6ed7daf70ecc4e9ee3838f9cce1749e9c62c301943a8a67364a96c31bbc67c49ee31526fb12ec2f4b15148f0ef472f98 + which: "npm:^3.0.0" + checksum: 10/9b22c4c53d4b2e014e7f990cf2e1d32d1830c5629d37a4ee56011bcdfb51424ca8dc3fb3fa550b4abe7e8f0efdd68468d733b754db371b06a5dd300663cf13a2 languageName: node linkType: hard -"@npmcli/run-script@npm:^7.0.0": - version: 7.0.4 - resolution: "@npmcli/run-script@npm:7.0.4" +"@nrwl/devkit@npm:16.10.0": + version: 16.10.0 + resolution: "@nrwl/devkit@npm:16.10.0" dependencies: - "@npmcli/node-gyp": "npm:^3.0.0" - "@npmcli/package-json": "npm:^5.0.0" - "@npmcli/promise-spawn": "npm:^7.0.0" - node-gyp: "npm:^10.0.0" - which: "npm:^4.0.0" - checksum: 10/f09268051f74af7d7be46e9911a23126d531160c338d3c05d53e6cd7994b88271fb4ec524139fe7f2d826525f15a281eafef3be02831adc1f68556a8a668621a + "@nx/devkit": "npm:16.10.0" + checksum: 10/2727b9927f8a7f3561c5eae72d3ca91d3ff0ea7c47da1d096d1654c85763acd580bbabb182db9e6f4f5df93152ae0972a0df7393f2dbad9d17b68549af313769 languageName: node linkType: hard -"@nrwl/devkit@npm:18.0.3": - version: 18.0.3 - resolution: "@nrwl/devkit@npm:18.0.3" +"@nrwl/tao@npm:16.10.0": + version: 16.10.0 + resolution: "@nrwl/tao@npm:16.10.0" dependencies: - "@nx/devkit": "npm:18.0.3" - checksum: 10/bb97aae663d573431a7fa8810e1b4e57913710e7611f4546f6bb6452b3c5c352b20f3b1fd61f8f4500e36cbcf621b461eeda93d85156145ee62dbca575c41b25 - languageName: node - linkType: hard - -"@nrwl/tao@npm:18.0.3": - version: 18.0.3 - resolution: "@nrwl/tao@npm:18.0.3" - dependencies: - nx: "npm:18.0.3" + nx: "npm:16.10.0" tslib: "npm:^2.3.0" bin: tao: index.js - checksum: 10/a9981bcceec72ba535e0c6be9ba2f333c6250a757774e73cd5711a507e7a58f0e66d04476fe0027c2f94d995178cc91a8123c01c80f24231fc8942f828d77add + checksum: 10/df495b60f98112ffbeb19ae3d9385ecc18b3b9a2dbde1c50a91d5111408afba218c32ba55e6a3adf7c935262f4c18417672712e14185de2ec61beb5ef23186dc languageName: node linkType: hard -"@nx/devkit@npm:18.0.3, @nx/devkit@npm:>=17.1.2 < 19": - version: 18.0.3 - resolution: "@nx/devkit@npm:18.0.3" +"@nx/devkit@npm:16.10.0, @nx/devkit@npm:>=16.5.1 < 17": + version: 16.10.0 + resolution: "@nx/devkit@npm:16.10.0" dependencies: - "@nrwl/devkit": "npm:18.0.3" + "@nrwl/devkit": "npm:16.10.0" ejs: "npm:^3.1.7" enquirer: "npm:~2.3.6" ignore: "npm:^5.0.4" - semver: "npm:^7.5.3" + semver: "npm:7.5.3" tmp: "npm:~0.2.1" tslib: "npm:^2.3.0" - yargs-parser: "npm:21.1.1" peerDependencies: - nx: ">= 16 <= 18" - checksum: 10/c13d5a50e975bb4feba47e7939227efa122f46b3d295c65732c6aa2aaad43002596c057001fdafe057d9167a40546c139f84786a0a35243b11279d9684fe9793 + nx: ">= 15 <= 17" + checksum: 10/d703e74d8360395dcafdc531e81c25ac6bbe46142169a5185f841067336b7dadce7f2102cfc2ef1c1826e3f9b92ca5b740a62ca4c32064265494dcd5a359ee21 languageName: node linkType: hard -"@nx/nx-darwin-arm64@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-darwin-arm64@npm:18.0.3" +"@nx/nx-darwin-arm64@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-darwin-arm64@npm:16.10.0" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@nx/nx-darwin-x64@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-darwin-x64@npm:18.0.3" +"@nx/nx-darwin-x64@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-darwin-x64@npm:16.10.0" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@nx/nx-freebsd-x64@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-freebsd-x64@npm:18.0.3" +"@nx/nx-freebsd-x64@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-freebsd-x64@npm:16.10.0" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@nx/nx-linux-arm-gnueabihf@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-linux-arm-gnueabihf@npm:18.0.3" +"@nx/nx-linux-arm-gnueabihf@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-arm-gnueabihf@npm:16.10.0" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@nx/nx-linux-arm64-gnu@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-linux-arm64-gnu@npm:18.0.3" +"@nx/nx-linux-arm64-gnu@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-arm64-gnu@npm:16.10.0" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@nx/nx-linux-arm64-musl@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-linux-arm64-musl@npm:18.0.3" +"@nx/nx-linux-arm64-musl@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-arm64-musl@npm:16.10.0" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@nx/nx-linux-x64-gnu@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-linux-x64-gnu@npm:18.0.3" +"@nx/nx-linux-x64-gnu@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-x64-gnu@npm:16.10.0" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@nx/nx-linux-x64-musl@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-linux-x64-musl@npm:18.0.3" +"@nx/nx-linux-x64-musl@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-linux-x64-musl@npm:16.10.0" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@nx/nx-win32-arm64-msvc@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-win32-arm64-msvc@npm:18.0.3" +"@nx/nx-win32-arm64-msvc@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-win32-arm64-msvc@npm:16.10.0" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@nx/nx-win32-x64-msvc@npm:18.0.3": - version: 18.0.3 - resolution: "@nx/nx-win32-x64-msvc@npm:18.0.3" +"@nx/nx-win32-x64-msvc@npm:16.10.0": + version: 16.10.0 + resolution: "@nx/nx-win32-x64-msvc@npm:16.10.0" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -5787,6 +5777,17 @@ __metadata: languageName: node linkType: hard +"@parcel/watcher@npm:2.0.4": + version: 2.0.4 + resolution: "@parcel/watcher@npm:2.0.4" + dependencies: + node-addon-api: "npm:^3.2.1" + node-gyp: "npm:latest" + node-gyp-build: "npm:^4.3.0" + checksum: 10/ec3ba32c16856c34460d79bc95887f68869201e0cae68c5d1d4cd1f0358673d76dea56e194ede1e83af78656bde4eef2b17716a7396b54f63a40e4655c7a63c4 + languageName: node + linkType: hard + "@petamoriken/float16@npm:^3.4.7": version: 3.5.0 resolution: "@petamoriken/float16@npm:3.5.0" @@ -6995,23 +6996,7 @@ __metadata: languageName: node linkType: hard -"@sigstore/bundle@npm:^2.1.1": - version: 2.1.1 - resolution: "@sigstore/bundle@npm:2.1.1" - dependencies: - "@sigstore/protobuf-specs": "npm:^0.2.1" - checksum: 10/e29916ad3f37d4e1c5b98d7a614cddb1301d4bdfa5ebe0cb2733f4cbc78710b8320aa62ad033e4702c5ec7bcd9c371278b7934ce45f3df71bb3ffa07f5502742 - languageName: node - linkType: hard - -"@sigstore/core@npm:^0.2.0": - version: 0.2.0 - resolution: "@sigstore/core@npm:0.2.0" - checksum: 10/6a9e7f0dcbaad3e330207f6ce0aa0cb229416eb8ece71a31e427f71f021ce25ef8230faaca93c8abf428dab391f63ef7a08c8a88e0237dee3b15daf35c53a86a - languageName: node - linkType: hard - -"@sigstore/protobuf-specs@npm:^0.2.0, @sigstore/protobuf-specs@npm:^0.2.1": +"@sigstore/protobuf-specs@npm:^0.2.0": version: 0.2.1 resolution: "@sigstore/protobuf-specs@npm:0.2.1" checksum: 10/cb0b9d9b3ef44a9f1729d85616c5d7c2ebccde303836a5a345ec33a500c7bd5205ffcc31332e0a90831cccc581dafbdf5b868f050c84270c8df6a4a6f2ce0bcb @@ -7029,18 +7014,6 @@ __metadata: languageName: node linkType: hard -"@sigstore/sign@npm:^2.2.1": - version: 2.2.1 - resolution: "@sigstore/sign@npm:2.2.1" - dependencies: - "@sigstore/bundle": "npm:^2.1.1" - "@sigstore/core": "npm:^0.2.0" - "@sigstore/protobuf-specs": "npm:^0.2.1" - make-fetch-happen: "npm:^13.0.0" - checksum: 10/a829c479418a86f9919d85aec0349fd4a9c297aaacc4e838580bc9b5ba9a372fb318b4829b78cc5c9e56b8fd1b7d11a06e31384eff55bd0813f5d0993f5fb9db - languageName: node - linkType: hard - "@sigstore/tuf@npm:^1.0.3": version: 1.0.3 resolution: "@sigstore/tuf@npm:1.0.3" @@ -7051,27 +7024,6 @@ __metadata: languageName: node linkType: hard -"@sigstore/tuf@npm:^2.3.0": - version: 2.3.0 - resolution: "@sigstore/tuf@npm:2.3.0" - dependencies: - "@sigstore/protobuf-specs": "npm:^0.2.1" - tuf-js: "npm:^2.2.0" - checksum: 10/c4a9e87c1d4b48de87526fd37b154382dd7caf6fe784329b829270ed431741bb1a4ecde6d8aa2bbe72124a24ef1b616c098a4b036cd04965e02f039de11acd4f - languageName: node - linkType: hard - -"@sigstore/verify@npm:^0.1.0": - version: 0.1.0 - resolution: "@sigstore/verify@npm:0.1.0" - dependencies: - "@sigstore/bundle": "npm:^2.1.1" - "@sigstore/core": "npm:^0.2.0" - "@sigstore/protobuf-specs": "npm:^0.2.1" - checksum: 10/9dc208a4d0ace4d836aa1717cd02236b480d883e2a7a4f40fb87ccb0e7b7e6d4805c5628bb5cc3aec392bafe866e59f3ce55c2b16ef9ed224ae6a60c07984e65 - languageName: node - linkType: hard - "@sinclair/typebox@npm:^0.27.8": version: 0.27.8 resolution: "@sinclair/typebox@npm:0.27.8" @@ -8478,13 +8430,6 @@ __metadata: languageName: node linkType: hard -"@tufjs/canonical-json@npm:2.0.0": - version: 2.0.0 - resolution: "@tufjs/canonical-json@npm:2.0.0" - checksum: 10/cc719a1d0d0ae1aa1ba551a82c87dcbefac088e433c03a3d8a1d547ea721350e47dab4ab5b0fca40d5c7ab1f4882e72edc39c9eae15bf47c45c43bcb6ee39f4f - languageName: node - linkType: hard - "@tufjs/models@npm:1.0.4": version: 1.0.4 resolution: "@tufjs/models@npm:1.0.4" @@ -8495,16 +8440,6 @@ __metadata: languageName: node linkType: hard -"@tufjs/models@npm:2.0.0": - version: 2.0.0 - resolution: "@tufjs/models@npm:2.0.0" - dependencies: - "@tufjs/canonical-json": "npm:2.0.0" - minimatch: "npm:^9.0.3" - checksum: 10/d89d618c74c4eed3906d9ba5bd1bd9d0fa7a73ad6266b11c74c13102ee00bfdbd8e73fe786bd2e8e3ae347f9a66f044d973a7466dc7c2c2f98a7ff926ff275c4 - languageName: node - linkType: hard - "@types/angular-route@npm:1.7.6": version: 1.7.6 resolution: "@types/angular-route@npm:1.7.6" @@ -10940,13 +10875,6 @@ __metadata: languageName: node linkType: hard -"abbrev@npm:^2.0.0": - version: 2.0.0 - resolution: "abbrev@npm:2.0.0" - checksum: 10/ca0a54e35bea4ece0ecb68a47b312e1a9a6f772408d5bcb9051230aaa94b0460671c5b5c9cb3240eb5b7bc94c52476550eb221f65a0bbd0145bdc9f3113a6707 - languageName: node - linkType: hard - "accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" @@ -11063,15 +10991,6 @@ __metadata: languageName: node linkType: hard -"agent-base@npm:^7.0.2, agent-base@npm:^7.1.0": - version: 7.1.0 - resolution: "agent-base@npm:7.1.0" - dependencies: - debug: "npm:^4.3.4" - checksum: 10/f7828f991470a0cc22cb579c86a18cbae83d8a3cbed39992ab34fc7217c4d126017f1c74d0ab66be87f71455318a8ea3e757d6a37881b8d0f2a2c6aa55e5418f - languageName: node - linkType: hard - "agentkeepalive@npm:^4.1.3, agentkeepalive@npm:^4.2.1": version: 4.2.1 resolution: "agentkeepalive@npm:4.2.1" @@ -11717,14 +11636,14 @@ __metadata: languageName: node linkType: hard -"axios@npm:^1.6.0": - version: 1.6.7 - resolution: "axios@npm:1.6.7" +"axios@npm:^1.0.0": + version: 1.5.1 + resolution: "axios@npm:1.5.1" dependencies: - follow-redirects: "npm:^1.15.4" + follow-redirects: "npm:^1.15.0" form-data: "npm:^4.0.0" proxy-from-env: "npm:^1.1.0" - checksum: 10/a1932b089ece759cd261f175d9ebf4d41c8994cf0c0767cda86055c7a19bcfdade8ae3464bf4cec4c8b142f4a657dc664fb77a41855e8376cf38b86d7a86518f + checksum: 10/67633db5867c789a6edb6e5229884501bef89584a6718220c243fd5a64de4ea7dcdfdf4f8368a672d582db78aaa9f8d7b619d39403b669f451e1242bbd4c7ee2 languageName: node linkType: hard @@ -12448,6 +12367,32 @@ __metadata: languageName: node linkType: hard +"cacache@npm:^16.1.0": + version: 16.1.1 + resolution: "cacache@npm:16.1.1" + dependencies: + "@npmcli/fs": "npm:^2.1.0" + "@npmcli/move-file": "npm:^2.0.0" + chownr: "npm:^2.0.0" + fs-minipass: "npm:^2.1.0" + glob: "npm:^8.0.1" + infer-owner: "npm:^1.0.4" + lru-cache: "npm:^7.7.1" + minipass: "npm:^3.1.6" + minipass-collect: "npm:^1.0.2" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + mkdirp: "npm:^1.0.4" + p-map: "npm:^4.0.0" + promise-inflight: "npm:^1.0.1" + rimraf: "npm:^3.0.2" + ssri: "npm:^9.0.0" + tar: "npm:^6.1.11" + unique-filename: "npm:^1.1.1" + checksum: 10/8356f969767ff11ed5e9dc6fcb3fc47d227431c6e68086a34ae08b2f3744909e6e22ae1868dc5ab094132a3d8dfc174f08bd7f3122abf50cf56fd789553d3d1f + languageName: node + linkType: hard + "cacache@npm:^17.0.0": version: 17.1.4 resolution: "cacache@npm:17.1.4" @@ -12468,26 +12413,6 @@ __metadata: languageName: node linkType: hard -"cacache@npm:^18.0.0": - version: 18.0.2 - resolution: "cacache@npm:18.0.2" - dependencies: - "@npmcli/fs": "npm:^3.1.0" - fs-minipass: "npm:^3.0.0" - glob: "npm:^10.2.2" - lru-cache: "npm:^10.0.1" - minipass: "npm:^7.0.3" - minipass-collect: "npm:^2.0.1" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - p-map: "npm:^4.0.0" - ssri: "npm:^10.0.0" - tar: "npm:^6.1.11" - unique-filename: "npm:^3.0.0" - checksum: 10/5ca58464f785d4d64ac2019fcad95451c8c89bea25949f63acd8987fcc3493eaef1beccc0fa39e673506d879d3fc1ab420760f8a14f8ddf46ea2d121805a5e96 - languageName: node - linkType: hard - "cachedir@npm:^2.3.0": version: 2.3.0 resolution: "cachedir@npm:2.3.0" @@ -13354,12 +13279,12 @@ __metadata: languageName: node linkType: hard -"conventional-changelog-angular@npm:7.0.0": - version: 7.0.0 - resolution: "conventional-changelog-angular@npm:7.0.0" +"conventional-changelog-angular@npm:6.0.0": + version: 6.0.0 + resolution: "conventional-changelog-angular@npm:6.0.0" dependencies: compare-func: "npm:^2.0.0" - checksum: 10/e7966d2fee5475e76263f30f8b714b2b592b5bf556df225b7091e5090831fc9a20b99598a7d2997e19c2ef8118c0a3150b1eba290786367b0f55a5ccfa804ec9 + checksum: 10/ddc59ead53a45b817d83208200967f5340866782b8362d5e2e34105fdfa3d3a31585ebbdec7750bdb9de53da869f847e8ca96634a9801f51e27ecf4e7ffe2bad languageName: node linkType: hard @@ -16622,13 +16547,6 @@ __metadata: languageName: node linkType: hard -"exponential-backoff@npm:^3.1.1": - version: 3.1.1 - resolution: "exponential-backoff@npm:3.1.1" - checksum: 10/2d9bbb6473de7051f96790d5f9a678f32e60ed0aa70741dc7fdc96fec8d631124ec3374ac144387604f05afff9500f31a1d45bd9eee4cdc2e4f9ad2d9b9d5dbd - languageName: node - linkType: hard - "expose-loader@npm:5.0.0": version: 5.0.0 resolution: "expose-loader@npm:5.0.0" @@ -17123,13 +17041,13 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.4": - version: 1.15.5 - resolution: "follow-redirects@npm:1.15.5" +"follow-redirects@npm:^1.0.0, follow-redirects@npm:^1.15.0": + version: 1.15.3 + resolution: "follow-redirects@npm:1.15.3" peerDependenciesMeta: debug: optional: true - checksum: 10/d467f13c1c6aa734599b8b369cd7a625b20081af358f6204ff515f6f4116eb440de9c4e0c49f10798eeb0df26c95dd05d5e0d9ddc5786ab1a8a8abefe92929b4 + checksum: 10/60d98693f4976892f8c654b16ef6d1803887a951898857ab0cdc009570b1c06314ad499505b7a040ac5b98144939f8597766e5e6a6859c0945d157b473aa6f5f languageName: node linkType: hard @@ -17348,7 +17266,7 @@ __metadata: languageName: node linkType: hard -"fs-minipass@npm:^2.0.0": +"fs-minipass@npm:^2.0.0, fs-minipass@npm:^2.1.0": version: 2.1.0 resolution: "fs-minipass@npm:2.1.0" dependencies: @@ -17820,7 +17738,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:10.3.10, glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.2.5, glob@npm:^10.2.7, glob@npm:^10.3.10, glob@npm:^10.3.7": +"glob@npm:10.3.10, glob@npm:^10.0.0, glob@npm:^10.2.2, glob@npm:^10.2.5, glob@npm:^10.2.7, glob@npm:^10.3.7": version: 10.3.10 resolution: "glob@npm:10.3.10" dependencies: @@ -17835,6 +17753,20 @@ __metadata: languageName: node linkType: hard +"glob@npm:7.1.4": + version: 7.1.4 + resolution: "glob@npm:7.1.4" + dependencies: + fs.realpath: "npm:^1.0.0" + inflight: "npm:^1.0.4" + inherits: "npm:2" + minimatch: "npm:^3.0.4" + once: "npm:^1.3.0" + path-is-absolute: "npm:^1.0.0" + checksum: 10/776bcc31371797eb5cf6b58c4618378f8df83d23f00aef8e98af5e7f0e59f5ee8b470c4e95e71cfa7a8682634849e21ea1f1ad38639c1828a2dbc2757bf7a63b + languageName: node + linkType: hard + "glob@npm:7.2.0": version: 7.2.0 resolution: "glob@npm:7.2.0" @@ -18244,7 +18176,7 @@ __metadata: json-source-map: "npm:0.6.1" jsurl: "npm:^0.1.5" kbar: "npm:0.1.0-beta.45" - lerna: "npm:8.1.2" + lerna: "npm:7.4.1" leven: "npm:^4.0.0" lodash: "npm:4.17.21" logfmt: "npm:^1.3.2" @@ -18710,15 +18642,6 @@ __metadata: languageName: node linkType: hard -"hosted-git-info@npm:^7.0.0": - version: 7.0.1 - resolution: "hosted-git-info@npm:7.0.1" - dependencies: - lru-cache: "npm:^10.0.1" - checksum: 10/5f740ecf3c70838e27446ff433a9a9a583de8747f7b661390b373ad12ca47edb937136e79999a4f953d0953079025a11df173f1fd9f7d52b0277b2fb9433e1c7 - languageName: node - linkType: hard - "hpack.js@npm:^2.1.6": version: 2.1.6 resolution: "hpack.js@npm:2.1.6" @@ -18946,16 +18869,6 @@ __metadata: languageName: node linkType: hard -"http-proxy-agent@npm:^7.0.0": - version: 7.0.0 - resolution: "http-proxy-agent@npm:7.0.0" - dependencies: - agent-base: "npm:^7.1.0" - debug: "npm:^4.3.4" - checksum: 10/dbaaf3d9f3fc4df4a5d7ec45d456ec50f575240b557160fa63427b447d1f812dd7fe4a4f17d2e1ba003d231f07edf5a856ea6d91cb32d533062ff20a7803ccac - languageName: node - linkType: hard - "http-proxy-middleware@npm:^2.0.3": version: 2.0.4 resolution: "http-proxy-middleware@npm:2.0.4" @@ -19050,16 +18963,6 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.1": - version: 7.0.2 - resolution: "https-proxy-agent@npm:7.0.2" - dependencies: - agent-base: "npm:^7.0.2" - debug: "npm:4" - checksum: 10/9ec844f78fd643608239c9c3f6819918631df5cd3e17d104cc507226a39b5d4adda9d790fc9fd63ac0d2bb8a761b2f9f60faa80584a9bf9d7f2e8c5ed0acd330 - languageName: node - linkType: hard - "human-signals@npm:^1.1.1": version: 1.1.1 resolution: "human-signals@npm:1.1.1" @@ -19178,12 +19081,12 @@ __metadata: languageName: node linkType: hard -"ignore-walk@npm:^6.0.4": - version: 6.0.4 - resolution: "ignore-walk@npm:6.0.4" +"ignore-walk@npm:^6.0.0": + version: 6.0.3 + resolution: "ignore-walk@npm:6.0.3" dependencies: minimatch: "npm:^9.0.0" - checksum: 10/a56c3f929bb0890ffb6e87dfaca7d5ce97f9e179fd68d49711edea55760aaee367cea3845d7620689b706249053c4b1805e21158f6751c7333f9b2ffb3668272 + checksum: 10/3cbc0b52c7dc405a3525898d705029f084ef6218df2a82b95520d72e3a6fb3ff893a4c22b73f36d2b7cefbe786a9687c4396de3c628be2844bc0728dc4e455cf languageName: node linkType: hard @@ -19418,6 +19321,13 @@ __metadata: languageName: node linkType: hard +"ip@npm:^1.1.5": + version: 1.1.5 + resolution: "ip@npm:1.1.5" + checksum: 10/40a00572cf06b53f4c7b7fe6270a8427ef4c6c0820a380f9f1eb48a323eb09c7dbd16245b472cf5a2d083911d0deae4d712b6e6c88b346fa274e8ce07756a7d6 + languageName: node + linkType: hard + "ip@npm:^2.0.0": version: 2.0.0 resolution: "ip@npm:2.0.0" @@ -20060,13 +19970,6 @@ __metadata: languageName: node linkType: hard -"isexe@npm:^3.1.1": - version: 3.1.1 - resolution: "isexe@npm:3.1.1" - checksum: 10/7fe1931ee4e88eb5aa524cd3ceb8c882537bc3a81b02e438b240e47012eef49c86904d0f0e593ea7c3a9996d18d0f1f3be8d3eaa92333977b0c3a9d353d5563e - languageName: node - linkType: hard - "isobject@npm:^3.0.1": version: 3.0.1 resolution: "isobject@npm:3.0.1" @@ -21305,13 +21208,14 @@ __metadata: languageName: node linkType: hard -"lerna@npm:8.1.2": - version: 8.1.2 - resolution: "lerna@npm:8.1.2" +"lerna@npm:7.4.1": + version: 7.4.1 + resolution: "lerna@npm:7.4.1" dependencies: - "@lerna/create": "npm:8.1.2" - "@npmcli/run-script": "npm:7.0.2" - "@nx/devkit": "npm:>=17.1.2 < 19" + "@lerna/child-process": "npm:7.4.1" + "@lerna/create": "npm:7.4.1" + "@npmcli/run-script": "npm:6.0.2" + "@nx/devkit": "npm:>=16.5.1 < 17" "@octokit/plugin-enterprise-rest": "npm:6.0.1" "@octokit/rest": "npm:19.0.11" byte-size: "npm:8.1.1" @@ -21319,7 +21223,7 @@ __metadata: clone-deep: "npm:4.0.1" cmd-shim: "npm:6.0.1" columnify: "npm:1.6.0" - conventional-changelog-angular: "npm:7.0.0" + conventional-changelog-angular: "npm:6.0.0" conventional-changelog-core: "npm:5.0.1" conventional-recommended-bump: "npm:7.0.1" cosmiconfig: "npm:^8.2.0" @@ -21354,14 +21258,14 @@ __metadata: npm-packlist: "npm:5.1.1" npm-registry-fetch: "npm:^14.0.5" npmlog: "npm:^6.0.2" - nx: "npm:>=17.1.2 < 19" + nx: "npm:>=16.5.1 < 17" p-map: "npm:4.0.0" p-map-series: "npm:2.1.0" p-pipe: "npm:3.1.0" p-queue: "npm:6.6.2" p-reduce: "npm:2.1.0" p-waterfall: "npm:2.1.1" - pacote: "npm:^17.0.5" + pacote: "npm:^15.2.0" pify: "npm:5.0.0" read-cmd-shim: "npm:4.0.0" read-package-json: "npm:6.0.4" @@ -21381,11 +21285,11 @@ __metadata: validate-npm-package-name: "npm:5.0.0" write-file-atomic: "npm:5.0.1" write-pkg: "npm:4.0.0" - yargs: "npm:17.7.2" - yargs-parser: "npm:21.1.1" + yargs: "npm:16.2.0" + yargs-parser: "npm:20.2.4" bin: lerna: dist/cli.js - checksum: 10/5f4267bb059e00b294985e5cc4e783155e5be58ecd73c1791be0c24ff77561f2699550cdad4faee8a3a5d2866ae18bd6d7c4bba4b0dae1b26056d1187616c8a6 + checksum: 10/3b837bd48adefc962cd0c5ce79e6e7716580ec88784cbb3b8ecc9cbbfea79de6536576f42432e87b677c7c1267bfe47a13ce0ec4ab8687ecb721be58cedd547a languageName: node linkType: hard @@ -21780,7 +21684,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:10.2.0, lru-cache@npm:^10.0.1, lru-cache@npm:^9.1.1 || ^10.0.0": +"lru-cache@npm:10.2.0, lru-cache@npm:^9.1.1 || ^10.0.0": version: 10.2.0 resolution: "lru-cache@npm:10.2.0" checksum: 10/502ec42c3309c0eae1ce41afca471f831c278566d45a5273a0c51102dee31e0e250a62fa9029c3370988df33a14188a38e682c16143b794de78668de3643e302 @@ -21805,7 +21709,7 @@ __metadata: languageName: node linkType: hard -"lru-cache@npm:^7.5.1, lru-cache@npm:^7.7.1": +"lru-cache@npm:^7.4.4, lru-cache@npm:^7.5.1, lru-cache@npm:^7.7.1": version: 7.12.0 resolution: "lru-cache@npm:7.12.0" checksum: 10/ac4e78bb5a04174389db377d325c9baecb5d5c5cf619894aa237891b3f5605e51426823b118753229b0bf4073db7091cc7593c096181e2ba20b4b5e2e59c9c9d @@ -21879,6 +21783,30 @@ __metadata: languageName: node linkType: hard +"make-fetch-happen@npm:^10.0.3": + version: 10.1.8 + resolution: "make-fetch-happen@npm:10.1.8" + dependencies: + agentkeepalive: "npm:^4.2.1" + cacache: "npm:^16.1.0" + http-cache-semantics: "npm:^4.1.0" + http-proxy-agent: "npm:^5.0.0" + https-proxy-agent: "npm:^5.0.0" + is-lambda: "npm:^1.0.1" + lru-cache: "npm:^7.7.1" + minipass: "npm:^3.1.6" + minipass-collect: "npm:^1.0.2" + minipass-fetch: "npm:^2.0.3" + minipass-flush: "npm:^1.0.5" + minipass-pipeline: "npm:^1.2.4" + negotiator: "npm:^0.6.3" + promise-retry: "npm:^2.0.1" + socks-proxy-agent: "npm:^7.0.0" + ssri: "npm:^9.0.0" + checksum: 10/0f83e6814d2aae5e37fc20dc4e4657419ad6e79771d78f7689ad037633836e37a1e65f799bce47a71f0202e5fdbc346aca78804c3b4ccfee621f00b1cd3176a3 + languageName: node + linkType: hard + "make-fetch-happen@npm:^11.0.0, make-fetch-happen@npm:^11.0.1, make-fetch-happen@npm:^11.1.1": version: 11.1.1 resolution: "make-fetch-happen@npm:11.1.1" @@ -21902,25 +21830,6 @@ __metadata: languageName: node linkType: hard -"make-fetch-happen@npm:^13.0.0": - version: 13.0.0 - resolution: "make-fetch-happen@npm:13.0.0" - dependencies: - "@npmcli/agent": "npm:^2.0.0" - cacache: "npm:^18.0.0" - http-cache-semantics: "npm:^4.1.1" - is-lambda: "npm:^1.0.1" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^3.0.0" - minipass-flush: "npm:^1.0.5" - minipass-pipeline: "npm:^1.2.4" - negotiator: "npm:^0.6.3" - promise-retry: "npm:^2.0.1" - ssri: "npm:^10.0.0" - checksum: 10/ded5a91a02b76381b06a4ec4d5c1d23ebbde15d402b3c3e4533b371dac7e2f7ca071ae71ae6dae72aa261182557b7b1b3fd3a705b39252dc17f74fa509d3e76f - languageName: node - linkType: hard - "make-fetch-happen@npm:^9.1.0": version: 9.1.0 resolution: "make-fetch-happen@npm:9.1.0" @@ -22355,15 +22264,6 @@ __metadata: languageName: node linkType: hard -"minipass-collect@npm:^2.0.1": - version: 2.0.1 - resolution: "minipass-collect@npm:2.0.1" - dependencies: - minipass: "npm:^7.0.3" - checksum: 10/b251bceea62090f67a6cced7a446a36f4cd61ee2d5cea9aee7fff79ba8030e416327a1c5aa2908dc22629d06214b46d88fdab8c51ac76bacbf5703851b5ad342 - languageName: node - linkType: hard - "minipass-fetch@npm:^1.3.2": version: 1.4.1 resolution: "minipass-fetch@npm:1.4.1" @@ -22379,6 +22279,21 @@ __metadata: languageName: node linkType: hard +"minipass-fetch@npm:^2.0.3": + version: 2.1.0 + resolution: "minipass-fetch@npm:2.1.0" + dependencies: + encoding: "npm:^0.1.13" + minipass: "npm:^3.1.6" + minipass-sized: "npm:^1.0.3" + minizlib: "npm:^2.1.2" + dependenciesMeta: + encoding: + optional: true + checksum: 10/33b6927ef8a4516e27878e1e9966a6dee5c2efb844584b39712a8c222cf7cc586ae00c09897ce3b21e77b6600ad4c7503f8bd732ef1a8bf98137f18c45c6d6c4 + languageName: node + linkType: hard + "minipass-fetch@npm:^3.0.0": version: 3.0.4 resolution: "minipass-fetch@npm:3.0.4" @@ -22431,7 +22346,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3": +"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3, minipass@npm:^3.1.6": version: 3.3.4 resolution: "minipass@npm:3.3.4" dependencies: @@ -22454,7 +22369,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.2, minipass@npm:^7.0.3": +"minipass@npm:^5.0.0 || ^6.0.2 || ^7.0.0, minipass@npm:^7.0.3": version: 7.0.4 resolution: "minipass@npm:7.0.4" checksum: 10/e864bd02ceb5e0707696d58f7ce3a0b89233f0d686ef0d447a66db705c0846a8dc6f34865cd85256c1472ff623665f616b90b8ff58058b2ad996c5de747d2d18 @@ -22907,6 +22822,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^3.2.1": + version: 3.2.1 + resolution: "node-addon-api@npm:3.2.1" + dependencies: + node-gyp: "npm:latest" + checksum: 10/681b52dfa3e15b0a8e5cf283cc0d8cd5fd2a57c559ae670fcfd20544cbb32f75de7648674110defcd17ab2c76ebef630aa7d2d2f930bc7a8cc439b20fe233518 + languageName: node + linkType: hard + "node-dir@npm:^0.1.10, node-dir@npm:^0.1.17": version: 0.1.17 resolution: "node-dir@npm:0.1.17" @@ -22958,23 +22882,34 @@ __metadata: languageName: node linkType: hard -"node-gyp@npm:^10.0.0": - version: 10.0.1 - resolution: "node-gyp@npm:10.0.1" +"node-gyp-build@npm:^4.3.0": + version: 4.5.0 + resolution: "node-gyp-build@npm:4.5.0" + bin: + node-gyp-build: bin.js + node-gyp-build-optional: optional.js + node-gyp-build-test: build-test.js + checksum: 10/1f6c2b519cfbf13fc60589d40b65d9aa8c8bfaefe99763a9a982a6518a9292c83f41adf558628cfcb748e2a55418ac91718b68bb6be7e02cfac90c82e412de9b + languageName: node + linkType: hard + +"node-gyp@npm:^9.0.0": + version: 9.0.0 + resolution: "node-gyp@npm:9.0.0" dependencies: env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - glob: "npm:^10.3.10" + glob: "npm:^7.1.4" graceful-fs: "npm:^4.2.6" - make-fetch-happen: "npm:^13.0.0" - nopt: "npm:^7.0.0" - proc-log: "npm:^3.0.0" + make-fetch-happen: "npm:^10.0.3" + nopt: "npm:^5.0.0" + npmlog: "npm:^6.0.0" + rimraf: "npm:^3.0.2" semver: "npm:^7.3.5" tar: "npm:^6.1.2" - which: "npm:^4.0.0" + which: "npm:^2.0.2" bin: node-gyp: bin/node-gyp.js - checksum: 10/578cf0c821f258ce4b6ebce4461eca4c991a4df2dee163c0624f2fe09c7d6d37240be4942285a0048d307230248ee0b18382d6623b9a0136ce9533486deddfa8 + checksum: 10/7a9f184dda7bd53970ac52e138b091b417505bef5be0a7d9a902137a55246afaebbae1263a0545b6d7d94af131bcd49ac99f18db0b801c5b4c627dd291c08a7f languageName: node linkType: hard @@ -23044,17 +22979,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:^7.0.0": - version: 7.2.0 - resolution: "nopt@npm:7.2.0" - dependencies: - abbrev: "npm:^2.0.0" - bin: - nopt: bin/nopt.js - checksum: 10/1e7489f17cbda452c8acaf596a8defb4ae477d2a9953b76eb96f4ec3f62c6b421cd5174eaa742f88279871fde9586d8a1d38fb3f53fa0c405585453be31dff4c - languageName: node - linkType: hard - "normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -23091,18 +23015,6 @@ __metadata: languageName: node linkType: hard -"normalize-package-data@npm:^6.0.0": - version: 6.0.0 - resolution: "normalize-package-data@npm:6.0.0" - dependencies: - hosted-git-info: "npm:^7.0.0" - is-core-module: "npm:^2.8.1" - semver: "npm:^7.3.5" - validate-npm-package-license: "npm:^3.0.4" - checksum: 10/e31e31a2ebaef93ef107feb9408f105044eeae9cb7d0d4619544ab2323cd4b15ca648b0d558ac29db2fece161c7b8658206bb27ebe9340df723f7174b3e2759d - languageName: node - linkType: hard - "normalize-path@npm:3.0.0, normalize-path@npm:^3.0.0, normalize-path@npm:~3.0.0": version: 3.0.0 resolution: "normalize-path@npm:3.0.0" @@ -23190,18 +23102,6 @@ __metadata: languageName: node linkType: hard -"npm-package-arg@npm:^11.0.0": - version: 11.0.1 - resolution: "npm-package-arg@npm:11.0.1" - dependencies: - hosted-git-info: "npm:^7.0.0" - proc-log: "npm:^3.0.0" - semver: "npm:^7.3.5" - validate-npm-package-name: "npm:^5.0.0" - checksum: 10/a16e632703e106b3e9a6b4902d14a3493c8371745bcf8ba8f4ea9f152e12d5ed927487931e9adf817d05ba97b04941b33fec1d140dbd7da09181b546fde35b3c - languageName: node - linkType: hard - "npm-packlist@npm:5.1.1": version: 5.1.1 resolution: "npm-packlist@npm:5.1.1" @@ -23216,28 +23116,28 @@ __metadata: languageName: node linkType: hard -"npm-packlist@npm:^8.0.0": - version: 8.0.2 - resolution: "npm-packlist@npm:8.0.2" +"npm-packlist@npm:^7.0.0": + version: 7.0.4 + resolution: "npm-packlist@npm:7.0.4" dependencies: - ignore-walk: "npm:^6.0.4" - checksum: 10/707206e5c09a1b8aa04e590592715ba5ab8732add1bbb5eeaff54b9c6b2740764c9e94c99e390c13245970b51c2cc92b8d44594c2784fcd96f255c7109622322 + ignore-walk: "npm:^6.0.0" + checksum: 10/b24644eefa21d33c55a8f49c64eda4b06edfb7d25853be8ded7346e73c6c447be8a0482314b74f04f94e3f5712e467505dc030826ba55a71d1b948459fad6486 languageName: node linkType: hard -"npm-pick-manifest@npm:^9.0.0": - version: 9.0.0 - resolution: "npm-pick-manifest@npm:9.0.0" +"npm-pick-manifest@npm:^8.0.0": + version: 8.0.2 + resolution: "npm-pick-manifest@npm:8.0.2" dependencies: npm-install-checks: "npm:^6.0.0" npm-normalize-package-bin: "npm:^3.0.0" - npm-package-arg: "npm:^11.0.0" + npm-package-arg: "npm:^10.0.0" semver: "npm:^7.3.5" - checksum: 10/29dca2a838ed35c714df1a76f76616df2df51ce31bc3ca5943a0668b2eca2a5aab448f9f89cadf7a77eb5e3831c554cebaf7802f3e432838acb34c1a74fa2786 + checksum: 10/3f10a34e12cbb576edb694562a32730c6c0244b2929b91202d1be62ece76bc8b282dc7e9535d313d598963f8e3d06d19973611418a191fe3102be149a8fa0910 languageName: node linkType: hard -"npm-registry-fetch@npm:^14.0.3, npm-registry-fetch@npm:^14.0.5": +"npm-registry-fetch@npm:^14.0.0, npm-registry-fetch@npm:^14.0.3, npm-registry-fetch@npm:^14.0.5": version: 14.0.5 resolution: "npm-registry-fetch@npm:14.0.5" dependencies: @@ -23252,21 +23152,6 @@ __metadata: languageName: node linkType: hard -"npm-registry-fetch@npm:^16.0.0": - version: 16.1.0 - resolution: "npm-registry-fetch@npm:16.1.0" - dependencies: - make-fetch-happen: "npm:^13.0.0" - minipass: "npm:^7.0.2" - minipass-fetch: "npm:^3.0.0" - minipass-json-stream: "npm:^1.0.1" - minizlib: "npm:^2.1.2" - npm-package-arg: "npm:^11.0.0" - proc-log: "npm:^3.0.0" - checksum: 10/ba760c9cdacb1219ac5d8fecc26b1c55d502b55d45ab85ad556353b9bc5ba664c226fda54284c06df8c7eecfdcacb1aa065838ea7d1b0189d24c4d3f186309d2 - languageName: node - linkType: hard - "npm-run-path@npm:^4.0.0, npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" @@ -23300,7 +23185,7 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^6.0.2": +"npmlog@npm:^6.0.0, npmlog@npm:^6.0.2": version: 6.0.2 resolution: "npmlog@npm:6.0.2" dependencies: @@ -23335,25 +23220,26 @@ __metadata: languageName: node linkType: hard -"nx@npm:18.0.3, nx@npm:>=17.1.2 < 19": - version: 18.0.3 - resolution: "nx@npm:18.0.3" +"nx@npm:16.10.0, nx@npm:>=16.5.1 < 17": + version: 16.10.0 + resolution: "nx@npm:16.10.0" dependencies: - "@nrwl/tao": "npm:18.0.3" - "@nx/nx-darwin-arm64": "npm:18.0.3" - "@nx/nx-darwin-x64": "npm:18.0.3" - "@nx/nx-freebsd-x64": "npm:18.0.3" - "@nx/nx-linux-arm-gnueabihf": "npm:18.0.3" - "@nx/nx-linux-arm64-gnu": "npm:18.0.3" - "@nx/nx-linux-arm64-musl": "npm:18.0.3" - "@nx/nx-linux-x64-gnu": "npm:18.0.3" - "@nx/nx-linux-x64-musl": "npm:18.0.3" - "@nx/nx-win32-arm64-msvc": "npm:18.0.3" - "@nx/nx-win32-x64-msvc": "npm:18.0.3" + "@nrwl/tao": "npm:16.10.0" + "@nx/nx-darwin-arm64": "npm:16.10.0" + "@nx/nx-darwin-x64": "npm:16.10.0" + "@nx/nx-freebsd-x64": "npm:16.10.0" + "@nx/nx-linux-arm-gnueabihf": "npm:16.10.0" + "@nx/nx-linux-arm64-gnu": "npm:16.10.0" + "@nx/nx-linux-arm64-musl": "npm:16.10.0" + "@nx/nx-linux-x64-gnu": "npm:16.10.0" + "@nx/nx-linux-x64-musl": "npm:16.10.0" + "@nx/nx-win32-arm64-msvc": "npm:16.10.0" + "@nx/nx-win32-x64-msvc": "npm:16.10.0" + "@parcel/watcher": "npm:2.0.4" "@yarnpkg/lockfile": "npm:^1.1.0" "@yarnpkg/parsers": "npm:3.0.0-rc.46" "@zkochan/js-yaml": "npm:0.0.6" - axios: "npm:^1.6.0" + axios: "npm:^1.0.0" chalk: "npm:^4.1.0" cli-cursor: "npm:3.1.0" cli-spinners: "npm:2.6.1" @@ -23364,23 +23250,24 @@ __metadata: figures: "npm:3.2.0" flat: "npm:^5.0.2" fs-extra: "npm:^11.1.0" + glob: "npm:7.1.4" ignore: "npm:^5.0.4" jest-diff: "npm:^29.4.1" js-yaml: "npm:4.1.0" jsonc-parser: "npm:3.2.0" lines-and-columns: "npm:~2.0.3" - minimatch: "npm:9.0.3" + minimatch: "npm:3.0.5" node-machine-id: "npm:1.1.12" npm-run-path: "npm:^4.0.1" open: "npm:^8.4.0" - ora: "npm:5.3.0" - semver: "npm:^7.5.3" + semver: "npm:7.5.3" string-width: "npm:^4.2.3" strong-log-transformer: "npm:^2.1.0" tar-stream: "npm:~2.2.0" tmp: "npm:~0.2.1" tsconfig-paths: "npm:^4.1.2" tslib: "npm:^2.3.0" + v8-compile-cache: "npm:2.3.0" yargs: "npm:^17.6.2" yargs-parser: "npm:21.1.1" peerDependencies: @@ -23414,8 +23301,7 @@ __metadata: optional: true bin: nx: bin/nx.js - nx-cloud: bin/nx-cloud.js - checksum: 10/0ea7bbd0babf5897a4593a87d4107ad26340377a82dbd0c7e4fa414757234e90315f507a6874a3210e3d0e78fd0380956329b225399039ced3a5c000f07b1135 + checksum: 10/748a28491ac607e6d3ab1878dc17b0a78a74be1a272a2775336a8110660d6c39e3e993793391b5810d2e482156421a247cce47b9c3035e4f156129a4b595dd2e languageName: node linkType: hard @@ -23655,22 +23541,6 @@ __metadata: languageName: node linkType: hard -"ora@npm:5.3.0": - version: 5.3.0 - resolution: "ora@npm:5.3.0" - dependencies: - bl: "npm:^4.0.3" - chalk: "npm:^4.1.0" - cli-cursor: "npm:^3.1.0" - cli-spinners: "npm:^2.5.0" - is-interactive: "npm:^1.0.0" - log-symbols: "npm:^4.0.0" - strip-ansi: "npm:^6.0.0" - wcwidth: "npm:^1.0.1" - checksum: 10/989a075b596c297acfee647010e555709bd657dedd9eee9ff99d923cbc65c68b6189c2c9ea58167675b101433509f87d1674a84047c7b766babab15d9220f1d5 - languageName: node - linkType: hard - "ora@npm:^5.4.1": version: 5.4.1 resolution: "ora@npm:5.4.1" @@ -23888,31 +23758,31 @@ __metadata: languageName: node linkType: hard -"pacote@npm:^17.0.5": - version: 17.0.6 - resolution: "pacote@npm:17.0.6" +"pacote@npm:^15.2.0": + version: 15.2.0 + resolution: "pacote@npm:15.2.0" dependencies: - "@npmcli/git": "npm:^5.0.0" + "@npmcli/git": "npm:^4.0.0" "@npmcli/installed-package-contents": "npm:^2.0.1" - "@npmcli/promise-spawn": "npm:^7.0.0" - "@npmcli/run-script": "npm:^7.0.0" - cacache: "npm:^18.0.0" + "@npmcli/promise-spawn": "npm:^6.0.1" + "@npmcli/run-script": "npm:^6.0.0" + cacache: "npm:^17.0.0" fs-minipass: "npm:^3.0.0" - minipass: "npm:^7.0.2" - npm-package-arg: "npm:^11.0.0" - npm-packlist: "npm:^8.0.0" - npm-pick-manifest: "npm:^9.0.0" - npm-registry-fetch: "npm:^16.0.0" + minipass: "npm:^5.0.0" + npm-package-arg: "npm:^10.0.0" + npm-packlist: "npm:^7.0.0" + npm-pick-manifest: "npm:^8.0.0" + npm-registry-fetch: "npm:^14.0.0" proc-log: "npm:^3.0.0" promise-retry: "npm:^2.0.1" - read-package-json: "npm:^7.0.0" + read-package-json: "npm:^6.0.0" read-package-json-fast: "npm:^3.0.0" - sigstore: "npm:^2.2.0" + sigstore: "npm:^1.3.0" ssri: "npm:^10.0.0" tar: "npm:^6.1.11" bin: pacote: lib/bin.js - checksum: 10/fe96b362623128c67b4974bc2d0e8721515927c3546f04e9f3b0df0fe93ab74a8ed59c2896dec3ad1ed5395a8e439b3b64007b32d31b4b86796b50c75dffc924 + checksum: 10/57e18f4f963abb5f67f794158a55c01ad23f76e56dcdc74e6b843dfdda017515b0e8c0f56e60e842cd5af5ab9b351afdc49fc70633994f0e5fc0c6c9f4bcaebc languageName: node linkType: hard @@ -26535,18 +26405,6 @@ __metadata: languageName: node linkType: hard -"read-package-json@npm:^7.0.0": - version: 7.0.0 - resolution: "read-package-json@npm:7.0.0" - dependencies: - glob: "npm:^10.2.2" - json-parse-even-better-errors: "npm:^3.0.0" - normalize-package-data: "npm:^6.0.0" - npm-normalize-package-bin: "npm:^3.0.0" - checksum: 10/b395d5330e9096cb533553e51c6dd123284a744e65c771fbd4d868ca600d2a61b867a4f10723e360608e839101fbe805448dd0079267b3232637ec8bb62bb080 - languageName: node - linkType: hard - "read-pkg-up@npm:^3.0.0": version: 3.0.0 resolution: "read-pkg-up@npm:3.0.0" @@ -27603,6 +27461,17 @@ __metadata: languageName: node linkType: hard +"semver@npm:7.5.3": + version: 7.5.3 + resolution: "semver@npm:7.5.3" + dependencies: + lru-cache: "npm:^6.0.0" + bin: + semver: bin/semver.js + checksum: 10/80b4b3784abff33bacf200727e012dc66768ed5835441e0a802ba9f3f5dd6b10ee366294711f5e7e13d73b82a6127ea55f11f9884d35e76a6a618dc11bc16ccf + languageName: node + linkType: hard + "semver@npm:7.6.0, semver@npm:7.x, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.0 resolution: "semver@npm:7.6.0" @@ -27828,7 +27697,7 @@ __metadata: languageName: node linkType: hard -"sigstore@npm:^1.4.0": +"sigstore@npm:^1.3.0, sigstore@npm:^1.4.0": version: 1.9.0 resolution: "sigstore@npm:1.9.0" dependencies: @@ -27843,20 +27712,6 @@ __metadata: languageName: node linkType: hard -"sigstore@npm:^2.2.0": - version: 2.2.0 - resolution: "sigstore@npm:2.2.0" - dependencies: - "@sigstore/bundle": "npm:^2.1.1" - "@sigstore/core": "npm:^0.2.0" - "@sigstore/protobuf-specs": "npm:^0.2.1" - "@sigstore/sign": "npm:^2.2.1" - "@sigstore/tuf": "npm:^2.3.0" - "@sigstore/verify": "npm:^0.1.0" - checksum: 10/d8e1fda202d2572b3bfa3eded15c9b826429187f52a287549074645670778cbdb78111cb8e3d0274f051838ee500db382be6124c45068985d095df54a3a0bd74 - languageName: node - linkType: hard - "simple-git@npm:^3.6.0": version: 3.16.0 resolution: "simple-git@npm:3.16.0" @@ -28095,24 +27950,13 @@ __metadata: languageName: node linkType: hard -"socks-proxy-agent@npm:^8.0.1": - version: 8.0.2 - resolution: "socks-proxy-agent@npm:8.0.2" +"socks@npm:^2.6.1, socks@npm:^2.6.2": + version: 2.6.2 + resolution: "socks@npm:2.6.2" dependencies: - agent-base: "npm:^7.0.2" - debug: "npm:^4.3.4" - socks: "npm:^2.7.1" - checksum: 10/ea727734bd5b2567597aa0eda14149b3b9674bb44df5937bbb9815280c1586994de734d965e61f1dd45661183d7b41f115fb9e432d631287c9063864cfcc2ecc - languageName: node - linkType: hard - -"socks@npm:^2.6.1, socks@npm:^2.6.2, socks@npm:^2.7.1": - version: 2.7.1 - resolution: "socks@npm:2.7.1" - dependencies: - ip: "npm:^2.0.0" + ip: "npm:^1.1.5" smart-buffer: "npm:^4.2.0" - checksum: 10/5074f7d6a13b3155fa655191df1c7e7a48ce3234b8ccf99afa2ccb56591c195e75e8bb78486f8e9ea8168e95a29573cbaad55b2b5e195160ae4d2ea6811ba833 + checksum: 10/820232ddaeb847ef33312c429fb51aae03e1b774917f189ef491048bb4c4d7742924064f72d7730e3aa08a3ddb6cc2bdcd5949d34c35597e4f6a66eefd994f14 languageName: node linkType: hard @@ -28412,7 +28256,7 @@ __metadata: languageName: node linkType: hard -"ssri@npm:^9.0.1": +"ssri@npm:^9.0.0, ssri@npm:^9.0.1": version: 9.0.1 resolution: "ssri@npm:9.0.1" dependencies: @@ -29828,17 +29672,6 @@ __metadata: languageName: node linkType: hard -"tuf-js@npm:^2.2.0": - version: 2.2.0 - resolution: "tuf-js@npm:2.2.0" - dependencies: - "@tufjs/models": "npm:2.0.0" - debug: "npm:^4.3.4" - make-fetch-happen: "npm:^13.0.0" - checksum: 10/a513ce533c06390b7d8767fe68250adac2535bc63c460e9ab8cbae8253da5ccd6fd204448a460536a6e77f7cf5fcf5a3b104971610f9f319a9b8f95b3b574b95 - languageName: node - linkType: hard - "tunnel-agent@npm:^0.6.0": version: 0.6.0 resolution: "tunnel-agent@npm:0.6.0" @@ -30528,6 +30361,13 @@ __metadata: languageName: node linkType: hard +"v8-compile-cache@npm:2.3.0": + version: 2.3.0 + resolution: "v8-compile-cache@npm:2.3.0" + checksum: 10/7de7423db6f48d76cffae93d70d503e160c97fc85e55945036d719111e20b33c4be5c21aa8b123a3da203bbb3bc4c8180f9667d5ccafcff11d749fae204ec7be + languageName: node + linkType: hard + "v8-to-istanbul@npm:^9.0.0, v8-to-istanbul@npm:^9.0.1": version: 9.1.0 resolution: "v8-to-istanbul@npm:9.1.0" @@ -31276,14 +31116,14 @@ __metadata: languageName: node linkType: hard -"which@npm:^4.0.0": - version: 4.0.0 - resolution: "which@npm:4.0.0" +"which@npm:^3.0.0": + version: 3.0.1 + resolution: "which@npm:3.0.1" dependencies: - isexe: "npm:^3.1.1" + isexe: "npm:^2.0.0" bin: node-which: bin/which.js - checksum: 10/f17e84c042592c21e23c8195108cff18c64050b9efb8459589116999ea9da6dd1509e6a1bac3aeebefd137be00fabbb61b5c2bc0aa0f8526f32b58ee2f545651 + checksum: 10/adf720fe9d84be2d9190458194f814b5e9015ae4b88711b150f30d0f4d0b646544794b86f02c7ebeec1db2029bc3e83a7ff156f542d7521447e5496543e26890 languageName: node linkType: hard @@ -31600,7 +31440,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:17.7.2, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": +"yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 984d2da9aeebad30de91a8d7162ea9cc058b796c Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Fri, 9 Feb 2024 13:48:53 +0000 Subject: [PATCH 14/50] LibraryPanels: Fix issue with repeated library panels (#82255) Fixes an issue where a library panel being repeated by a template variable would briefly use the All value for the first repeat instance --- public/app/features/panel/state/actions.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/features/panel/state/actions.ts b/public/app/features/panel/state/actions.ts index 2e01dd5a732..c326cf812ae 100644 --- a/public/app/features/panel/state/actions.ts +++ b/public/app/features/panel/state/actions.ts @@ -147,15 +147,16 @@ export function loadLibraryPanelAndUpdate(panel: PanelModel): ThunkResult try { const libPanel = await getLibraryPanel(uid, true); panel.initLibraryPanel(libPanel); - await dispatch(initPanelState(panel)); - const dashboard = getStore().dashboard.getModel(); + const dashboard = getStore().dashboard.getModel(); if (panel.repeat && dashboard) { const panelIndex = dashboard.panels.findIndex((p) => p.id === panel.id); dashboard.repeatPanel(panel, panelIndex); dashboard.sortPanelsByGridPos(); dashboard.events.publish(new DashboardPanelsChangedEvent()); } + + await dispatch(initPanelState(panel)); } catch (ex) { console.log('ERROR: ', ex); dispatch( From de4acb27ce203c4755771a195ab7ba533ec939dc Mon Sep 17 00:00:00 2001 From: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Date: Fri, 9 Feb 2024 07:49:48 -0600 Subject: [PATCH 15/50] Table: Add initial row index (#82200) * Add initial row index prop in table --- .../grafana-ui/src/components/Table/RowsList.tsx | 11 ++++++++--- .../src/components/Table/Table.test.tsx | 16 ++++++++++++++++ .../grafana-ui/src/components/Table/Table.tsx | 2 ++ .../grafana-ui/src/components/Table/types.ts | 2 ++ 4 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/grafana-ui/src/components/Table/RowsList.tsx b/packages/grafana-ui/src/components/Table/RowsList.tsx index 878aa1cf69e..6b16f4430a2 100644 --- a/packages/grafana-ui/src/components/Table/RowsList.tsx +++ b/packages/grafana-ui/src/components/Table/RowsList.tsx @@ -44,6 +44,7 @@ interface RowsListProps { onCellFilterAdded?: TableFilterActionCallback; timeRange?: TimeRange; footerPaginationEnabled: boolean; + initialRowIndex?: number; } export const RowsList = (props: RowsListProps) => { @@ -66,9 +67,10 @@ export const RowsList = (props: RowsListProps) => { listHeight, listRef, enableSharedCrosshair = false, + initialRowIndex = undefined, } = props; - const [rowHighlightIndex, setRowHighlightIndex] = useState(undefined); + const [rowHighlightIndex, setRowHighlightIndex] = useState(initialRowIndex); const theme = useTheme2(); const panelContext = usePanelContext(); @@ -203,6 +205,7 @@ export const RowsList = (props: RowsListProps) => { ({ index, style, rowHighlightIndex }: { index: number; style: CSSProperties; rowHighlightIndex?: number }) => { const indexForPagination = rowIndexForPagination(index); const row = rows[indexForPagination]; + let additionalProps: React.HTMLAttributes = {}; prepareRow(row); @@ -210,11 +213,13 @@ export const RowsList = (props: RowsListProps) => { if (rowHighlightIndex !== undefined && row.index === rowHighlightIndex) { style = { ...style, backgroundColor: theme.components.table.rowHoverBackground }; + additionalProps = { + 'aria-selected': 'true', + }; } - return (
onRowHover(index, data)} onMouseLeave={onRowLeave} diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index 4b2322923d4..5d855d25a4c 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -89,6 +89,7 @@ function getTestContext(propOverrides: Partial = {}) { onSortByChange, onCellFilterAdded, onColumnResize, + initialRowIndex: undefined, }; Object.assign(props, propOverrides); @@ -657,4 +658,19 @@ describe('Table', () => { expect(subTable.style.height).toBe('108px'); }); }); + + describe('when mounted with scrolled to specific row', () => { + it('the row should be visible', async () => { + getTestContext({ + initialRowIndex: 2, + }); + expect(getTable()).toBeInTheDocument(); + + const rows = within(getTable()).getAllByRole('row'); + expect(rows).toHaveLength(5); + + let selected = within(getTable()).getByRole('row', { selected: true }); + expect(selected).toBeVisible(); + }); + }); }); diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index 7f65db3f71d..7bb9e9cf976 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -48,6 +48,7 @@ export const Table = memo((props: Props) => { cellHeight = TableCellHeight.Sm, timeRange, enableSharedCrosshair = false, + initialRowIndex = undefined, } = props; const listRef = useRef(null); @@ -307,6 +308,7 @@ export const Table = memo((props: Props) => { tableStyles={tableStyles} footerPaginationEnabled={Boolean(enablePagination)} enableSharedCrosshair={enableSharedCrosshair} + initialRowIndex={initialRowIndex} />
) : ( diff --git a/packages/grafana-ui/src/components/Table/types.ts b/packages/grafana-ui/src/components/Table/types.ts index b99a3cad619..62a6d1471ee 100644 --- a/packages/grafana-ui/src/components/Table/types.ts +++ b/packages/grafana-ui/src/components/Table/types.ts @@ -97,6 +97,8 @@ export interface Props { /** @alpha Used by SparklineCell when provided */ timeRange?: TimeRange; enableSharedCrosshair?: boolean; + // The index of the field value that the table will initialize scrolled to + initialRowIndex?: number; } /** From 790e1feb9319c250d82ef0dbd327c16059e418d8 Mon Sep 17 00:00:00 2001 From: Dan Cech Date: Fri, 9 Feb 2024 09:35:39 -0500 Subject: [PATCH 16/50] Chore: Update test database initialization (#81673) * streamline initialization of test databases, support on-disk sqlite test db * clean up test databases * introduce testsuite helper * use testsuite everywhere we use a test db * update documentation * improve error handling * disable entity integration test until we can figure out locking error --- contribute/backend/style-guide.md | 29 +++- pkg/api/api_test.go | 11 ++ pkg/api/pluginproxy/ds_proxy_test.go | 5 + .../commands/conflict_user_command_test.go | 5 + .../encrypt_datasource_passwords_test.go | 5 + pkg/infra/db/db.go | 10 +- pkg/infra/filestorage/fs_integration_test.go | 5 + pkg/infra/kvstore/kvstore_test.go | 5 + pkg/infra/remotecache/remotecache_test.go | 5 + pkg/infra/serverlock/serverlock_test.go | 5 + .../usagestats/service/usage_stats_test.go | 5 + .../statscollector/concurrent_users_test.go | 6 + pkg/login/social/socialimpl/service_test.go | 5 + pkg/server/wire.go | 3 +- .../accesscontrol/acimpl/service_test.go | 5 + .../accesscontrol/database/database_test.go | 6 + pkg/services/accesscontrol/filter_test.go | 5 + .../accesscontrol/migrator/migrator_test.go | 5 + .../resourcepermissions/store_test.go | 5 + pkg/services/alerting/service_test.go | 5 + .../accesscontrol/accesscontrol_test.go | 5 + .../annotationsimpl/annotations_test.go | 5 + .../loki/historian_store_test.go | 5 + .../anonimpl/anonstore/database_test.go | 5 + pkg/services/anonymous/anonimpl/impl_test.go | 5 + pkg/services/apikey/apikeyimpl/store_test.go | 5 + pkg/services/auth/authimpl/auth_token_test.go | 5 + pkg/services/auth/jwt/auth_test.go | 5 + .../dashboards/database/database_test.go | 6 + .../dashboard_service_integration_test.go | 5 + .../database/database_test.go | 5 + .../service/service_test.go | 5 + .../dashverimpl/store_test.go | 5 + .../datasources/service/datasource_test.go | 5 + .../oauthserver/store/database_test.go | 5 + .../folderimpl/dashboard_folder_store_test.go | 6 + .../libraryelements/libraryelements_test.go | 5 + .../librarypanels/librarypanels_test.go | 6 + .../live/database/tests/storage_test.go | 5 + pkg/services/live/live_test.go | 5 + pkg/services/login/authinfoimpl/store_test.go | 5 + .../loginattemptimpl/store_test.go | 5 + .../ngalert/api/api_provisioning_test.go | 5 + .../ngalert/migration/migration_test.go | 5 + .../ngalert/notifier/alertmanager_test.go | 5 + .../ngalert/provisioning/provisioning_test.go | 11 ++ pkg/services/ngalert/state/manager_test.go | 5 + .../ngalert/store/alertmanager_test.go | 5 + pkg/services/org/orgimpl/store_test.go | 5 + .../playlist/playlistimpl/store_test.go | 5 + .../plugins_integration_test.go | 5 + .../pluginsettings/service/service_test.go | 5 + .../preference/prefimpl/store_test.go | 5 + .../notifiers/config_reader_test.go | 5 + .../publicdashboards/api/common_test.go | 5 + .../database/database_test.go | 6 + .../publicdashboards/service/service_test.go | 5 + pkg/services/query/query_test.go | 5 + .../queryhistory/queryhistory_test.go | 5 + pkg/services/quota/quotaimpl/store_test.go | 5 + pkg/services/searchV2/service_bench_test.go | 5 + .../kvstore/migrations/datasource_mig_test.go | 5 + pkg/services/secrets/kvstore/plugin_test.go | 5 + pkg/services/secrets/manager/manager_test.go | 5 + .../serviceaccounts/database/store_test.go | 5 + .../shorturls/shorturlimpl/shorturl_test.go | 5 + .../signingkeys/signingkeystore/store_test.go | 5 + .../migrations/accesscontrol/test/ac_test.go | 45 ++---- .../sqlstore/migrations/migrations_test.go | 86 ++++++----- .../sqlstore/permissions/dashboard_test.go | 5 + .../sqlstore/searchstore/search_test.go | 5 + pkg/services/sqlstore/sqlstore.go | 136 +++++++++-------- pkg/services/sqlstore/sqlstore_test.go | 8 + pkg/services/sqlstore/sqlutil/sqlutil.go | 142 ++++++++++++++---- .../ssosettings/database/database_test.go | 5 + pkg/services/star/starimpl/store_test.go | 5 + pkg/services/stats/statsimpl/stats_test.go | 5 + .../sqlstash/sql_storage_server_test.go | 5 + .../tests/{common.go => common_test.go} | 5 + .../entity/tests/server_integration_test.go | 4 +- pkg/services/store/service_test.go | 5 + pkg/services/tag/tagimpl/store_test.go | 5 + pkg/services/team/teamimpl/store_test.go | 5 + .../temp_user/tempuserimpl/store_test.go | 5 + pkg/services/user/userimpl/store_test.go | 5 + pkg/tests/api/alerting/api_testing_test.go | 5 + .../api/azuremonitor/azuremonitor_test.go | 5 + pkg/tests/api/correlations/common_test.go | 5 + .../api/dashboards/api_dashboards_test.go | 5 + .../api/elasticsearch/elasticsearch_test.go | 5 + pkg/tests/api/folders/api_folders_test.go | 5 + pkg/tests/api/graphite/graphite_test.go | 5 + pkg/tests/api/influxdb/influxdb_test.go | 5 + pkg/tests/api/loki/loki_test.go | 5 + pkg/tests/api/opentdsb/opentdsb_test.go | 5 + pkg/tests/api/plugins/api_plugins_test.go | 5 + .../backendplugin/backendplugin_test.go | 5 + pkg/tests/api/prometheus/prometheus_test.go | 5 + pkg/tests/api/stats/admin_test.go | 5 + pkg/tests/testinfra/testinfra.go | 18 +-- pkg/tests/testsuite/testsuite.go | 15 ++ pkg/tests/web/index_view_test.go | 5 + pkg/tsdb/legacydata/service/service_test.go | 5 + pkg/tsdb/mssql/mssql_test.go | 16 +- 104 files changed, 801 insertions(+), 189 deletions(-) create mode 100644 pkg/api/api_test.go create mode 100644 pkg/services/ngalert/provisioning/provisioning_test.go rename pkg/services/store/entity/tests/{common.go => common_test.go} (96%) create mode 100644 pkg/tests/testsuite/testsuite.go diff --git a/contribute/backend/style-guide.md b/contribute/backend/style-guide.md index 39fb84c3cdb..4d50bf95b2e 100644 --- a/contribute/backend/style-guide.md +++ b/contribute/backend/style-guide.md @@ -29,13 +29,40 @@ We value clean and readable code, that is loosely coupled and covered by unit te Tests must use the standard library, `testing`. For assertions, prefer using [testify](https://github.com/stretchr/testify). +### Test Suite and Database Tests + +We have a [testsuite](https://github.com/grafana/grafana/tree/main/pkg/tests/testsuite) package which provides utilities for package-level setup and teardown. + +Currently this is just used to ensure that test databases are correctly set up and torn down, but it also provides a place we can attach future tasks. + +Each package SHOULD include a [TestMain](https://pkg.go.dev/testing#hdr-Main) function that calls `testsuite.Run(m)`: + +```go +package mypkg + +import ( + "testing" + + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} +``` + +You only need to define `TestMain` in one `_test.go` file within each package. + +> Warning +> For tests that use the database, you MUST define `TestMain` so that the test databases can be cleaned up properly. + ### Integration Tests We run unit and integration tests separately, to help keep our CI pipeline running smoothly and provide a better developer experience. To properly mark a test as being an integration test, you must format your test function definition as follows, with the function name starting with `TestIntegration` and the check for `testing.Short()`: -``` +```go func TestIntegrationFoo(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/api/api_test.go b/pkg/api/api_test.go new file mode 100644 index 00000000000..0cbeea1f752 --- /dev/null +++ b/pkg/api/api_test.go @@ -0,0 +1,11 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 99ec8bcaaea..13e6ed00735 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -45,9 +45,14 @@ import ( secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/web" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestDataSourceProxy_routeRule(t *testing.T) { cfg := &setting.Cfg{} diff --git a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go index d15eb50f495..c96bf2e2cc6 100644 --- a/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go +++ b/pkg/cmd/grafana-cli/commands/conflict_user_command_test.go @@ -23,11 +23,16 @@ import ( "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) // "Skipping conflicting users test for mysql as it does make unique constraint case insensitive by default const ignoredDatabase = migrator.MySQL +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestBuildConflictBlock(t *testing.T) { type testBuildConflictBlock struct { desc string diff --git a/pkg/cmd/grafana-cli/commands/datamigrations/encrypt_datasource_passwords_test.go b/pkg/cmd/grafana-cli/commands/datamigrations/encrypt_datasource_passwords_test.go index 99b89d59eb6..d24e0782cab 100644 --- a/pkg/cmd/grafana-cli/commands/datamigrations/encrypt_datasource_passwords_test.go +++ b/pkg/cmd/grafana-cli/commands/datamigrations/encrypt_datasource_passwords_test.go @@ -12,9 +12,14 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestPasswordMigrationCommand(t *testing.T) { // setup datasources with password, basic_auth and none store := db.InitTestDB(t) diff --git a/pkg/infra/db/db.go b/pkg/infra/db/db.go index 6cf32ae6606..08cea4a761f 100644 --- a/pkg/infra/db/db.go +++ b/pkg/infra/db/db.go @@ -10,6 +10,8 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/session" + "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" + "github.com/grafana/grafana/pkg/setting" ) type DB interface { @@ -51,10 +53,16 @@ type DB interface { type Session = sqlstore.DBSession type InitTestDBOpt = sqlstore.InitTestDBOpt +var SetupTestDB = sqlstore.SetupTestDB var InitTestDB = sqlstore.InitTestDB -var InitTestDBwithCfg = sqlstore.InitTestDBWithCfg +var CleanupTestDB = sqlstore.CleanupTestDB var ProvideService = sqlstore.ProvideService +func InitTestDBwithCfg(t sqlutil.ITestDB, opts ...InitTestDBOpt) (*sqlstore.SQLStore, *setting.Cfg) { + store := InitTestDB(t, opts...) + return store, store.Cfg +} + func IsTestDbSQLite() bool { if db, present := os.LookupEnv("GRAFANA_TEST_DB"); !present || db == "sqlite" { return true diff --git a/pkg/infra/filestorage/fs_integration_test.go b/pkg/infra/filestorage/fs_integration_test.go index 9336636ab23..13d74b5de35 100644 --- a/pkg/infra/filestorage/fs_integration_test.go +++ b/pkg/infra/filestorage/fs_integration_test.go @@ -12,12 +12,17 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/tests/testsuite" ) const ( pngImageBase64 = "iVBORw0KGgoNAANSUhEUgAAAC4AAAAmCAYAAAC76qlaAAAABHNCSVQICAgIfAhkiAAAABl0RVh0U29mdHdhcmUAZ25vbWUtc2NyZWVuc2hvdO8Dvz4AAABFSURBVFiF7c5BDQAhEACx4/x7XjzwGELSKuiamfke9N8OnBKvidfEa+I18Zp4TbwmXhOvidfEa+I18Zp4TbwmXhOvidc2lcsESD1LGnUAAAAASUVORK5CYII=" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type fsTestCase struct { name string skip *bool diff --git a/pkg/infra/kvstore/kvstore_test.go b/pkg/infra/kvstore/kvstore_test.go index e3471285e26..f94fd7dc0d2 100644 --- a/pkg/infra/kvstore/kvstore_test.go +++ b/pkg/infra/kvstore/kvstore_test.go @@ -10,8 +10,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func createTestableKVStore(t *testing.T) KVStore { t.Helper() diff --git a/pkg/infra/remotecache/remotecache_test.go b/pkg/infra/remotecache/remotecache_test.go index 0daf3202abc..a51a836fa6c 100644 --- a/pkg/infra/remotecache/remotecache_test.go +++ b/pkg/infra/remotecache/remotecache_test.go @@ -13,8 +13,13 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func createTestClient(t *testing.T, opts *setting.RemoteCacheOptions, sqlstore db.DB) CacheStorage { t.Helper() diff --git a/pkg/infra/serverlock/serverlock_test.go b/pkg/infra/serverlock/serverlock_test.go index 79212a02c57..3c060c0f22e 100644 --- a/pkg/infra/serverlock/serverlock_test.go +++ b/pkg/infra/serverlock/serverlock_test.go @@ -11,8 +11,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func createTestableServerLock(t *testing.T) *ServerLockService { t.Helper() diff --git a/pkg/infra/usagestats/service/usage_stats_test.go b/pkg/infra/usagestats/service/usage_stats_test.go index fceb924ad35..352345aca8c 100644 --- a/pkg/infra/usagestats/service/usage_stats_test.go +++ b/pkg/infra/usagestats/service/usage_stats_test.go @@ -25,8 +25,13 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + // This is to ensure that the interface contract is held by the implementation func Test_InterfaceContractValidity(t *testing.T) { newUsageStats := func() usagestats.Service { diff --git a/pkg/infra/usagestats/statscollector/concurrent_users_test.go b/pkg/infra/usagestats/statscollector/concurrent_users_test.go index 15c006441bd..33ffcb3244a 100644 --- a/pkg/infra/usagestats/statscollector/concurrent_users_test.go +++ b/pkg/infra/usagestats/statscollector/concurrent_users_test.go @@ -14,9 +14,15 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/stats/statsimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) +// run tests with cleanup +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestConcurrentUsersMetrics(t *testing.T) { sqlStore, cfg := db.InitTestDBwithCfg(t) statsService := statsimpl.ProvideService(&setting.Cfg{}, sqlStore) diff --git a/pkg/login/social/socialimpl/service_test.go b/pkg/login/social/socialimpl/service_test.go index a1c703ea1ba..a0f80172de4 100644 --- a/pkg/login/social/socialimpl/service_test.go +++ b/pkg/login/social/socialimpl/service_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingsimpl" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestSocialService_ProvideService(t *testing.T) { type testEnv struct { features featuremgmt.FeatureToggles diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 44d0b7bc5ba..adf79d115ce 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -131,6 +131,7 @@ import ( "github.com/grafana/grafana/pkg/services/signingkeys" "github.com/grafana/grafana/pkg/services/signingkeys/signingkeysimpl" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/services/ssosettings" ssoSettingsImpl "github.com/grafana/grafana/pkg/services/ssosettings/ssosettingsimpl" starApi "github.com/grafana/grafana/pkg/services/star/api" @@ -441,7 +442,7 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser return &Server{}, nil } -func InitializeForTest(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { +func InitializeForTest(t sqlutil.ITestDB, cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*TestEnv, error) { wire.Build(wireExtsTestSet) return &TestEnv{Server: &Server{}, SQLStore: &sqlstore.SQLStore{}}, nil } diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index ff0649cc126..d9e6467a072 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -21,8 +21,13 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func setupTestEnv(t testing.TB) *Service { t.Helper() cfg := setting.NewCfg() diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index 34da7704fda..106c9eff14b 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -23,8 +23,14 @@ import ( "github.com/grafana/grafana/pkg/services/team/teamimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +// run tests with cleanup +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type getUserPermissionsTestCase struct { desc string anonymousUser bool diff --git a/pkg/services/accesscontrol/filter_test.go b/pkg/services/accesscontrol/filter_test.go index a68d2a6b4f7..480bee31345 100644 --- a/pkg/services/accesscontrol/filter_test.go +++ b/pkg/services/accesscontrol/filter_test.go @@ -14,6 +14,7 @@ import ( "github.com/grafana/grafana/pkg/services/datasources" dsService "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" ) type filterDatasourcesTestCase struct { @@ -27,6 +28,10 @@ type filterDatasourcesTestCase struct { expectErr bool } +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestFilter_Datasources(t *testing.T) { tests := []filterDatasourcesTestCase{ { diff --git a/pkg/services/accesscontrol/migrator/migrator_test.go b/pkg/services/accesscontrol/migrator/migrator_test.go index 779bc399fa3..bfa35f17efc 100644 --- a/pkg/services/accesscontrol/migrator/migrator_test.go +++ b/pkg/services/accesscontrol/migrator/migrator_test.go @@ -14,8 +14,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" ac "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func batchInsertPermissions(cnt int, sqlStore db.DB) error { now := time.Now() diff --git a/pkg/services/accesscontrol/resourcepermissions/store_test.go b/pkg/services/accesscontrol/resourcepermissions/store_test.go index d83d41a4fad..1af01485939 100644 --- a/pkg/services/accesscontrol/resourcepermissions/store_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/store_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/tests/testsuite" ) type setUserResourcePermissionTest struct { @@ -34,6 +35,10 @@ type setUserResourcePermissionTest struct { seeds []SetResourcePermissionCommand } +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationStore_SetUserResourcePermission(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/alerting/service_test.go b/pkg/services/alerting/service_test.go index d3470094baf..e929498602d 100644 --- a/pkg/services/alerting/service_test.go +++ b/pkg/services/alerting/service_test.go @@ -18,8 +18,13 @@ import ( encryptionservice "github.com/grafana/grafana/pkg/services/encryption/service" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestService(t *testing.T) { sqlStore := &sqlStore{ db: db.InitTestDB(t), diff --git a/pkg/services/annotations/accesscontrol/accesscontrol_test.go b/pkg/services/annotations/accesscontrol/accesscontrol_test.go index 7bb4d74f2a6..12ece80dd2c 100644 --- a/pkg/services/annotations/accesscontrol/accesscontrol_test.go +++ b/pkg/services/annotations/accesscontrol/accesscontrol_test.go @@ -12,9 +12,14 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/stretchr/testify/require" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationAuthorize(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/annotations/annotationsimpl/annotations_test.go b/pkg/services/annotations/annotationsimpl/annotations_test.go index 9b6a824e423..ef407f09a83 100644 --- a/pkg/services/annotations/annotationsimpl/annotations_test.go +++ b/pkg/services/annotations/annotationsimpl/annotations_test.go @@ -29,8 +29,13 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationAnnotationListingWithRBAC(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go index b29272563c5..1123a7bbddc 100644 --- a/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go +++ b/pkg/services/annotations/annotationsimpl/loki/historian_store_test.go @@ -27,11 +27,16 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state/historian" historymodel "github.com/grafana/grafana/pkg/services/ngalert/state/historian/model" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationAlertStateHistoryStore(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/anonymous/anonimpl/anonstore/database_test.go b/pkg/services/anonymous/anonimpl/anonstore/database_test.go index 0da29d5f6b7..54e625662ed 100644 --- a/pkg/services/anonymous/anonimpl/anonstore/database_test.go +++ b/pkg/services/anonymous/anonimpl/anonstore/database_test.go @@ -9,8 +9,13 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationAnonStore_DeleteDevicesOlderThan(t *testing.T) { store := db.InitTestDB(t) anonDBStore := ProvideAnonDBStore(store, 0) diff --git a/pkg/services/anonymous/anonimpl/impl_test.go b/pkg/services/anonymous/anonimpl/impl_test.go index e3c93db2ff2..a84e913f3b1 100644 --- a/pkg/services/anonymous/anonimpl/impl_test.go +++ b/pkg/services/anonymous/anonimpl/impl_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationDeviceService_tag(t *testing.T) { type tagReq struct { httpReq *http.Request diff --git a/pkg/services/apikey/apikeyimpl/store_test.go b/pkg/services/apikey/apikeyimpl/store_test.go index 925e418d404..3b5089526cd 100644 --- a/pkg/services/apikey/apikeyimpl/store_test.go +++ b/pkg/services/apikey/apikeyimpl/store_test.go @@ -14,8 +14,13 @@ import ( "github.com/grafana/grafana/pkg/services/apikey" "github.com/grafana/grafana/pkg/services/auth/identity" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type getStore func(db.DB) store type getApiKeysTestCase struct { diff --git a/pkg/services/auth/authimpl/auth_token_test.go b/pkg/services/auth/authimpl/auth_token_test.go index 5b947fb915b..b0a8beb94f1 100644 --- a/pkg/services/auth/authimpl/auth_token_test.go +++ b/pkg/services/auth/authimpl/auth_token_test.go @@ -19,8 +19,13 @@ import ( "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationUserAuthToken(t *testing.T) { ctx := createTestContext(t) usr := &user.User{ID: int64(10)} diff --git a/pkg/services/auth/jwt/auth_test.go b/pkg/services/auth/jwt/auth_test.go index f8ffc542ccd..fee5ac05b25 100644 --- a/pkg/services/auth/jwt/auth_test.go +++ b/pkg/services/auth/jwt/auth_test.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) type scenarioContext struct { @@ -39,6 +40,10 @@ type cachingScenarioFunc func(*testing.T, cachingScenarioContext) const subject = "foo-subj" +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestVerifyUsingPKIXPublicKeyFile(t *testing.T) { key := rsaKeys[0] unknownKey := rsaKeys[1] diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 0aacdf667ba..c1f6c44f6c9 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -29,9 +29,15 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) +// run tests with cleanup +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationDashboardDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index 4c095921220..79a9ea4d384 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -27,10 +27,15 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) const testOrgID int64 = 1 +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationIntegratedDashboardService(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/dashboardsnapshots/database/database_test.go b/pkg/services/dashboardsnapshots/database/database_test.go index f19461d4fdb..3a2b732ae48 100644 --- a/pkg/services/dashboardsnapshots/database/database_test.go +++ b/pkg/services/dashboardsnapshots/database/database_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationDashboardSnapshotDBAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/dashboardsnapshots/service/service_test.go b/pkg/services/dashboardsnapshots/service/service_test.go index f4b80d9d9c9..70c924e1648 100644 --- a/pkg/services/dashboardsnapshots/service/service_test.go +++ b/pkg/services/dashboardsnapshots/service/service_test.go @@ -15,8 +15,13 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestDashboardSnapshotsService(t *testing.T) { sqlStore := db.InitTestDB(t) cfg := setting.NewCfg() diff --git a/pkg/services/dashboardversion/dashverimpl/store_test.go b/pkg/services/dashboardversion/dashverimpl/store_test.go index f3583bcd82b..cccd1d220ce 100644 --- a/pkg/services/dashboardversion/dashverimpl/store_test.go +++ b/pkg/services/dashboardversion/dashverimpl/store_test.go @@ -13,9 +13,14 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/dashboards" dashver "github.com/grafana/grafana/pkg/services/dashboardversion" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type getStore func(db.DB) store func testIntegrationGetDashboardVersion(t *testing.T, fn getStore) { diff --git a/pkg/services/datasources/service/datasource_test.go b/pkg/services/datasources/service/datasource_test.go index 3672bb9e817..c9818f4bcb4 100644 --- a/pkg/services/datasources/service/datasource_test.go +++ b/pkg/services/datasources/service/datasource_test.go @@ -31,8 +31,13 @@ import ( secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore" secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type dataSourceMockRetriever struct { res []*datasources.DataSource } diff --git a/pkg/services/extsvcauth/oauthserver/store/database_test.go b/pkg/services/extsvcauth/oauthserver/store/database_test.go index 69b50ade317..ae03a1c6f8f 100644 --- a/pkg/services/extsvcauth/oauthserver/store/database_test.go +++ b/pkg/services/extsvcauth/oauthserver/store/database_test.go @@ -12,8 +12,13 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/extsvcauth/oauthserver" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestStore_RegisterAndGetClient(t *testing.T) { s := &store{db: db.InitTestDB(t, db.InitTestDBOpt{FeatureFlags: []string{featuremgmt.FlagExternalServiceAuth}})} tests := []struct { diff --git a/pkg/services/folder/folderimpl/dashboard_folder_store_test.go b/pkg/services/folder/folderimpl/dashboard_folder_store_test.go index 4f69dfbfa1d..12d8c65c1b4 100644 --- a/pkg/services/folder/folderimpl/dashboard_folder_store_test.go +++ b/pkg/services/folder/folderimpl/dashboard_folder_store_test.go @@ -15,8 +15,14 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +// run tests with cleanup +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationDashboardFolderStore(t *testing.T) { var sqlStore *sqlstore.SQLStore var cfg *setting.Cfg diff --git a/pkg/services/libraryelements/libraryelements_test.go b/pkg/services/libraryelements/libraryelements_test.go index a2078ce200d..861c68a0119 100644 --- a/pkg/services/libraryelements/libraryelements_test.go +++ b/pkg/services/libraryelements/libraryelements_test.go @@ -44,12 +44,17 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/web" ) const userInDbName = "user_in_db" const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad" +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestDeleteLibraryPanelsInFolder(t *testing.T) { scenarioWithPanel(t, "When an admin tries to delete a folder that contains connected library elements, it should fail", func(t *testing.T, sc scenarioContext) { diff --git a/pkg/services/librarypanels/librarypanels_test.go b/pkg/services/librarypanels/librarypanels_test.go index 53b6ce6ecda..9c57b3f1d40 100644 --- a/pkg/services/librarypanels/librarypanels_test.go +++ b/pkg/services/librarypanels/librarypanels_test.go @@ -40,11 +40,17 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) const userInDbName = "user_in_db" const userInDbAvatar = "/avatar/402d08de060496d6b6874495fe20f5ad" +// run tests with cleanup +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestConnectLibraryPanelsForDashboard(t *testing.T) { scenarioWithLibraryPanel(t, "When an admin tries to store a dashboard with a library panel, it should connect the two", func(t *testing.T, sc scenarioContext) { diff --git a/pkg/services/live/database/tests/storage_test.go b/pkg/services/live/database/tests/storage_test.go index cd3a483e390..b9ce2d2bd2c 100644 --- a/pkg/services/live/database/tests/storage_test.go +++ b/pkg/services/live/database/tests/storage_test.go @@ -7,8 +7,13 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/live/model" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationLiveMessage(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/live/live_test.go b/pkg/services/live/live_test.go index f19f1a8e30b..f37266acb16 100644 --- a/pkg/services/live/live_test.go +++ b/pkg/services/live/live_test.go @@ -17,8 +17,13 @@ import ( "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func Test_provideLiveService_RedisUnavailable(t *testing.T) { cfg := setting.NewCfg() diff --git a/pkg/services/login/authinfoimpl/store_test.go b/pkg/services/login/authinfoimpl/store_test.go index 147673b3b5c..9fad32337e8 100644 --- a/pkg/services/login/authinfoimpl/store_test.go +++ b/pkg/services/login/authinfoimpl/store_test.go @@ -13,8 +13,13 @@ import ( "github.com/grafana/grafana/pkg/services/login" secretstest "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationAuthInfoStore(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/loginattempt/loginattemptimpl/store_test.go b/pkg/services/loginattempt/loginattemptimpl/store_test.go index 45c27c0c90f..194e64ff572 100644 --- a/pkg/services/loginattempt/loginattemptimpl/store_test.go +++ b/pkg/services/loginattempt/loginattemptimpl/store_test.go @@ -8,8 +8,13 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationLoginAttemptsQuery(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index f2d196cda2a..1fc0fb12bb2 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -35,10 +35,15 @@ import ( secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestProvisioningApi(t *testing.T) { t.Run("policies", func(t *testing.T) { t.Run("successful GET returns 200", func(t *testing.T) { diff --git a/pkg/services/ngalert/migration/migration_test.go b/pkg/services/ngalert/migration/migration_test.go index 63d90b62dc7..ce887535d43 100644 --- a/pkg/services/ngalert/migration/migration_test.go +++ b/pkg/services/ngalert/migration/migration_test.go @@ -30,9 +30,14 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + // TestServiceStart tests the wrapper method that decides when to run the migration based on migration status and settings. func TestServiceStart(t *testing.T) { tc := []struct { diff --git a/pkg/services/ngalert/notifier/alertmanager_test.go b/pkg/services/ngalert/notifier/alertmanager_test.go index 907e97ad362..17e3af804e7 100644 --- a/pkg/services/ngalert/notifier/alertmanager_test.go +++ b/pkg/services/ngalert/notifier/alertmanager_test.go @@ -17,8 +17,13 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/database" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func setupAMTest(t *testing.T) *alertmanager { dir := t.TempDir() cfg := &setting.Cfg{ diff --git a/pkg/services/ngalert/provisioning/provisioning_test.go b/pkg/services/ngalert/provisioning/provisioning_test.go new file mode 100644 index 00000000000..f274f08b869 --- /dev/null +++ b/pkg/services/ngalert/provisioning/provisioning_test.go @@ -0,0 +1,11 @@ +package provisioning + +import ( + "testing" + + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} diff --git a/pkg/services/ngalert/state/manager_test.go b/pkg/services/ngalert/state/manager_test.go index ee582b7856e..365ffbd0c1f 100644 --- a/pkg/services/ngalert/state/manager_test.go +++ b/pkg/services/ngalert/state/manager_test.go @@ -34,9 +34,14 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/state" "github.com/grafana/grafana/pkg/services/ngalert/state/historian" "github.com/grafana/grafana/pkg/services/ngalert/tests" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestWarmStateCache(t *testing.T) { evaluationTime, err := time.Parse("2006-01-02", "2021-03-25") require.NoError(t, err) diff --git a/pkg/services/ngalert/store/alertmanager_test.go b/pkg/services/ngalert/store/alertmanager_test.go index 35a6920c615..4433760c8e1 100644 --- a/pkg/services/ngalert/store/alertmanager_test.go +++ b/pkg/services/ngalert/store/alertmanager_test.go @@ -13,8 +13,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationAlertmanagerStore(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/org/orgimpl/store_test.go b/pkg/services/org/orgimpl/store_test.go index bc9d0452183..292c08d0335 100644 --- a/pkg/services/org/orgimpl/store_test.go +++ b/pkg/services/org/orgimpl/store_test.go @@ -21,8 +21,13 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationOrgDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/playlist/playlistimpl/store_test.go b/pkg/services/playlist/playlistimpl/store_test.go index f0d733deb1c..9eb8c57f767 100644 --- a/pkg/services/playlist/playlistimpl/store_test.go +++ b/pkg/services/playlist/playlistimpl/store_test.go @@ -11,8 +11,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/playlist" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type getStore func(db.DB) store func testIntegrationPlaylistDataAccess(t *testing.T, fn getStore) { diff --git a/pkg/services/pluginsintegration/plugins_integration_test.go b/pkg/services/pluginsintegration/plugins_integration_test.go index f42099c05da..46f450878a5 100644 --- a/pkg/services/pluginsintegration/plugins_integration_test.go +++ b/pkg/services/pluginsintegration/plugins_integration_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore" "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/tsdb/azuremonitor" cloudmonitoring "github.com/grafana/grafana/pkg/tsdb/cloud-monitoring" "github.com/grafana/grafana/pkg/tsdb/cloudwatch" @@ -41,6 +42,10 @@ import ( "github.com/grafana/grafana/pkg/tsdb/tempo" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationPluginManager(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/pluginsintegration/pluginsettings/service/service_test.go b/pkg/services/pluginsintegration/pluginsettings/service/service_test.go index e76d1aedf28..44b7ccea597 100644 --- a/pkg/services/pluginsintegration/pluginsettings/service/service_test.go +++ b/pkg/services/pluginsintegration/pluginsettings/service/service_test.go @@ -12,8 +12,13 @@ import ( "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestService_DecryptedValuesCache(t *testing.T) { t.Run("When plugin settings hasn't been updated, encrypted JSON should be fetched from cache", func(t *testing.T) { ctx := context.Background() diff --git a/pkg/services/preference/prefimpl/store_test.go b/pkg/services/preference/prefimpl/store_test.go index 43d56d4c97f..afbfea86240 100644 --- a/pkg/services/preference/prefimpl/store_test.go +++ b/pkg/services/preference/prefimpl/store_test.go @@ -11,8 +11,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" pref "github.com/grafana/grafana/pkg/services/preference" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type getStore func(db.DB) store func testIntegrationPreferencesDataAccess(t *testing.T, fn getStore) { diff --git a/pkg/services/provisioning/notifiers/config_reader_test.go b/pkg/services/provisioning/notifiers/config_reader_test.go index 979a1c9dd21..ad0f87e6feb 100644 --- a/pkg/services/provisioning/notifiers/config_reader_test.go +++ b/pkg/services/provisioning/notifiers/config_reader_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) var ( @@ -34,6 +35,10 @@ var ( unknownNotifier = "./testdata/test-configs/unknown-notifier" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestNotificationAsConfig(t *testing.T) { var sqlStore *sqlstore.SQLStore var orgService org.Service diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go index 3f016a34e46..7a857bc6f14 100644 --- a/pkg/services/publicdashboards/api/common_test.go +++ b/pkg/services/publicdashboards/api/common_test.go @@ -34,9 +34,14 @@ import ( fakeSecrets "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/web" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func setupTestServer( t *testing.T, cfg *setting.Cfg, diff --git a/pkg/services/publicdashboards/database/database_test.go b/pkg/services/publicdashboards/database/database_test.go index 88a216de755..a76ca6d3154 100644 --- a/pkg/services/publicdashboards/database/database_test.go +++ b/pkg/services/publicdashboards/database/database_test.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) @@ -33,6 +34,11 @@ var DefaultTimeSettings = &TimeSettings{} // Default time to pass in with seconds rounded var DefaultTime = time.Now().UTC().Round(time.Second) +// run tests with cleanup +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestLogPrefix(t *testing.T) { assert.Equal(t, LogPrefix, "publicdashboards.store") } diff --git a/pkg/services/publicdashboards/service/service_test.go b/pkg/services/publicdashboards/service/service_test.go index 54f8f9cd32e..bf982dcd5d8 100644 --- a/pkg/services/publicdashboards/service/service_test.go +++ b/pkg/services/publicdashboards/service/service_test.go @@ -31,6 +31,7 @@ import ( "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -40,6 +41,10 @@ var defaultPubdashTimeSettings = &TimeSettings{} var dashboardData = simplejson.NewFromAny(map[string]any{"time": map[string]any{"from": "now-8h", "to": "now"}}) var SignedInUser = &user.SignedInUser{UserID: 1234, Login: "user@login.com"} +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestLogPrefix(t *testing.T) { assert.Equal(t, LogPrefix, "publicdashboards.service") } diff --git a/pkg/services/query/query_test.go b/pkg/services/query/query_test.go index 3db35179074..c5786af6b37 100644 --- a/pkg/services/query/query_test.go +++ b/pkg/services/query/query_test.go @@ -41,9 +41,14 @@ import ( secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/web" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestParseMetricRequest(t *testing.T) { t.Run("Test a simple single datasource query", func(t *testing.T) { tc := setup(t) diff --git a/pkg/services/queryhistory/queryhistory_test.go b/pkg/services/queryhistory/queryhistory_test.go index 203ae361eef..80535378d86 100644 --- a/pkg/services/queryhistory/queryhistory_test.go +++ b/pkg/services/queryhistory/queryhistory_test.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/web" ) @@ -33,6 +34,10 @@ var ( testDsUID2 = "ABch1a1" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type scenarioContext struct { ctx *web.Context service *QueryHistoryService diff --git a/pkg/services/quota/quotaimpl/store_test.go b/pkg/services/quota/quotaimpl/store_test.go index d332ab97851..2e8211871d1 100644 --- a/pkg/services/quota/quotaimpl/store_test.go +++ b/pkg/services/quota/quotaimpl/store_test.go @@ -8,8 +8,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/quota" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationQuotaDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/searchV2/service_bench_test.go b/pkg/services/searchV2/service_bench_test.go index a6b503c1bcd..7c9c8e55b78 100644 --- a/pkg/services/searchV2/service_bench_test.go +++ b/pkg/services/searchV2/service_bench_test.go @@ -19,8 +19,13 @@ import ( "github.com/grafana/grafana/pkg/services/store" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + // setupBenchEnv will set up a database with folderCount folders and dashboardsPerFolder dashboards per folder // It will also set up and run the search service // and create a signed in user object with explicit permissions on each dashboard and folder. diff --git a/pkg/services/secrets/kvstore/migrations/datasource_mig_test.go b/pkg/services/secrets/kvstore/migrations/datasource_mig_test.go index 275c7391a51..608c7cde49f 100644 --- a/pkg/services/secrets/kvstore/migrations/datasource_mig_test.go +++ b/pkg/services/secrets/kvstore/migrations/datasource_mig_test.go @@ -20,8 +20,13 @@ import ( secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore" secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func SetupTestDataSourceSecretMigrationService(t *testing.T, sqlStore db.DB, kvStore kvstore.KVStore, secretsStore secretskvs.SecretsKVStore, compatibility bool) *DataSourceSecretMigrationService { t.Helper() cfg := &setting.Cfg{} diff --git a/pkg/services/secrets/kvstore/plugin_test.go b/pkg/services/secrets/kvstore/plugin_test.go index 460adea650d..8cd70129df6 100644 --- a/pkg/services/secrets/kvstore/plugin_test.go +++ b/pkg/services/secrets/kvstore/plugin_test.go @@ -8,8 +8,13 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/plugins/backendplugin/secretsmanagerplugin" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + // Set fatal flag to true, then simulate a plugin start failure // Should result in an error from the secret store provider func TestFatalPluginErr_PluginFailsToStartWithFatalFlagSet(t *testing.T) { diff --git a/pkg/services/secrets/manager/manager_test.go b/pkg/services/secrets/manager/manager_test.go index 7b83dc45ef9..50396397e07 100644 --- a/pkg/services/secrets/manager/manager_test.go +++ b/pkg/services/secrets/manager/manager_test.go @@ -20,9 +20,14 @@ import ( "github.com/grafana/grafana/pkg/services/secrets/database" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestSecretsService_EnvelopeEncryption(t *testing.T) { testDB := db.InitTestDB(t) store := database.ProvideSecretsStore(testDB) diff --git a/pkg/services/serviceaccounts/database/store_test.go b/pkg/services/serviceaccounts/database/store_test.go index c3cb7782496..57bbd8e4d46 100644 --- a/pkg/services/serviceaccounts/database/store_test.go +++ b/pkg/services/serviceaccounts/database/store_test.go @@ -20,8 +20,13 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + // Service Account should not create an org on its own func TestStore_CreateServiceAccountOrgNonExistant(t *testing.T) { _, store := setupTestDatabase(t) diff --git a/pkg/services/shorturls/shorturlimpl/shorturl_test.go b/pkg/services/shorturls/shorturlimpl/shorturl_test.go index 37913f1833e..e55804bfcea 100644 --- a/pkg/services/shorturls/shorturlimpl/shorturl_test.go +++ b/pkg/services/shorturls/shorturlimpl/shorturl_test.go @@ -10,8 +10,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/shorturls" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestShortURLService(t *testing.T) { user := &user.SignedInUser{UserID: 1} store := db.InitTestDB(t) diff --git a/pkg/services/signingkeys/signingkeystore/store_test.go b/pkg/services/signingkeys/signingkeystore/store_test.go index 2ddebe006cb..2cb8cadc60c 100644 --- a/pkg/services/signingkeys/signingkeystore/store_test.go +++ b/pkg/services/signingkeys/signingkeystore/store_test.go @@ -10,8 +10,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/signingkeys" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationSigningKeyStore(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go index c811260a588..f145a5b7b65 100644 --- a/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go +++ b/pkg/services/sqlstore/migrations/accesscontrol/test/ac_test.go @@ -2,7 +2,6 @@ package test import ( "fmt" - "os" "testing" "time" @@ -105,37 +104,6 @@ func convertToRawPermissions(permissions []accesscontrol.Permission) []rawPermis return raw } -func getDBType() string { - dbType := migrator.SQLite - - // environment variable present for test db? - if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { - dbType = db - } - return dbType -} - -func getTestDB(t *testing.T, dbType string) sqlutil.TestDB { - switch dbType { - case "mysql": - return sqlutil.MySQLTestDB() - case "postgres": - return sqlutil.PostgresTestDB() - default: - f, err := os.CreateTemp(".", "grafana-test-db-") - require.NoError(t, err) - t.Cleanup(func() { - err := os.Remove(f.Name()) - require.NoError(t, err) - }) - - return sqlutil.TestDB{ - DriverName: "sqlite3", - ConnStr: f.Name(), - } - } -} - func TestMigrations(t *testing.T) { // Run initial migration to have a working DB x := setupTestDB(t) @@ -253,12 +221,21 @@ func TestMigrations(t *testing.T) { func setupTestDB(t *testing.T) *xorm.Engine { t.Helper() - dbType := getDBType() - testDB := getTestDB(t, dbType) + dbType := sqlutil.GetTestDBType() + testDB, err := sqlutil.GetTestDB(dbType) + require.NoError(t, err) + + t.Cleanup(testDB.Cleanup) x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr) require.NoError(t, err) + t.Cleanup(func() { + if err := x.Close(); err != nil { + fmt.Printf("failed to close xorm engine: %v", err) + } + }) + err = migrator.NewDialect(x.DriverName()).CleanDB(x) require.NoError(t, err) diff --git a/pkg/services/sqlstore/migrations/migrations_test.go b/pkg/services/sqlstore/migrations/migrations_test.go index 4479e8c95bc..dc3be715b9e 100644 --- a/pkg/services/sqlstore/migrations/migrations_test.go +++ b/pkg/services/sqlstore/migrations/migrations_test.go @@ -3,7 +3,6 @@ package migrations import ( "errors" "fmt" - "os" "strings" "sync" "sync/atomic" @@ -21,13 +20,23 @@ import ( ) func TestMigrations(t *testing.T) { - testDB := sqlutil.SQLite3TestDB() + testDB, err := sqlutil.GetTestDB(SQLite) + require.NoError(t, err) + + t.Cleanup(testDB.Cleanup) + const query = `select count(*) as count from migration_log` result := struct{ Count int }{} x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr) require.NoError(t, err) + t.Cleanup(func() { + if err := x.Close(); err != nil { + fmt.Printf("failed to close xorm engine: %v", err) + } + }) + err = NewDialect(x.DriverName()).CleanDB(x) require.NoError(t, err) @@ -61,16 +70,25 @@ func TestMigrations(t *testing.T) { } func TestMigrationLock(t *testing.T) { - dbType := getDBType() + dbType := sqlutil.GetTestDBType() if dbType == SQLite { t.Skip() } - testDB := getTestDB(t, dbType) + testDB, err := sqlutil.GetTestDB(dbType) + require.NoError(t, err) + + t.Cleanup(testDB.Cleanup) x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr) require.NoError(t, err) + t.Cleanup(func() { + if err := x.Close(); err != nil { + fmt.Printf("failed to close xorm engine: %v", err) + } + }) + dialect := NewDialect(x.DriverName()) sess := x.NewSession() @@ -157,17 +175,28 @@ func TestMigrationLock(t *testing.T) { } func TestMigratorLocking(t *testing.T) { - dbType := getDBType() - testDB := getTestDB(t, dbType) + dbType := sqlutil.GetTestDBType() + // skip for SQLite for now since it occasionally fails for not clear reason // anyway starting migrations concurretly for the same migrator is impossible use case if dbType == SQLite { t.Skip() } + testDB, err := sqlutil.GetTestDB(dbType) + require.NoError(t, err) + + t.Cleanup(testDB.Cleanup) + x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr) require.NoError(t, err) + t.Cleanup(func() { + if err := x.Close(); err != nil { + fmt.Printf("failed to close xorm engine: %v", err) + } + }) + err = NewDialect(x.DriverName()).CleanDB(x) require.NoError(t, err) @@ -194,17 +223,27 @@ func TestMigratorLocking(t *testing.T) { } func TestDatabaseLocking(t *testing.T) { - dbType := getDBType() + dbType := sqlutil.GetTestDBType() + // skip for SQLite since there is no database locking (only migrator locking) if dbType == SQLite { t.Skip() } - testDB := getTestDB(t, dbType) + testDB, err := sqlutil.GetTestDB(dbType) + require.NoError(t, err) + + t.Cleanup(testDB.Cleanup) x, err := xorm.NewEngine(testDB.DriverName, testDB.ConnStr) require.NoError(t, err) + t.Cleanup(func() { + if err := x.Close(); err != nil { + fmt.Printf("failed to close xorm engine: %v", err) + } + }) + err = NewDialect(x.DriverName()).CleanDB(x) require.NoError(t, err) @@ -280,37 +319,6 @@ func checkStepsAndDatabaseMatch(t *testing.T, mg *Migrator, expected []string) { require.Failf(t, "the number of migrations does not match log in database", msg) } -func getDBType() string { - dbType := SQLite - - // environment variable present for test db? - if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { - dbType = db - } - return dbType -} - -func getTestDB(t *testing.T, dbType string) sqlutil.TestDB { - switch dbType { - case "mysql": - return sqlutil.MySQLTestDB() - case "postgres": - return sqlutil.PostgresTestDB() - default: - f, err := os.CreateTemp(".", "grafana-test-db-") - require.NoError(t, err) - t.Cleanup(func() { - err := os.Remove(f.Name()) - require.NoError(t, err) - }) - - return sqlutil.TestDB{ - DriverName: "sqlite3", - ConnStr: f.Name(), - } - } -} - func replaceDBName(t *testing.T, connStr, dbType string) string { switch dbType { case "mysql": diff --git a/pkg/services/sqlstore/permissions/dashboard_test.go b/pkg/services/sqlstore/permissions/dashboard_test.go index 3992d9f3b24..a586e78b052 100644 --- a/pkg/services/sqlstore/permissions/dashboard_test.go +++ b/pkg/services/sqlstore/permissions/dashboard_test.go @@ -31,8 +31,13 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/tag/tagimpl" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegration_DashboardPermissionFilter(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/sqlstore/searchstore/search_test.go b/pkg/services/sqlstore/searchstore/search_test.go index d2c06ccd795..5e9f9660b2a 100644 --- a/pkg/services/sqlstore/searchstore/search_test.go +++ b/pkg/services/sqlstore/searchstore/search_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) @@ -26,6 +27,10 @@ const ( page int64 = 1 ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestBuilder_EqualResults_Basic(t *testing.T) { user := &user.SignedInUser{ UserID: 1, diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index 334c72ad4f5..a99603c40ad 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -91,8 +91,8 @@ func ProvideService(cfg *setting.Cfg, return s, nil } -func ProvideServiceForTests(cfg *setting.Cfg, features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator) (*SQLStore, error) { - return initTestDB(cfg, features, migrations, InitTestDBOpt{EnsureDefaultOrgAndUser: true}) +func ProvideServiceForTests(t sqlutil.ITestDB, cfg *setting.Cfg, features featuremgmt.FeatureToggles, migrations registry.DatabaseMigrator) (*SQLStore, error) { + return initTestDB(t, cfg, features, migrations, InitTestDBOpt{EnsureDefaultOrgAndUser: true}) } func newSQLStore(cfg *setting.Cfg, engine *xorm.Engine, @@ -145,11 +145,6 @@ func (ss *SQLStore) Migrate(isDatabaseLockingEnabled bool) error { return migrator.Start(isDatabaseLockingEnabled, ss.dbCfg.MigrationLockAttemptTimeout) } -// Sync syncs changes to the database. -func (ss *SQLStore) Sync() error { - return ss.engine.Sync2() -} - // Reset resets database state. // If default org and user creation is enabled, it will be ensured they exist in the database. func (ss *SQLStore) Reset() error { @@ -388,16 +383,10 @@ func (ss *SQLStore) RecursiveQueriesAreSupported() (bool, error) { return *ss.recursiveQueriesAreSupported, nil } -// ITestDB is an interface of arguments for testing db -type ITestDB interface { - Helper() - Fatalf(format string, args ...any) - Logf(format string, args ...any) - Log(args ...any) -} - +var testSQLStoreSetup = false var testSQLStore *SQLStore var testSQLStoreMutex sync.Mutex +var testSQLStoreCleanup []func() // InitTestDBOpt contains options for InitTestDB. type InitTestDBOpt struct { @@ -407,10 +396,10 @@ type InitTestDBOpt struct { } // InitTestDBWithMigration initializes the test DB given custom migrations. -func InitTestDBWithMigration(t ITestDB, migration registry.DatabaseMigrator, opts ...InitTestDBOpt) *SQLStore { +func InitTestDBWithMigration(t sqlutil.ITestDB, migration registry.DatabaseMigrator, opts ...InitTestDBOpt) *SQLStore { t.Helper() features := getFeaturesForTesting(opts...) - store, err := initTestDB(setting.NewCfg(), features, migration, opts...) + store, err := initTestDB(t, setting.NewCfg(), features, migration, opts...) if err != nil { t.Fatalf("failed to initialize sql store: %s", err) } @@ -418,20 +407,46 @@ func InitTestDBWithMigration(t ITestDB, migration registry.DatabaseMigrator, opt } // InitTestDB initializes the test DB. -func InitTestDB(t ITestDB, opts ...InitTestDBOpt) *SQLStore { +func InitTestDB(t sqlutil.ITestDB, opts ...InitTestDBOpt) *SQLStore { t.Helper() features := getFeaturesForTesting(opts...) - store, err := initTestDB(setting.NewCfg(), features, migrations.ProvideOSSMigrations(features), opts...) + store, err := initTestDB(t, setting.NewCfg(), features, migrations.ProvideOSSMigrations(features), opts...) if err != nil { t.Fatalf("failed to initialize sql store: %s", err) } return store } -func InitTestDBWithCfg(t ITestDB, opts ...InitTestDBOpt) (*SQLStore, *setting.Cfg) { - store := InitTestDB(t, opts...) - return store, store.Cfg +func SetupTestDB() { + testSQLStoreMutex.Lock() + defer testSQLStoreMutex.Unlock() + if testSQLStoreSetup { + fmt.Printf("ERROR: Test DB already set up, SetupTestDB called twice\n") + os.Exit(1) + } + testSQLStoreSetup = true +} + +func CleanupTestDB() { + testSQLStoreMutex.Lock() + defer testSQLStoreMutex.Unlock() + if !testSQLStoreSetup { + fmt.Printf("ERROR: Test DB not set up, SetupTestDB not called\n") + os.Exit(1) + } + if testSQLStore != nil { + if err := testSQLStore.GetEngine().Close(); err != nil { + fmt.Printf("Failed to close testSQLStore engine: %s\n", err) + } + + for _, cleanup := range testSQLStoreCleanup { + cleanup() + } + + testSQLStoreCleanup = []func(){} + testSQLStore = nil + } } func getFeaturesForTesting(opts ...InitTestDBOpt) featuremgmt.FeatureToggles { @@ -450,24 +465,43 @@ func getFeaturesForTesting(opts ...InitTestDBOpt) featuremgmt.FeatureToggles { } //nolint:gocyclo -func initTestDB(testCfg *setting.Cfg, +func initTestDB(t sqlutil.ITestDB, testCfg *setting.Cfg, features featuremgmt.FeatureToggles, migration registry.DatabaseMigrator, opts ...InitTestDBOpt) (*SQLStore, error) { testSQLStoreMutex.Lock() defer testSQLStoreMutex.Unlock() + if !testSQLStoreSetup { + t.Fatalf(` + +ERROR: Test DB not set up, are you missing TestMain? + +https://github.com/grafana/grafana/blob/main/contribute/backend/style-guide.md + +Example: + +package mypkg + +import ( + "testing" + + "github.com/grafana/grafana/pkg/tests/testsuite" +) + +func TestMain(m *testing.M) { + testsuite.Run(m) +} + +`) + os.Exit(1) + } if len(opts) == 0 { opts = []InitTestDBOpt{{EnsureDefaultOrgAndUser: false, FeatureFlags: []string{}}} } if testSQLStore == nil { - dbType := migrator.SQLite - - // environment variable present for test db? - if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { - dbType = db - } + dbType := sqlutil.GetTestDBType() // set test db config cfg := setting.NewCfg() @@ -482,21 +516,21 @@ func initTestDB(testCfg *setting.Cfg, if _, err := sec.NewKey("type", dbType); err != nil { return nil, err } - switch dbType { - case "mysql": - if _, err := sec.NewKey("connection_string", sqlutil.MySQLTestDB().ConnStr); err != nil { - return nil, err - } - case "postgres": - if _, err := sec.NewKey("connection_string", sqlutil.PostgresTestDB().ConnStr); err != nil { - return nil, err - } - default: - if _, err := sec.NewKey("connection_string", sqlutil.SQLite3TestDB().ConnStr); err != nil { - return nil, err - } + + testDB, err := sqlutil.GetTestDB(dbType) + if err != nil { + return nil, err } + if _, err := sec.NewKey("connection_string", testDB.ConnStr); err != nil { + return nil, err + } + if _, err := sec.NewKey("path", testDB.Path); err != nil { + return nil, err + } + + testSQLStoreCleanup = append(testSQLStoreCleanup, testDB.Cleanup) + // useful if you already have a database that you want to use for tests. // cannot just set it on testSQLStore as it overrides the config in Init if _, present := os.LookupEnv("SKIP_MIGRATIONS"); present { @@ -539,24 +573,6 @@ func initTestDB(testCfg *setting.Cfg, if err := testSQLStore.Migrate(false); err != nil { return nil, err } - - if err := testSQLStore.Dialect.TruncateDBTables(engine); err != nil { - return nil, err - } - - if err := testSQLStore.Reset(); err != nil { - return nil, err - } - - // Make sure the changes are synced, so they get shared with eventual other DB connections - // XXX: Why is this only relevant when not skipping migrations? - if !testSQLStore.dbCfg.SkipMigrations { - if err := testSQLStore.Sync(); err != nil { - return nil, err - } - } - - return testSQLStore, nil } // nolint:staticcheck diff --git a/pkg/services/sqlstore/sqlstore_test.go b/pkg/services/sqlstore/sqlstore_test.go index 0441a0ff368..54da359d9f0 100644 --- a/pkg/services/sqlstore/sqlstore_test.go +++ b/pkg/services/sqlstore/sqlstore_test.go @@ -2,6 +2,7 @@ package sqlstore import ( "context" + "os" "testing" "time" @@ -11,6 +12,13 @@ import ( "github.com/grafana/grafana/pkg/services/org" ) +func TestMain(m *testing.M) { + SetupTestDB() + code := m.Run() + CleanupTestDB() + os.Exit(code) +} + func TestIntegrationIsUniqueConstraintViolation(t *testing.T) { store := InitTestDB(t) diff --git a/pkg/services/sqlstore/sqlutil/sqlutil.go b/pkg/services/sqlstore/sqlutil/sqlutil.go index 2207bdd1450..38bf06b2476 100644 --- a/pkg/services/sqlstore/sqlutil/sqlutil.go +++ b/pkg/services/sqlstore/sqlutil/sqlutil.go @@ -1,25 +1,123 @@ package sqlutil import ( + "errors" "fmt" + "io/fs" "os" + "path/filepath" ) +// ITestDB is an interface of arguments for testing db +type ITestDB interface { + Helper() + Fatalf(format string, args ...any) + Logf(format string, args ...any) + Log(args ...any) + Cleanup(func()) +} + type TestDB struct { DriverName string ConnStr string + Path string + Cleanup func() } -func SQLite3TestDB() TestDB { - // To run all tests in a local test database, set ConnStr to "grafana_test.db" - return TestDB{ - DriverName: "sqlite3", - // ConnStr specifies an In-memory database shared between connections. - ConnStr: "file::memory:?cache=shared", +func GetTestDBType() string { + dbType := "sqlite3" + + // environment variable present for test db? + if db, present := os.LookupEnv("GRAFANA_TEST_DB"); present { + dbType = db } + return dbType } -func MySQLTestDB() TestDB { +func GetTestDB(dbType string) (*TestDB, error) { + switch dbType { + case "mysql": + return mySQLTestDB() + case "postgres": + return postgresTestDB() + case "sqlite3": + return sqLite3TestDB() + } + + return nil, fmt.Errorf("unknown test db type: %s", dbType) +} + +func sqLite3TestDB() (*TestDB, error) { + if os.Getenv("SQLITE_INMEMORY") == "true" { + return &TestDB{ + DriverName: "sqlite3", + ConnStr: "file::memory:", + Cleanup: func() {}, + }, nil + } + + ret := &TestDB{ + DriverName: "sqlite3", + Cleanup: func() {}, + } + + sqliteDb := os.Getenv("SQLITE_TEST_DB") + if sqliteDb == "" { + // try to create a database file in the user's cache directory + dir, err := os.UserCacheDir() + if err != nil { + return nil, err + } + + // if cache dir doesn't exist, fall back to temp dir + if _, err := os.Stat(dir); errors.Is(err, fs.ErrNotExist) { + dir = os.TempDir() + if _, err := os.Stat(dir); err != nil { + return nil, err + } + } + + err = os.Mkdir(filepath.Join(dir, "grafana-test"), 0750) + if err != nil && !errors.Is(err, fs.ErrExist) { + return nil, err + } + + f, err := os.CreateTemp(filepath.Join(dir, "grafana-test"), "grafana-test-*.db") + if err != nil { + return nil, err + } + + sqliteDb = f.Name() + + ret.Cleanup = func() { + // remove db file if it exists + err := os.Remove(sqliteDb) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + fmt.Printf("Error removing sqlite db file %s: %v\n", sqliteDb, err) + } + + // remove wal & shm files if they exist + err = os.Remove(sqliteDb + "-wal") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + fmt.Printf("Error removing sqlite wal file %s: %v\n", sqliteDb+"-wal", err) + } + err = os.Remove(sqliteDb + "-shm") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + fmt.Printf("Error removing sqlite shm file %s: %v\n", sqliteDb+"-shm", err) + } + } + } + + ret.ConnStr = "file:" + sqliteDb + "?cache=private&mode=rwc" + if os.Getenv("SQLITE_JOURNAL_MODE") != "false" { + ret.ConnStr += "&_journal_mode=WAL" + } + ret.Path = sqliteDb + + return ret, nil +} + +func mySQLTestDB() (*TestDB, error) { host := os.Getenv("MYSQL_HOST") if host == "" { host = "localhost" @@ -29,13 +127,14 @@ func MySQLTestDB() TestDB { port = "3306" } conn_str := fmt.Sprintf("grafana:password@tcp(%s:%s)/grafana_tests?collation=utf8mb4_unicode_ci&sql_mode='ANSI_QUOTES'&parseTime=true", host, port) - return TestDB{ + return &TestDB{ DriverName: "mysql", ConnStr: conn_str, - } + Cleanup: func() {}, + }, nil } -func PostgresTestDB() TestDB { +func postgresTestDB() (*TestDB, error) { host := os.Getenv("POSTGRES_HOST") if host == "" { host = "localhost" @@ -44,25 +143,10 @@ func PostgresTestDB() TestDB { if port == "" { port = "5432" } - connStr := fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanatest sslmode=disable", - host, port) - return TestDB{ + connStr := fmt.Sprintf("user=grafanatest password=grafanatest host=%s port=%s dbname=grafanatest sslmode=disable", host, port) + return &TestDB{ DriverName: "postgres", ConnStr: connStr, - } -} - -func MSSQLTestDB() TestDB { - host := os.Getenv("MSSQL_HOST") - if host == "" { - host = "localhost" - } - port := os.Getenv("MSSQL_PORT") - if port == "" { - port = "1433" - } - return TestDB{ - DriverName: "mssql", - ConnStr: fmt.Sprintf("server=%s;port=%s;database=grafanatest;user id=grafana;password=Password!", host, port), - } + Cleanup: func() {}, + }, nil } diff --git a/pkg/services/ssosettings/database/database_test.go b/pkg/services/ssosettings/database/database_test.go index df62c3f4242..cc937721b03 100644 --- a/pkg/services/ssosettings/database/database_test.go +++ b/pkg/services/ssosettings/database/database_test.go @@ -12,12 +12,17 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/services/ssosettings/models" + "github.com/grafana/grafana/pkg/tests/testsuite" ) const ( withinDuration = 5 * time.Minute ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationGetSSOSettings(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/star/starimpl/store_test.go b/pkg/services/star/starimpl/store_test.go index 52bb12cfde4..1b725ca5224 100644 --- a/pkg/services/star/starimpl/store_test.go +++ b/pkg/services/star/starimpl/store_test.go @@ -8,8 +8,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/star" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type getStore func(db.DB) store func testIntegrationUserStarsDataAccess(t *testing.T, fn getStore) { diff --git a/pkg/services/stats/statsimpl/stats_test.go b/pkg/services/stats/statsimpl/stats_test.go index 6b32590ed18..de191803ac5 100644 --- a/pkg/services/stats/statsimpl/stats_test.go +++ b/pkg/services/stats/statsimpl/stats_test.go @@ -19,8 +19,13 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationStatsDataAccess(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/store/entity/sqlstash/sql_storage_server_test.go b/pkg/services/store/entity/sqlstash/sql_storage_server_test.go index 7851939deb8..17ca9701220 100644 --- a/pkg/services/store/entity/sqlstash/sql_storage_server_test.go +++ b/pkg/services/store/entity/sqlstash/sql_storage_server_test.go @@ -11,8 +11,13 @@ import ( "github.com/grafana/grafana/pkg/services/store/entity" "github.com/grafana/grafana/pkg/services/store/entity/db/dbimpl" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestCreate(t *testing.T) { s := setUpTestServer(t) diff --git a/pkg/services/store/entity/tests/common.go b/pkg/services/store/entity/tests/common_test.go similarity index 96% rename from pkg/services/store/entity/tests/common.go rename to pkg/services/store/entity/tests/common_test.go index 7bd8b4fc9c2..1094f2d8cf6 100644 --- a/pkg/services/store/entity/tests/common.go +++ b/pkg/services/store/entity/tests/common_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/store/entity/sqlstash" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func createServiceAccountAdminToken(t *testing.T, env *server.TestEnv) (string, *user.SignedInUser) { t.Helper() diff --git a/pkg/services/store/entity/tests/server_integration_test.go b/pkg/services/store/entity/tests/server_integration_test.go index 2ac11cb6821..44eec13b401 100644 --- a/pkg/services/store/entity/tests/server_integration_test.go +++ b/pkg/services/store/entity/tests/server_integration_test.go @@ -110,9 +110,9 @@ func requireVersionMatch(t *testing.T, obj *entity.Entity, m objectVersionMatche } func TestIntegrationEntityServer(t *testing.T) { + // TODO figure out why this still runs into sqlite database locked error if true { - // TODO: enable this test once we fix test "database locked" issues - t.Skip() + t.Skip("skipping integration test") } if testing.Short() { diff --git a/pkg/services/store/service_test.go b/pkg/services/store/service_test.go index 0546461787a..298d41a0d91 100644 --- a/pkg/services/store/service_test.go +++ b/pkg/services/store/service_test.go @@ -19,6 +19,7 @@ import ( "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" testdatasource "github.com/grafana/grafana/pkg/tsdb/grafana-testdata-datasource" ) @@ -68,6 +69,10 @@ var ( }}) ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestListFiles(t *testing.T) { roots := []storageRuntime{publicStaticFilesStorage} diff --git a/pkg/services/tag/tagimpl/store_test.go b/pkg/services/tag/tagimpl/store_test.go index 3d631136d39..a3bf9c2783e 100644 --- a/pkg/services/tag/tagimpl/store_test.go +++ b/pkg/services/tag/tagimpl/store_test.go @@ -8,8 +8,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/tag" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + type getStore func(db.DB) store func testIntegrationSavingTags(t *testing.T, fn getStore) { diff --git a/pkg/services/team/teamimpl/store_test.go b/pkg/services/team/teamimpl/store_test.go index e864b08bf18..7c1a4c92c1c 100644 --- a/pkg/services/team/teamimpl/store_test.go +++ b/pkg/services/team/teamimpl/store_test.go @@ -22,8 +22,13 @@ import ( "github.com/grafana/grafana/pkg/services/team/sortopts" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationTeamCommandsAndQueries(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/temp_user/tempuserimpl/store_test.go b/pkg/services/temp_user/tempuserimpl/store_test.go index 7fa636110e5..a5db34a8167 100644 --- a/pkg/services/temp_user/tempuserimpl/store_test.go +++ b/pkg/services/temp_user/tempuserimpl/store_test.go @@ -9,8 +9,13 @@ import ( "github.com/grafana/grafana/pkg/infra/db" tempuser "github.com/grafana/grafana/pkg/services/temp_user" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationTempUserCommandsAndQueries(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index 39d87e8d30d..33ffd7ded73 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -19,8 +19,13 @@ import ( "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationUserGet(t *testing.T) { testCases := []struct { name string diff --git a/pkg/tests/api/alerting/api_testing_test.go b/pkg/tests/api/alerting/api_testing_test.go index 47fe06df74d..6b01b4f163d 100644 --- a/pkg/tests/api/alerting/api_testing_test.go +++ b/pkg/tests/api/alerting/api_testing_test.go @@ -24,6 +24,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) @@ -31,6 +32,10 @@ const ( TESTDATA_UID = "testdata" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestGrafanaRuleConfig(t *testing.T) { dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ DisableLegacyAlerting: true, diff --git a/pkg/tests/api/azuremonitor/azuremonitor_test.go b/pkg/tests/api/azuremonitor/azuremonitor_test.go index 099424f7c91..a7cfc3f0fb9 100644 --- a/pkg/tests/api/azuremonitor/azuremonitor_test.go +++ b/pkg/tests/api/azuremonitor/azuremonitor_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationAzureMonitor(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/correlations/common_test.go b/pkg/tests/api/correlations/common_test.go index 01c166788f0..35696356f03 100644 --- a/pkg/tests/api/correlations/common_test.go +++ b/pkg/tests/api/correlations/common_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) type errorResponseBody struct { @@ -30,6 +31,10 @@ type TestContext struct { t *testing.T } +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func NewTestEnv(t *testing.T) TestContext { t.Helper() dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go index d6b9297db4a..60bd745e740 100644 --- a/pkg/tests/api/dashboards/api_dashboards_test.go +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -30,9 +30,14 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationDashboardQuota(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/elasticsearch/elasticsearch_test.go b/pkg/tests/api/elasticsearch/elasticsearch_test.go index 1bfc4ab9f26..d651be84d5a 100644 --- a/pkg/tests/api/elasticsearch/elasticsearch_test.go +++ b/pkg/tests/api/elasticsearch/elasticsearch_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationElasticsearch(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/folders/api_folders_test.go b/pkg/tests/api/folders/api_folders_test.go index 70af39036f4..d541757864e 100644 --- a/pkg/tests/api/folders/api_folders_test.go +++ b/pkg/tests/api/folders/api_folders_test.go @@ -15,11 +15,16 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/util/retryer" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestGetFolders(t *testing.T) { // Setup Grafana and its Database dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ diff --git a/pkg/tests/api/graphite/graphite_test.go b/pkg/tests/api/graphite/graphite_test.go index 615209d625f..e003574ac1d 100644 --- a/pkg/tests/api/graphite/graphite_test.go +++ b/pkg/tests/api/graphite/graphite_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationGraphite(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/influxdb/influxdb_test.go b/pkg/tests/api/influxdb/influxdb_test.go index bdaf6fefe46..14a9b315fe9 100644 --- a/pkg/tests/api/influxdb/influxdb_test.go +++ b/pkg/tests/api/influxdb/influxdb_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationInflux(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/loki/loki_test.go b/pkg/tests/api/loki/loki_test.go index 8264c093670..f5cffd1c39b 100644 --- a/pkg/tests/api/loki/loki_test.go +++ b/pkg/tests/api/loki/loki_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationLoki(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/opentdsb/opentdsb_test.go b/pkg/tests/api/opentdsb/opentdsb_test.go index 2c04ac73ad1..284e36d9297 100644 --- a/pkg/tests/api/opentdsb/opentdsb_test.go +++ b/pkg/tests/api/opentdsb/opentdsb_test.go @@ -18,8 +18,13 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationOpenTSDB(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/plugins/api_plugins_test.go b/pkg/tests/api/plugins/api_plugins_test.go index 4fc2295ed8b..762c72e3f5c 100644 --- a/pkg/tests/api/plugins/api_plugins_test.go +++ b/pkg/tests/api/plugins/api_plugins_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) const ( @@ -32,6 +33,10 @@ const ( var updateSnapshotFlag = false +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationPlugins(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/plugins/backendplugin/backendplugin_test.go b/pkg/tests/api/plugins/backendplugin/backendplugin_test.go index 31a53717ab0..9da079766e6 100644 --- a/pkg/tests/api/plugins/backendplugin/backendplugin_test.go +++ b/pkg/tests/api/plugins/backendplugin/backendplugin_test.go @@ -27,10 +27,15 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) const loginCookieName = "grafana_session" +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationBackendPlugins(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/prometheus/prometheus_test.go b/pkg/tests/api/prometheus/prometheus_test.go index a2de4708232..df2182ff743 100644 --- a/pkg/tests/api/prometheus/prometheus_test.go +++ b/pkg/tests/api/prometheus/prometheus_test.go @@ -17,8 +17,13 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationPrometheus(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") diff --git a/pkg/tests/api/stats/admin_test.go b/pkg/tests/api/stats/admin_test.go index 5a9c9107115..0204f24d8fa 100644 --- a/pkg/tests/api/stats/admin_test.go +++ b/pkg/tests/api/stats/admin_test.go @@ -16,8 +16,13 @@ import ( "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestIntegrationAdminStats(t *testing.T) { t.Run("with unified alerting enabled", func(t *testing.T) { url := grafanaSetup(t, testinfra.GrafanaOpts{ diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index c05f89f21c7..6a579f1a94d 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -50,9 +50,8 @@ func StartGrafanaEnv(t *testing.T, grafDir, cfgPath string) (string, *server.Tes serverOpts := server.Options{Listener: listener, HomePath: grafDir} apiServerOpts := api.ServerOptions{Listener: listener} - env, err := server.InitializeForTest(cfg, serverOpts, apiServerOpts) + env, err := server.InitializeForTest(t, cfg, serverOpts, apiServerOpts) require.NoError(t, err) - require.NoError(t, env.SQLStore.Sync()) require.NotNil(t, env.SQLStore.Cfg) dbSec, err := env.SQLStore.Cfg.Raw.GetSection("database") @@ -99,21 +98,6 @@ func StartGrafanaEnv(t *testing.T, grafDir, cfgPath string) (string, *server.Tes return addr, env } -// SetUpDatabase sets up the Grafana database. -func SetUpDatabase(t *testing.T, grafDir string) *sqlstore.SQLStore { - t.Helper() - - sqlStore := db.InitTestDB(t, sqlstore.InitTestDBOpt{ - EnsureDefaultOrgAndUser: true, - }) - - // Make sure changes are synced with other goroutines - err := sqlStore.Sync() - require.NoError(t, err) - - return sqlStore -} - // CreateGrafDir creates the Grafana directory. // The log by default is muted in the regression test, to activate it, pass option EnableLog = true func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) { diff --git a/pkg/tests/testsuite/testsuite.go b/pkg/tests/testsuite/testsuite.go new file mode 100644 index 00000000000..87ce57eaa15 --- /dev/null +++ b/pkg/tests/testsuite/testsuite.go @@ -0,0 +1,15 @@ +package testsuite + +import ( + "os" + "testing" + + "github.com/grafana/grafana/pkg/infra/db" +) + +func Run(m *testing.M) { + db.SetupTestDB() + code := m.Run() + db.CleanupTestDB() + os.Exit(code) +} diff --git a/pkg/tests/web/index_view_test.go b/pkg/tests/web/index_view_test.go index e8208f083f2..330553c2638 100644 --- a/pkg/tests/web/index_view_test.go +++ b/pkg/tests/web/index_view_test.go @@ -19,8 +19,13 @@ import ( secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/grafana/grafana/pkg/tests/testsuite" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + // TestIntegrationIndexView tests the Grafana index view. func TestIntegrationIndexView(t *testing.T) { if testing.Short() { diff --git a/pkg/tsdb/legacydata/service/service_test.go b/pkg/tsdb/legacydata/service/service_test.go index 89d8b7c0c74..0abac279e1a 100644 --- a/pkg/tsdb/legacydata/service/service_test.go +++ b/pkg/tsdb/legacydata/service/service_test.go @@ -27,9 +27,14 @@ import ( secretskvs "github.com/grafana/grafana/pkg/services/secrets/kvstore" secretsmng "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/tests/testsuite" "github.com/grafana/grafana/pkg/tsdb/legacydata" ) +func TestMain(m *testing.M) { + testsuite.Run(m) +} + func TestHandleRequest(t *testing.T) { t.Run("Should invoke plugin manager QueryData when handling request for query", func(t *testing.T) { client := &fakePluginsClient{} diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index df9e2f3903c..b7feb0bb4b0 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -5,7 +5,7 @@ import ( "database/sql" "fmt" "math/rand" - "strings" + "os" "testing" "time" @@ -15,7 +15,6 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/infra/db" - "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb/sqleng" ) @@ -1475,9 +1474,16 @@ func TestGenerateConnectionString(t *testing.T) { func initMSSQLTestDB(t *testing.T, jsonData sqleng.JsonData) *sql.DB { t.Helper() - testDB := sqlutil.MSSQLTestDB() - db, err := sql.Open(testDB.DriverName, strings.Replace(testDB.ConnStr, "localhost", - serverIP, 1)) + host := os.Getenv("MSSQL_HOST") + if host == "" { + host = serverIP + } + port := os.Getenv("MSSQL_PORT") + if port == "" { + port = "1433" + } + + db, err := sql.Open("mssql", fmt.Sprintf("server=%s;port=%s;database=grafanatest;user id=grafana;password=Password!", host, port)) require.NoError(t, err) db.SetMaxOpenConns(jsonData.MaxOpenConns) From fbdd27c23758caed634341f4513e8c4bcb1e70f7 Mon Sep 17 00:00:00 2001 From: Konrad Lalik Date: Fri, 9 Feb 2024 15:46:28 +0100 Subject: [PATCH 17/50] Alerting: Add support for UTF-8 characters in notification policies and silences (#81455) * Add label matcher validation to support UTF-8 characters * Add double quotes wrapping and escaping on displating matcher form inputs * Apply matchers encoding and decoding on the RTKQ layer * Fix unescaping order * Revert "Apply matchers encoding and decoding on the RTKQ layer" This reverts commit 4d963c43b589526b9932d547b5873f223974fad3. * Add matchers formatter * Fix code organization to prevent breaking worker * Add matcher formatter to Policy and Modal components * Unquote matchers when finding matching policy instances * Add tests for quoting and unquoting * Rename cloud matcher formatter * Revert unintended change * Allow empty matcher values * fix test --- .../alerting/unified/NotificationPolicies.tsx | 6 +- .../EditNotificationPolicyForm.tsx | 2 +- .../notification-policies/Matchers.tsx | 14 ++-- .../notification-policies/Modals.tsx | 26 ++++--- .../notification-policies/Policy.tsx | 21 ++++-- .../NotificationPolicyMatchers.tsx | 10 ++- .../notificaton-preview/NotificationRoute.tsx | 6 +- .../NotificationRouteDetailsModal.tsx | 24 ++++-- ...eAlertmanagerNotificationRoutingPreview.ts | 5 +- .../alerting/unified/routeGroupsMatcher.ts | 25 ++++++- .../alerting/unified/useRouteGroupsMatcher.ts | 60 ++++++++------- .../alerting/unified/utils/alertmanager.ts | 11 ++- .../alerting/unified/utils/amroutes.test.ts | 74 ++++++++++++++++++- .../alerting/unified/utils/amroutes.ts | 19 ++++- .../alerting/unified/utils/matchers.test.ts | 42 ++++++++++- .../alerting/unified/utils/matchers.ts | 37 ++++++++++ .../unified/utils/notification-policies.ts | 16 +++- 17 files changed, 323 insertions(+), 75 deletions(-) diff --git a/public/app/features/alerting/unified/NotificationPolicies.tsx b/public/app/features/alerting/unified/NotificationPolicies.tsx index 2d29c11cdb3..5309c537cfc 100644 --- a/public/app/features/alerting/unified/NotificationPolicies.tsx +++ b/public/app/features/alerting/unified/NotificationPolicies.tsx @@ -54,8 +54,8 @@ const AmRoutes = () => { const [contactPointFilter, setContactPointFilter] = useState(); const [labelMatchersFilter, setLabelMatchersFilter] = useState([]); + const { selectedAlertmanager, hasConfigurationAPI, isGrafanaAlertmanager } = useAlertmanager(); const { getRouteGroupsMap } = useRouteGroupsMatcher(); - const { selectedAlertmanager, hasConfigurationAPI } = useAlertmanager(); const contactPointsState = useGetContactPointsState(selectedAlertmanager ?? ''); @@ -93,9 +93,9 @@ const AmRoutes = () => { useEffect(() => { if (rootRoute && alertGroups) { - triggerGetRouteGroupsMap(rootRoute, alertGroups); + triggerGetRouteGroupsMap(rootRoute, alertGroups, { unquoteMatchers: !isGrafanaAlertmanager }); } - }, [rootRoute, alertGroups, triggerGetRouteGroupsMap]); + }, [rootRoute, alertGroups, triggerGetRouteGroupsMap, isGrafanaAlertmanager]); // these are computed from the contactPoint and labels matchers filter const routesMatchingFilters = useMemo(() => { diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx index f7eabb4896d..0d2ca067d4e 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx @@ -134,7 +134,7 @@ export const AmRoutesExpandedForm = ({ error={errors.object_matchers?.[index]?.value?.message} > diff --git a/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx b/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx index f379658d2c7..458dc4a0258 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Matchers.tsx @@ -6,12 +6,13 @@ import { GrafanaTheme2 } from '@grafana/data'; import { getTagColorsFromName, useStyles2, Stack } from '@grafana/ui'; import { ObjectMatcher } from 'app/plugins/datasource/alertmanager/types'; +import { MatcherFormatter, matcherFormatter } from '../../utils/matchers'; import { HoverCard } from '../HoverCard'; -type MatchersProps = { matchers: ObjectMatcher[] }; +type MatchersProps = { matchers: ObjectMatcher[]; formatter?: MatcherFormatter }; // renders the first N number of matchers -const Matchers: FC = ({ matchers }) => { +const Matchers: FC = ({ matchers, formatter = 'default' }) => { const styles = useStyles2(getStyles); const NUM_MATCHERS = 5; @@ -24,7 +25,7 @@ const Matchers: FC = ({ matchers }) => { {firstFew.map((matcher) => ( - + ))} {/* TODO hover state to show all matchers we're not showing */} {hasMoreMatchers && ( @@ -51,15 +52,16 @@ const Matchers: FC = ({ matchers }) => { interface MatcherBadgeProps { matcher: ObjectMatcher; + formatter?: MatcherFormatter; } -const MatcherBadge: FC = ({ matcher: [label, operator, value] }) => { +const MatcherBadge: FC = ({ matcher, formatter = 'default' }) => { const styles = useStyles2(getStyles); return ( -
+
- {label} {operator} {value} + {matcherFormatter[formatter](matcher)}
); diff --git a/public/app/features/alerting/unified/components/notification-policies/Modals.tsx b/public/app/features/alerting/unified/components/notification-policies/Modals.tsx index 099642f87d3..9985b7261f3 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Modals.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Modals.tsx @@ -11,6 +11,7 @@ import { } from 'app/plugins/datasource/alertmanager/types'; import { FormAmRoute } from '../../types/amroutes'; +import { MatcherFormatter } from '../../utils/matchers'; import { AlertGroup } from '../alert-groups/AlertGroup'; import { useGetAmRouteReceiverWithGrafanaAppTypes } from '../receivers/grafanaAppReceivers/grafanaApp'; @@ -210,6 +211,7 @@ const useAlertGroupsModal = (): [ const [showModal, setShowModal] = useState(false); const [alertGroups, setAlertGroups] = useState([]); const [matchers, setMatchers] = useState([]); + const [formatter, setFormatter] = useState('default'); const handleDismiss = useCallback(() => { setShowModal(false); @@ -217,13 +219,19 @@ const useAlertGroupsModal = (): [ setMatchers([]); }, []); - const handleShow = useCallback((alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[]) => { - setAlertGroups(alertGroups); - if (matchers) { - setMatchers(matchers); - } - setShowModal(true); - }, []); + const handleShow = useCallback( + (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[], formatter?: MatcherFormatter) => { + setAlertGroups(alertGroups); + if (matchers) { + setMatchers(matchers); + } + if (formatter) { + setFormatter(formatter); + } + setShowModal(true); + }, + [] + ); const instancesByState = useMemo(() => { const instances = alertGroups.flatMap((group) => group.alerts); @@ -242,7 +250,7 @@ const useAlertGroupsModal = (): [ Matchers - + } > @@ -265,7 +273,7 @@ const useAlertGroupsModal = (): [ ), - [alertGroups, handleDismiss, instancesByState, matchers, showModal] + [alertGroups, handleDismiss, instancesByState, matchers, formatter, showModal] ); return [modalElement, handleShow, handleDismiss]; diff --git a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx index b6ba61d28c5..c2069f80cf7 100644 --- a/public/app/features/alerting/unified/components/notification-policies/Policy.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/Policy.tsx @@ -32,7 +32,8 @@ import { ReceiversState } from 'app/types'; import { AlertmanagerAction, useAlertmanagerAbilities, useAlertmanagerAbility } from '../../hooks/useAbilities'; import { INTEGRATION_ICONS } from '../../types/contact-points'; -import { normalizeMatchers } from '../../utils/matchers'; +import { getAmMatcherFormatter } from '../../utils/alertmanager'; +import { MatcherFormatter, normalizeMatchers } from '../../utils/matchers'; import { createContactPointLink, createMuteTimingLink } from '../../utils/misc'; import { InheritableProperties, getInheritedProperties } from '../../utils/notification-policies'; import { Authorize } from '../Authorize'; @@ -55,7 +56,6 @@ interface PolicyComponentProps { provisioned?: boolean; inheritedProperties?: Partial; routesMatchingFilters?: RouteWithID[]; - // routeAlertGroupsMap?: Map; matchingInstancesPreview?: { groupsMap?: Map; enabled: boolean }; @@ -65,7 +65,11 @@ interface PolicyComponentProps { onEditPolicy: (route: RouteWithID, isDefault?: boolean, isAutogenerated?: boolean) => void; onAddPolicy: (route: RouteWithID) => void; onDeletePolicy: (route: RouteWithID) => void; - onShowAlertInstances: (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[]) => void; + onShowAlertInstances: ( + alertGroups: AlertmanagerGroup[], + matchers?: ObjectMatcher[], + formatter?: MatcherFormatter + ) => void; isAutoGenerated?: boolean; } @@ -194,7 +198,7 @@ const Policy = (props: PolicyComponentProps) => { ) ) : hasMatchers ? ( - + ) : ( No matchers )} @@ -325,7 +329,11 @@ interface MetadataRowProps { matchingAlertGroups?: AlertmanagerGroup[]; matchers?: ObjectMatcher[]; isDefaultPolicy: boolean; - onShowAlertInstances: (alertGroups: AlertmanagerGroup[], matchers?: ObjectMatcher[]) => void; + onShowAlertInstances: ( + alertGroups: AlertmanagerGroup[], + matchers?: ObjectMatcher[], + formatter?: MatcherFormatter + ) => void; } function MetadataRow({ @@ -361,7 +369,8 @@ function MetadataRow({ { - matchingAlertGroups && onShowAlertInstances(matchingAlertGroups, matchers); + matchingAlertGroups && + onShowAlertInstances(matchingAlertGroups, matchers, getAmMatcherFormatter(alertManagerSourceName)); }} data-testid="matching-instances" > diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPolicyMatchers.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPolicyMatchers.tsx index ad459deedb0..cb7c4492d1c 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPolicyMatchers.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationPolicyMatchers.tsx @@ -4,18 +4,24 @@ import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { useStyles2 } from '@grafana/ui'; +import { MatcherFormatter } from '../../../utils/matchers'; import { Matchers } from '../../notification-policies/Matchers'; import { hasEmptyMatchers, isDefaultPolicy, RouteWithPath } from './route'; -export function NotificationPolicyMatchers({ route }: { route: RouteWithPath }) { +interface Props { + route: RouteWithPath; + matcherFormatter: MatcherFormatter; +} + +export function NotificationPolicyMatchers({ route, matcherFormatter }: Props) { const styles = useStyles2(getStyles); if (isDefaultPolicy(route)) { return
Default policy
; } else if (hasEmptyMatchers(route)) { return
No matchers
; } else { - return ; + return ; } } diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx index 1a7f9a13944..a95e1bbb8a6 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRoute.tsx @@ -9,6 +9,7 @@ import { Button, getTagColorIndexFromName, TagList, useStyles2 } from '@grafana/ import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types'; import { Stack } from '../../../../../../plugins/datasource/parca/QueryEditor/Stack'; +import { getAmMatcherFormatter } from '../../../utils/alertmanager'; import { AlertInstanceMatch } from '../../../utils/notification-policies'; import { CollapseToggle } from '../../CollapseToggle'; import { MetaText } from '../../MetaText'; @@ -58,7 +59,10 @@ function NotificationRouteHeader({
onExpandRouteClick(!expandRoute)} className={styles.expandable}> Notification policy - +
diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx index f66e525f8a1..8de20bc227d 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/NotificationRouteDetailsModal.tsx @@ -3,20 +3,26 @@ import { compact } from 'lodash'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; -import { Button, Icon, Modal, useStyles2 } from '@grafana/ui'; +import { Button, Icon, Modal, Stack, useStyles2 } from '@grafana/ui'; import { Receiver } from '../../../../../../plugins/datasource/alertmanager/types'; -import { Stack } from '../../../../../../plugins/datasource/parca/QueryEditor/Stack'; import { AlertmanagerAction } from '../../../hooks/useAbilities'; import { AlertmanagerProvider } from '../../../state/AlertmanagerContext'; -import { GRAFANA_DATASOURCE_NAME } from '../../../utils/datasource'; +import { getAmMatcherFormatter } from '../../../utils/alertmanager'; +import { MatcherFormatter } from '../../../utils/matchers'; import { makeAMLink } from '../../../utils/misc'; import { Authorize } from '../../Authorize'; import { Matchers } from '../../notification-policies/Matchers'; import { hasEmptyMatchers, isDefaultPolicy, RouteWithPath } from './route'; -function PolicyPath({ route, routesByIdMap }: { routesByIdMap: Map; route: RouteWithPath }) { +interface Props { + routesByIdMap: Map; + route: RouteWithPath; + matcherFormatter: MatcherFormatter; +} + +function PolicyPath({ route, routesByIdMap, matcherFormatter }: Props) { const styles = useStyles2(getStyles); const routePathIds = route.path?.slice(1) ?? []; const routePathObjects = [...compact(routePathIds.map((id) => routesByIdMap.get(id))), route]; @@ -31,7 +37,7 @@ function PolicyPath({ route, routesByIdMap }: { routesByIdMap: MapNo matchers
) : ( - + )}
@@ -60,7 +66,7 @@ export function NotificationRouteDetailsModal({ const isDefault = isDefaultPolicy(route); return ( - + {!isDefault && ( <> - + )}
diff --git a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts index 0762ae77aa2..a8df723da12 100644 --- a/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts +++ b/public/app/features/alerting/unified/components/rule-editor/notificaton-preview/useAlertmanagerNotificationRoutingPreview.ts @@ -6,6 +6,7 @@ import { Labels } from '../../../../../../types/unified-alerting-dto'; import { useAlertmanagerConfig } from '../../../hooks/useAlertmanagerConfig'; import { useRouteGroupsMatcher } from '../../../useRouteGroupsMatcher'; import { addUniqueIdentifierToRoute } from '../../../utils/amroutes'; +import { GRAFANA_RULES_SOURCE_NAME } from '../../../utils/datasource'; import { AlertInstanceMatch, computeInheritedTree, normalizeRoute } from '../../../utils/notification-policies'; import { getRoutesByIdMap, RouteWithPath } from './route'; @@ -55,7 +56,9 @@ export const useAlertmanagerNotificationRoutingPreview = ( if (!rootRoute) { return; } - return await matchInstancesToRoute(rootRoute, potentialInstances); + return await matchInstancesToRoute(rootRoute, potentialInstances, { + unquoteMatchers: alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME, + }); }, [rootRoute, potentialInstances]); return { diff --git a/public/app/features/alerting/unified/routeGroupsMatcher.ts b/public/app/features/alerting/unified/routeGroupsMatcher.ts index 80b951ffc14..bce330c7f78 100644 --- a/public/app/features/alerting/unified/routeGroupsMatcher.ts +++ b/public/app/features/alerting/unified/routeGroupsMatcher.ts @@ -6,11 +6,20 @@ import { findMatchingAlertGroups, findMatchingRoutes, normalizeRoute, + unquoteRouteMatchers, } from './utils/notification-policies'; +export interface MatchOptions { + unquoteMatchers?: boolean; +} + export const routeGroupsMatcher = { - getRouteGroupsMap(rootRoute: RouteWithID, groups: AlertmanagerGroup[]): Map { - const normalizedRootRoute = normalizeRoute(rootRoute); + getRouteGroupsMap( + rootRoute: RouteWithID, + groups: AlertmanagerGroup[], + options?: MatchOptions + ): Map { + const normalizedRootRoute = getNormalizedRoute(rootRoute, options); function addRouteGroups(route: RouteWithID, acc: Map) { const routeGroups = findMatchingAlertGroups(normalizedRootRoute, route, groups); @@ -25,10 +34,14 @@ export const routeGroupsMatcher = { return routeGroupsMap; }, - matchInstancesToRoute(routeTree: RouteWithID, instancesToMatch: Labels[]): Map { + matchInstancesToRoute( + routeTree: RouteWithID, + instancesToMatch: Labels[], + options?: MatchOptions + ): Map { const result = new Map(); - const normalizedRootRoute = normalizeRoute(routeTree); + const normalizedRootRoute = getNormalizedRoute(routeTree, options); instancesToMatch.forEach((instance) => { const matchingRoutes = findMatchingRoutes(normalizedRootRoute, Object.entries(instance)); @@ -47,4 +60,8 @@ export const routeGroupsMatcher = { }, }; +function getNormalizedRoute(route: RouteWithID, options?: MatchOptions): RouteWithID { + return options?.unquoteMatchers ? unquoteRouteMatchers(normalizeRoute(route)) : normalizeRoute(route); +} + export type RouteGroupsMatcher = typeof routeGroupsMatcher; diff --git a/public/app/features/alerting/unified/useRouteGroupsMatcher.ts b/public/app/features/alerting/unified/useRouteGroupsMatcher.ts index 2b57e4e0d34..a48d3e4c611 100644 --- a/public/app/features/alerting/unified/useRouteGroupsMatcher.ts +++ b/public/app/features/alerting/unified/useRouteGroupsMatcher.ts @@ -6,7 +6,7 @@ import { Labels } from '../../../types/unified-alerting-dto'; import { logError, logInfo } from './Analytics'; import { createWorker } from './createRouteGroupsMatcherWorker'; -import type { RouteGroupsMatcher } from './routeGroupsMatcher'; +import type { MatchOptions, RouteGroupsMatcher } from './routeGroupsMatcher'; let routeMatcher: comlink.Remote | undefined; @@ -55,43 +55,49 @@ export function useRouteGroupsMatcher() { return () => null; }, []); - const getRouteGroupsMap = useCallback(async (rootRoute: RouteWithID, alertGroups: AlertmanagerGroup[]) => { - validateWorker(routeMatcher); + const getRouteGroupsMap = useCallback( + async (rootRoute: RouteWithID, alertGroups: AlertmanagerGroup[], options?: MatchOptions) => { + validateWorker(routeMatcher); - const startTime = performance.now(); + const startTime = performance.now(); - const result = await routeMatcher.getRouteGroupsMap(rootRoute, alertGroups); + const result = await routeMatcher.getRouteGroupsMap(rootRoute, alertGroups, options); - const timeSpent = performance.now() - startTime; + const timeSpent = performance.now() - startTime; - logInfo(`Route Groups Matched in ${timeSpent} ms`, { - matchingTime: timeSpent.toString(), - alertGroupsCount: alertGroups.length.toString(), - // Counting all nested routes might be too time-consuming, so we only count the first level - topLevelRoutesCount: rootRoute.routes?.length.toString() ?? '0', - }); + logInfo(`Route Groups Matched in ${timeSpent} ms`, { + matchingTime: timeSpent.toString(), + alertGroupsCount: alertGroups.length.toString(), + // Counting all nested routes might be too time-consuming, so we only count the first level + topLevelRoutesCount: rootRoute.routes?.length.toString() ?? '0', + }); - return result; - }, []); + return result; + }, + [] + ); - const matchInstancesToRoute = useCallback(async (rootRoute: RouteWithID, instancesToMatch: Labels[]) => { - validateWorker(routeMatcher); + const matchInstancesToRoute = useCallback( + async (rootRoute: RouteWithID, instancesToMatch: Labels[], options?: MatchOptions) => { + validateWorker(routeMatcher); - const startTime = performance.now(); + const startTime = performance.now(); - const result = await routeMatcher.matchInstancesToRoute(rootRoute, instancesToMatch); + const result = await routeMatcher.matchInstancesToRoute(rootRoute, instancesToMatch, options); - const timeSpent = performance.now() - startTime; + const timeSpent = performance.now() - startTime; - logInfo(`Instances Matched in ${timeSpent} ms`, { - matchingTime: timeSpent.toString(), - instancesToMatchCount: instancesToMatch.length.toString(), - // Counting all nested routes might be too time-consuming, so we only count the first level - topLevelRoutesCount: rootRoute.routes?.length.toString() ?? '0', - }); + logInfo(`Instances Matched in ${timeSpent} ms`, { + matchingTime: timeSpent.toString(), + instancesToMatchCount: instancesToMatch.length.toString(), + // Counting all nested routes might be too time-consuming, so we only count the first level + topLevelRoutesCount: rootRoute.routes?.length.toString() ?? '0', + }); - return result; - }, []); + return result; + }, + [] + ); return { getRouteGroupsMap, matchInstancesToRoute }; } diff --git a/public/app/features/alerting/unified/utils/alertmanager.ts b/public/app/features/alerting/unified/utils/alertmanager.ts index e81b9195ffc..2738b703844 100644 --- a/public/app/features/alerting/unified/utils/alertmanager.ts +++ b/public/app/features/alerting/unified/utils/alertmanager.ts @@ -15,7 +15,8 @@ import { Labels } from 'app/types/unified-alerting-dto'; import { MatcherFieldValue } from '../types/silence-form'; import { getAllDataSources } from './config'; -import { DataSourceType } from './datasource'; +import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './datasource'; +import { MatcherFormatter, unquoteWithUnescape } from './matchers'; export function addDefaultsToAlertmanagerConfig(config: AlertManagerCortexConfig): AlertManagerCortexConfig { // add default receiver if it does not exist @@ -53,6 +54,10 @@ export function renameMuteTimings(newMuteTimingName: string, oldMuteTimingName: }; } +export function unescapeObjectMatchers(matchers: ObjectMatcher[]): ObjectMatcher[] { + return matchers.map(([name, operator, value]) => [name, operator, unquoteWithUnescape(value)]); +} + export function matcherToOperator(matcher: Matcher): MatcherOperator { if (matcher.isEqual) { if (matcher.isRegex) { @@ -177,6 +182,10 @@ export function combineMatcherStrings(...matcherStrings: string[]): string { return matchersToString(uniqueMatchers); } +export function getAmMatcherFormatter(alertmanagerSourceName?: string): MatcherFormatter { + return alertmanagerSourceName === GRAFANA_RULES_SOURCE_NAME ? 'default' : 'unquote'; +} + export function getAllAlertmanagerDataSources() { return getAllDataSources().filter((ds) => ds.type === DataSourceType.Alertmanager); } diff --git a/public/app/features/alerting/unified/utils/amroutes.test.ts b/public/app/features/alerting/unified/utils/amroutes.test.ts index 3c66e772d0b..dbb71f012c2 100644 --- a/public/app/features/alerting/unified/utils/amroutes.test.ts +++ b/public/app/features/alerting/unified/utils/amroutes.test.ts @@ -1,8 +1,9 @@ -import { Route } from 'app/plugins/datasource/alertmanager/types'; +import { MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types'; import { FormAmRoute } from '../types/amroutes'; import { amRouteToFormAmRoute, emptyRoute, formAmRouteToAmRoute } from './amroutes'; +import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; const emptyAmRoute: Route = { receiver: '', @@ -53,6 +54,58 @@ describe('formAmRouteToAmRoute', () => { expect(amRoute.group_by).toStrictEqual(['SHOULD BE SET']); }); }); + + it('should quote and escape matcher values', () => { + // Arrange + const route: FormAmRoute = buildFormAmRoute({ + id: '1', + object_matchers: [ + { name: 'foo', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar"baz' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar\\baz' }, + { name: 'foo', operator: MatcherOperator.equal, value: '\\bar\\baz"\\' }, + ], + }); + + // Act + const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' }); + + // Assert + expect(amRoute.matchers).toStrictEqual([ + 'foo="bar"', + 'foo="bar\\"baz"', + 'foo="bar\\\\baz"', + 'foo="\\\\bar\\\\baz\\"\\\\"', + ]); + }); + + it('should allow matchers with empty values for cloud AM', () => { + // Arrange + const route: FormAmRoute = buildFormAmRoute({ + id: '1', + object_matchers: [{ name: 'foo', operator: MatcherOperator.equal, value: '' }], + }); + + // Act + const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' }); + + // Assert + expect(amRoute.matchers).toStrictEqual(['foo=""']); + }); + + it('should allow matchers with empty values for Grafana AM', () => { + // Arrange + const route: FormAmRoute = buildFormAmRoute({ + id: '1', + object_matchers: [{ name: 'foo', operator: MatcherOperator.equal, value: '' }], + }); + + // Act + const amRoute = formAmRouteToAmRoute(GRAFANA_RULES_SOURCE_NAME, route, { id: 'root' }); + + // Assert + expect(amRoute.object_matchers).toStrictEqual([['foo', MatcherOperator.equal, '']]); + }); }); describe('amRouteToFormAmRoute', () => { @@ -101,4 +154,23 @@ describe('amRouteToFormAmRoute', () => { expect(formRoute.overrideGrouping).toBe(true); }); }); + + it('should unquote and unescape matchers values', () => { + // Arrange + const amRoute = buildAmRoute({ + matchers: ['foo=bar', 'foo="bar"', 'foo="bar"baz"', 'foo="bar\\\\baz"', 'foo="\\\\bar\\\\baz"\\\\"'], + }); + + // Act + const formRoute = amRouteToFormAmRoute(amRoute); + + // Assert + expect(formRoute.object_matchers).toStrictEqual([ + { name: 'foo', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar"baz' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar\\baz' }, + { name: 'foo', operator: MatcherOperator.equal, value: '\\bar\\baz"\\' }, + ]); + }); }); diff --git a/public/app/features/alerting/unified/utils/amroutes.ts b/public/app/features/alerting/unified/utils/amroutes.ts index 9fc29f0c3d0..f3724401587 100644 --- a/public/app/features/alerting/unified/utils/amroutes.ts +++ b/public/app/features/alerting/unified/utils/amroutes.ts @@ -8,7 +8,7 @@ import { MatcherFieldValue } from '../types/silence-form'; import { matcherToMatcherField } from './alertmanager'; import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; -import { normalizeMatchers, parseMatcher } from './matchers'; +import { normalizeMatchers, parseMatcher, quoteWithEscape, unquoteWithUnescape } from './matchers'; import { findExistingRoute } from './routeTree'; import { isValidPrometheusDuration, safeParseDurationstr } from './time'; @@ -94,7 +94,14 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo const objectMatchers = route.object_matchers?.map((matcher) => ({ name: matcher[0], operator: matcher[1], value: matcher[2] })) ?? []; - const matchers = route.matchers?.map((matcher) => matcherToMatcherField(parseMatcher(matcher))) ?? []; + const matchers = + route.matchers + ?.map((matcher) => matcherToMatcherField(parseMatcher(matcher))) + .map(({ name, operator, value }) => ({ + name, + operator, + value: unquoteWithUnescape(value), + })) ?? []; return { id, @@ -149,8 +156,10 @@ export const formAmRouteToAmRoute = ( const overrideRepeatInterval = overrideTimings && repeatIntervalValue; const repeat_interval = overrideRepeatInterval ? repeatIntervalValue : INHERIT_FROM_PARENT; + + // Empty matcher values are valid. Such matchers require specified label to not exists const object_matchers: ObjectMatcher[] | undefined = formAmRoute.object_matchers - ?.filter((route) => route.name && route.value && route.operator) + ?.filter((route) => route.name && route.operator && route.value !== null && route.value !== undefined) .map(({ name, operator, value }) => [name, operator, value]); const routes = formAmRoute.routes?.map((subRoute) => @@ -176,7 +185,9 @@ export const formAmRouteToAmRoute = ( // Grafana maintains a fork of AM to support all utf-8 characters in the "object_matchers" property values but this // does not exist in upstream AlertManager if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) { - amRoute.matchers = formAmRoute.object_matchers?.map(({ name, operator, value }) => `${name}${operator}${value}`); + amRoute.matchers = formAmRoute.object_matchers?.map( + ({ name, operator, value }) => `${name}${operator}${quoteWithEscape(value)}` + ); amRoute.object_matchers = undefined; } else { amRoute.object_matchers = normalizeMatchers(amRoute); diff --git a/public/app/features/alerting/unified/utils/matchers.test.ts b/public/app/features/alerting/unified/utils/matchers.test.ts index 43b3cbc2320..9f879789a22 100644 --- a/public/app/features/alerting/unified/utils/matchers.test.ts +++ b/public/app/features/alerting/unified/utils/matchers.test.ts @@ -1,6 +1,12 @@ import { MatcherOperator, Route } from '../../../../plugins/datasource/alertmanager/types'; -import { getMatcherQueryParams, normalizeMatchers, parseQueryParamMatchers } from './matchers'; +import { + getMatcherQueryParams, + normalizeMatchers, + parseQueryParamMatchers, + quoteWithEscape, + unquoteWithUnescape, +} from './matchers'; describe('Unified Alerting matchers', () => { describe('getMatcherQueryParams tests', () => { @@ -61,3 +67,37 @@ describe('Unified Alerting matchers', () => { }); }); }); + +describe('quoteWithEscape', () => { + const samples: string[][] = [ + ['bar', '"bar"'], + ['b"ar"', '"b\\"ar\\""'], + ['b\\ar\\', '"b\\\\ar\\\\"'], + ['wa{r}ni$ng!', '"wa{r}ni$ng!"'], + ]; + + it.each(samples)('should escape and quote %s to %s', (raw, quoted) => { + const quotedMatcher = quoteWithEscape(raw); + expect(quotedMatcher).toBe(quoted); + }); +}); + +describe('unquoteWithUnescape', () => { + const samples: string[][] = [ + ['bar', 'bar'], + ['"bar"', 'bar'], + ['"b\\"ar\\""', 'b"ar"'], + ['"b\\\\ar\\\\"', 'b\\ar\\'], + ['"wa{r}ni$ng!"', 'wa{r}ni$ng!'], + ]; + + it.each(samples)('should unquote and unescape %s to %s', (quoted, raw) => { + const unquotedMatcher = unquoteWithUnescape(quoted); + expect(unquotedMatcher).toBe(raw); + }); + + it('should not unescape unquoted string', () => { + const unquoted = unquoteWithUnescape('un\\"quo\\\\ted'); + expect(unquoted).toBe('un\\"quo\\\\ted'); + }); +}); diff --git a/public/app/features/alerting/unified/utils/matchers.ts b/public/app/features/alerting/unified/utils/matchers.ts index aa1b7187f85..7b8c7ef4a05 100644 --- a/public/app/features/alerting/unified/utils/matchers.ts +++ b/public/app/features/alerting/unified/utils/matchers.ts @@ -108,4 +108,41 @@ export const normalizeMatchers = (route: Route): ObjectMatcher[] => { return matchers; }; +/** + * Quotes string and escapes double quote and backslash characters + */ +export function quoteWithEscape(input: string) { + const escaped = input.replace(/[\\"]/g, (c) => `\\${c}`); + return `"${escaped}"`; +} + +/** + * Unquotes and unescapes a string **if it has been quoted** + */ +export function unquoteWithUnescape(input: string) { + if (!/^"(.*)"$/.test(input)) { + return input; + } + + return input + .replace(/^"(.*)"$/, '$1') + .replace(/\\"/g, '"') + .replace(/\\\\/g, '\\'); +} + +export const matcherFormatter = { + default: ([name, operator, value]: ObjectMatcher): string => { + // Value can be an empty string which we want to display as "" + const formattedValue = value || ''; + return `${name} ${operator} ${formattedValue}`; + }, + unquote: ([name, operator, value]: ObjectMatcher): string => { + // Unquoted value can be an empty string which we want to display as "" + const unquotedValue = unquoteWithUnescape(value) || '""'; + return `${name} ${operator} ${unquotedValue}`; + }, +} as const; + +export type MatcherFormatter = keyof typeof matcherFormatter; + export type Label = [string, string]; diff --git a/public/app/features/alerting/unified/utils/notification-policies.ts b/public/app/features/alerting/unified/utils/notification-policies.ts index d25de2728fa..a0fed72bd66 100644 --- a/public/app/features/alerting/unified/utils/notification-policies.ts +++ b/public/app/features/alerting/unified/utils/notification-policies.ts @@ -9,7 +9,7 @@ import { } from 'app/plugins/datasource/alertmanager/types'; import { Labels } from 'app/types/unified-alerting-dto'; -import { Label, normalizeMatchers } from './matchers'; +import { Label, normalizeMatchers, unquoteWithUnescape } from './matchers'; // If a policy has no matchers it still can be a match, hence matchers can be empty and match can be true // So we cannot use null as an indicator of no match @@ -124,6 +124,20 @@ export function normalizeRoute(rootRoute: RouteWithID): RouteWithID { return normalizedRootRoute; } +export function unquoteRouteMatchers(route: RouteWithID): RouteWithID { + function unquoteRoute(route: RouteWithID) { + route.object_matchers = route.object_matchers?.map(([name, operator, value]) => { + return [name, operator, unquoteWithUnescape(value)]; + }); + route.routes?.forEach(unquoteRoute); + } + + const unwrappedRootRoute = structuredClone(route); + unquoteRoute(unwrappedRootRoute); + + return unwrappedRootRoute; +} + /** * find all of the groups that have instances that match the route, thay way we can find all instances * (and their grouping) for the given route From 32a1f3955a92e01b54dbb9a623b333f2cf1414f7 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Fri, 9 Feb 2024 09:09:34 -0600 Subject: [PATCH 18/50] Canvas: Keep tooltip open until dismissed (#82213) --- public/app/features/canvas/runtime/element.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/canvas/runtime/element.tsx b/public/app/features/canvas/runtime/element.tsx index 03f5214916d..fea795bb618 100644 --- a/public/app/features/canvas/runtime/element.tsx +++ b/public/app/features/canvas/runtime/element.tsx @@ -459,7 +459,7 @@ export class ElementState implements LayerElement { handleMouseEnter = (event: React.MouseEvent, isSelected: boolean | undefined) => { const scene = this.getScene(); - if (!scene?.isEditingEnabled) { + if (!scene?.isEditingEnabled && !scene?.tooltip?.isOpen) { this.handleTooltip(event); } else if (!isSelected) { scene?.connections.handleMouseEnter(event); From 6f62d970e39916e8ec9960da6a31d40570f440b3 Mon Sep 17 00:00:00 2001 From: Jo Date: Fri, 9 Feb 2024 16:35:58 +0100 Subject: [PATCH 19/50] JWT Authentication: Add support for specifying groups in auth.jwt for teamsync (#82175) * merge JSON search logic * document public methods * improve test coverage * use separate JWT setting struct * correct use of cfg.JWTAuth * add group tests * fix DynMap typing * add settings to default ini * add groups option to devenv path * fix test * lint * revert jwt-proxy change * remove redundant check * fix parallel test --- conf/defaults.ini | 1 + conf/sample.ini | 2 +- devenv/docker/blocks/auth/jwt_proxy/readme.md | 1 + pkg/api/admin_users_test.go | 6 +- pkg/api/frontendsettings.go | 6 +- pkg/api/user_test.go | 6 +- pkg/login/social/connectors/common.go | 60 ---- pkg/login/social/connectors/generic_oauth.go | 10 +- .../social/connectors/generic_oauth_test.go | 202 ------------ pkg/login/social/connectors/social_base.go | 5 +- pkg/services/auth/jwt/auth.go | 2 +- pkg/services/auth/jwt/auth_test.go | 34 +- pkg/services/auth/jwt/jwt.go | 4 +- pkg/services/auth/jwt/key_sets.go | 16 +- pkg/services/auth/jwt/validation.go | 2 +- pkg/services/authn/authnimpl/service.go | 2 +- pkg/services/authn/authnimpl/usage_stats.go | 2 +- .../authn/authnimpl/usage_stats_test.go | 2 +- pkg/services/authn/clients/jwt.go | 68 ++-- pkg/services/authn/clients/jwt_test.go | 306 ++++++++++++------ pkg/services/contexthandler/contexthandler.go | 4 +- .../contexthandler/contexthandler_test.go | 4 +- pkg/services/login/authinfo.go | 6 +- pkg/services/login/authinfo_test.go | 9 +- pkg/setting/setting.go | 39 +-- pkg/setting/setting_jwt.go | 48 +++ pkg/util/json.go | 108 +++++++ pkg/util/json_test.go | 155 +++++++++ 28 files changed, 601 insertions(+), 509 deletions(-) create mode 100644 pkg/setting/setting_jwt.go create mode 100644 pkg/util/json_test.go diff --git a/conf/defaults.ini b/conf/defaults.ini index 1ff2d4612ee..c15a21eb6e6 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -840,6 +840,7 @@ key_file = key_id = role_attribute_path = role_attribute_strict = false +groups_attribute_path = auto_sign_up = false url_login = false allow_assign_grafana_admin = false diff --git a/conf/sample.ini b/conf/sample.ini index 66631fd8c6a..17ad690d76e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -774,6 +774,7 @@ # Use in conjunction with key_file in case the JWT token's header specifies a key ID in "kid" field ;key_id = some-key-id ;role_attribute_path = +;groups_attribute_path = ;role_attribute_strict = false ;auto_sign_up = false ;url_login = false @@ -1639,4 +1640,3 @@ [public_dashboards] # Set to false to disable public dashboards ;enabled = true - diff --git a/devenv/docker/blocks/auth/jwt_proxy/readme.md b/devenv/docker/blocks/auth/jwt_proxy/readme.md index f3e32d147c4..823287f54e3 100644 --- a/devenv/docker/blocks/auth/jwt_proxy/readme.md +++ b/devenv/docker/blocks/auth/jwt_proxy/readme.md @@ -24,6 +24,7 @@ expect_claims = {"iss": "http://env.grafana.local:8087/realms/grafana", "azp": " auto_sign_up = true role_attribute_path = contains(roles[*], 'grafanaadmin') && 'GrafanaAdmin' || contains(roles[*], 'admin') && 'Admin' || contains(roles[*], 'editor') && 'Editor' || 'Viewer' role_attribute_strict = false +groups_attribute_path = groups[] allow_assign_grafana_admin = true ``` diff --git a/pkg/api/admin_users_test.go b/pkg/api/admin_users_test.go index c449854c322..d40417f72f3 100644 --- a/pkg/api/admin_users_test.go +++ b/pkg/api/admin_users_test.go @@ -320,9 +320,9 @@ func Test_AdminUpdateUserPermissions(t *testing.T) { case login.GenericOAuthModule: socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{AllowAssignGrafanaAdmin: tc.allowAssignGrafanaAdmin, Enabled: tc.authEnabled, SkipOrgRoleSync: tc.skipOrgRoleSync} case login.JWTModule: - cfg.JWTAuthEnabled = tc.authEnabled - cfg.JWTAuthSkipOrgRoleSync = tc.skipOrgRoleSync - cfg.JWTAuthAllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin + cfg.JWTAuth.Enabled = tc.authEnabled + cfg.JWTAuth.SkipOrgRoleSync = tc.skipOrgRoleSync + cfg.JWTAuth.AllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin } hs := &HTTPServer{ diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index ce055ded32b..3016bd44c1e 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -173,8 +173,8 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro AllowOrgCreate: (hs.Cfg.AllowUserOrgCreate && c.IsSignedIn) || c.IsGrafanaAdmin, AuthProxyEnabled: hs.Cfg.AuthProxyEnabled, LdapEnabled: hs.Cfg.LDAPAuthEnabled, - JwtHeaderName: hs.Cfg.JWTAuthHeaderName, - JwtUrlLogin: hs.Cfg.JWTAuthURLLogin, + JwtHeaderName: hs.Cfg.JWTAuth.HeaderName, + JwtUrlLogin: hs.Cfg.JWTAuth.URLLogin, AlertingErrorOrTimeout: hs.Cfg.AlertingErrorOrTimeout, AlertingNoDataOrNullValues: hs.Cfg.AlertingNoDataOrNullValues, AlertingMinInterval: hs.Cfg.AlertingMinInterval, @@ -321,7 +321,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro OAuthSkipOrgRoleUpdateSync: hs.Cfg.OAuthSkipOrgRoleUpdateSync, SAMLSkipOrgRoleSync: hs.Cfg.SAMLSkipOrgRoleSync, LDAPSkipOrgRoleSync: hs.Cfg.LDAPSkipOrgRoleSync, - JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuthSkipOrgRoleSync, + JWTAuthSkipOrgRoleSync: hs.Cfg.JWTAuth.SkipOrgRoleSync, GoogleSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GoogleProviderName]), GrafanaComSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GrafanaComProviderName]), GenericOAuthSkipOrgRoleSync: parseSkipOrgRoleSyncEnabled(oauthProviders[social.GenericOAuthProviderName]), diff --git a/pkg/api/user_test.go b/pkg/api/user_test.go index 964eb23da87..e8b96c5b2fa 100644 --- a/pkg/api/user_test.go +++ b/pkg/api/user_test.go @@ -302,9 +302,9 @@ func Test_GetUserByID(t *testing.T) { case login.GenericOAuthModule: socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{AllowAssignGrafanaAdmin: tc.allowAssignGrafanaAdmin, Enabled: tc.authEnabled, SkipOrgRoleSync: tc.skipOrgRoleSync} case login.JWTModule: - cfg.JWTAuthEnabled = tc.authEnabled - cfg.JWTAuthSkipOrgRoleSync = tc.skipOrgRoleSync - cfg.JWTAuthAllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin + cfg.JWTAuth.Enabled = tc.authEnabled + cfg.JWTAuth.SkipOrgRoleSync = tc.skipOrgRoleSync + cfg.JWTAuth.AllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin } hs := &HTTPServer{ diff --git a/pkg/login/social/connectors/common.go b/pkg/login/social/connectors/common.go index 93517fed981..96bf84020ba 100644 --- a/pkg/login/social/connectors/common.go +++ b/pkg/login/social/connectors/common.go @@ -2,8 +2,6 @@ package connectors import ( "context" - "encoding/json" - "errors" "fmt" "io" "net/http" @@ -12,7 +10,6 @@ import ( "strconv" "strings" - "github.com/jmespath/go-jmespath" "github.com/mitchellh/mapstructure" "golang.org/x/oauth2" @@ -96,63 +93,6 @@ func (s *SocialBase) httpGet(ctx context.Context, client *http.Client, url strin return response, nil } -func (s *SocialBase) searchJSONForAttr(attributePath string, data []byte) (any, error) { - if attributePath == "" { - return "", errors.New("no attribute path specified") - } - - if len(data) == 0 { - return "", errors.New("empty user info JSON response provided") - } - - var buf any - if err := json.Unmarshal(data, &buf); err != nil { - return "", fmt.Errorf("%v: %w", "failed to unmarshal user info JSON response", err) - } - - val, err := jmespath.Search(attributePath, buf) - if err != nil { - return "", fmt.Errorf("failed to search user info JSON response with provided path: %q: %w", attributePath, err) - } - - return val, nil -} - -func (s *SocialBase) searchJSONForStringAttr(attributePath string, data []byte) (string, error) { - val, err := s.searchJSONForAttr(attributePath, data) - if err != nil { - return "", err - } - - strVal, ok := val.(string) - if ok { - return strVal, nil - } - - return "", nil -} - -func (s *SocialBase) searchJSONForStringArrayAttr(attributePath string, data []byte) ([]string, error) { - val, err := s.searchJSONForAttr(attributePath, data) - if err != nil { - return []string{}, err - } - - ifArr, ok := val.([]any) - if !ok { - return []string{}, nil - } - - result := []string{} - for _, v := range ifArr { - if strVal, ok := v.(string); ok { - result = append(result, strVal) - } - } - - return result, nil -} - func createOAuthConfig(info *social.OAuthInfo, cfg *setting.Cfg, defaultName string) *oauth2.Config { var authStyle oauth2.AuthStyle switch strings.ToLower(info.AuthStyle) { diff --git a/pkg/login/social/connectors/generic_oauth.go b/pkg/login/social/connectors/generic_oauth.go index a001aef73d4..a8ddd13fd2a 100644 --- a/pkg/login/social/connectors/generic_oauth.go +++ b/pkg/login/social/connectors/generic_oauth.go @@ -363,7 +363,7 @@ func (s *SocialGenericOAuth) extractEmail(data *UserInfoJson) string { } if s.emailAttributePath != "" { - email, err := s.searchJSONForStringAttr(s.emailAttributePath, data.rawJSON) + email, err := util.SearchJSONForStringAttr(s.emailAttributePath, data.rawJSON) if err != nil { s.log.Error("Failed to search JSON for attribute", "error", err) } else if email != "" { @@ -395,7 +395,7 @@ func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string { if s.loginAttributePath != "" { s.log.Debug("Searching for login among JSON", "loginAttributePath", s.loginAttributePath) - login, err := s.searchJSONForStringAttr(s.loginAttributePath, data.rawJSON) + login, err := util.SearchJSONForStringAttr(s.loginAttributePath, data.rawJSON) if err != nil { s.log.Error("Failed to search JSON for login attribute", "error", err) } @@ -415,7 +415,7 @@ func (s *SocialGenericOAuth) extractLogin(data *UserInfoJson) string { func (s *SocialGenericOAuth) extractUserName(data *UserInfoJson) string { if s.nameAttributePath != "" { - name, err := s.searchJSONForStringAttr(s.nameAttributePath, data.rawJSON) + name, err := util.SearchJSONForStringAttr(s.nameAttributePath, data.rawJSON) if err != nil { s.log.Error("Failed to search JSON for attribute", "error", err) } else if name != "" { @@ -443,7 +443,7 @@ func (s *SocialGenericOAuth) extractGroups(data *UserInfoJson) ([]string, error) return []string{}, nil } - return s.searchJSONForStringArrayAttr(s.groupsAttributePath, data.rawJSON) + return util.SearchJSONForStringSliceAttr(s.groupsAttributePath, data.rawJSON) } func (s *SocialGenericOAuth) FetchPrivateEmail(ctx context.Context, client *http.Client) (string, error) { @@ -554,7 +554,7 @@ func (s *SocialGenericOAuth) fetchTeamMembershipsFromTeamsUrl(ctx context.Contex return nil, err } - return s.searchJSONForStringArrayAttr(s.teamIdsAttributePath, response.Body) + return util.SearchJSONForStringSliceAttr(s.teamIdsAttributePath, response.Body) } func (s *SocialGenericOAuth) FetchOrganizations(ctx context.Context, client *http.Client) ([]string, bool) { diff --git a/pkg/login/social/connectors/generic_oauth_test.go b/pkg/login/social/connectors/generic_oauth_test.go index bf991f18712..3812a2d43bd 100644 --- a/pkg/login/social/connectors/generic_oauth_test.go +++ b/pkg/login/social/connectors/generic_oauth_test.go @@ -23,208 +23,6 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func TestSearchJSONForEmail(t *testing.T) { - t.Run("Given a generic OAuth provider", func(t *testing.T) { - provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - - tests := []struct { - Name string - UserInfoJSONResponse []byte - EmailAttributePath string - ExpectedResult string - ExpectedError string - }{ - { - Name: "Given an invalid user info JSON response", - UserInfoJSONResponse: []byte("{"), - EmailAttributePath: "attributes.email", - ExpectedResult: "", - ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input", - }, - { - Name: "Given an empty user info JSON response and empty JMES path", - UserInfoJSONResponse: []byte{}, - EmailAttributePath: "", - ExpectedResult: "", - ExpectedError: "no attribute path specified", - }, - { - Name: "Given an empty user info JSON response and valid JMES path", - UserInfoJSONResponse: []byte{}, - EmailAttributePath: "attributes.email", - ExpectedResult: "", - ExpectedError: "empty user info JSON response provided", - }, - { - Name: "Given a simple user info JSON response and valid JMES path", - UserInfoJSONResponse: []byte(`{ - "attributes": { - "email": "grafana@localhost" - } -}`), - EmailAttributePath: "attributes.email", - ExpectedResult: "grafana@localhost", - }, - { - Name: "Given a user info JSON response with e-mails array and valid JMES path", - UserInfoJSONResponse: []byte(`{ - "attributes": { - "emails": ["grafana@localhost", "admin@localhost"] - } -}`), - EmailAttributePath: "attributes.emails[0]", - ExpectedResult: "grafana@localhost", - }, - { - Name: "Given a nested user info JSON response and valid JMES path", - UserInfoJSONResponse: []byte(`{ - "identities": [ - { - "userId": "grafana@localhost" - }, - { - "userId": "admin@localhost" - } - ] -}`), - EmailAttributePath: "identities[0].userId", - ExpectedResult: "grafana@localhost", - }, - } - - for _, test := range tests { - provider.emailAttributePath = test.EmailAttributePath - t.Run(test.Name, func(t *testing.T) { - actualResult, err := provider.searchJSONForStringAttr(test.EmailAttributePath, test.UserInfoJSONResponse) - if test.ExpectedError == "" { - require.NoError(t, err, "Testing case %q", test.Name) - } else { - require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name) - } - require.Equal(t, test.ExpectedResult, actualResult) - }) - } - }) -} - -func TestSearchJSONForGroups(t *testing.T) { - t.Run("Given a generic OAuth provider", func(t *testing.T) { - provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - - tests := []struct { - Name string - UserInfoJSONResponse []byte - GroupsAttributePath string - ExpectedResult []string - ExpectedError string - }{ - { - Name: "Given an invalid user info JSON response", - UserInfoJSONResponse: []byte("{"), - GroupsAttributePath: "attributes.groups", - ExpectedResult: []string{}, - ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input", - }, - { - Name: "Given an empty user info JSON response and empty JMES path", - UserInfoJSONResponse: []byte{}, - GroupsAttributePath: "", - ExpectedResult: []string{}, - ExpectedError: "no attribute path specified", - }, - { - Name: "Given an empty user info JSON response and valid JMES path", - UserInfoJSONResponse: []byte{}, - GroupsAttributePath: "attributes.groups", - ExpectedResult: []string{}, - ExpectedError: "empty user info JSON response provided", - }, - { - Name: "Given a simple user info JSON response and valid JMES path", - UserInfoJSONResponse: []byte(`{ - "attributes": { - "groups": ["foo", "bar"] - } -}`), - GroupsAttributePath: "attributes.groups[]", - ExpectedResult: []string{"foo", "bar"}, - }, - } - - for _, test := range tests { - provider.groupsAttributePath = test.GroupsAttributePath - t.Run(test.Name, func(t *testing.T) { - actualResult, err := provider.searchJSONForStringArrayAttr(test.GroupsAttributePath, test.UserInfoJSONResponse) - if test.ExpectedError == "" { - require.NoError(t, err, "Testing case %q", test.Name) - } else { - require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name) - } - require.Equal(t, test.ExpectedResult, actualResult) - }) - } - }) -} - -func TestSearchJSONForRole(t *testing.T) { - t.Run("Given a generic OAuth provider", func(t *testing.T) { - provider := NewGenericOAuthProvider(social.NewOAuthInfo(), &setting.Cfg{}, &ssosettingstests.MockService{}, featuremgmt.WithFeatures()) - - tests := []struct { - Name string - UserInfoJSONResponse []byte - RoleAttributePath string - ExpectedResult string - ExpectedError string - }{ - { - Name: "Given an invalid user info JSON response", - UserInfoJSONResponse: []byte("{"), - RoleAttributePath: "attributes.role", - ExpectedResult: "", - ExpectedError: "failed to unmarshal user info JSON response: unexpected end of JSON input", - }, - { - Name: "Given an empty user info JSON response and empty JMES path", - UserInfoJSONResponse: []byte{}, - RoleAttributePath: "", - ExpectedResult: "", - ExpectedError: "no attribute path specified", - }, - { - Name: "Given an empty user info JSON response and valid JMES path", - UserInfoJSONResponse: []byte{}, - RoleAttributePath: "attributes.role", - ExpectedResult: "", - ExpectedError: "empty user info JSON response provided", - }, - { - Name: "Given a simple user info JSON response and valid JMES path", - UserInfoJSONResponse: []byte(`{ - "attributes": { - "role": "admin" - } -}`), - RoleAttributePath: "attributes.role", - ExpectedResult: "admin", - }, - } - - for _, test := range tests { - provider.info.RoleAttributePath = test.RoleAttributePath - t.Run(test.Name, func(t *testing.T) { - actualResult, err := provider.searchJSONForStringAttr(test.RoleAttributePath, test.UserInfoJSONResponse) - if test.ExpectedError == "" { - require.NoError(t, err, "Testing case %q", test.Name) - } else { - require.EqualError(t, err, test.ExpectedError, "Testing case %q", test.Name) - } - require.Equal(t, test.ExpectedResult, actualResult) - }) - } - }) -} - func TestUserInfoSearchesForEmailAndRole(t *testing.T) { provider := NewGenericOAuthProvider(&social.OAuthInfo{ EmailAttributePath: "email", diff --git a/pkg/login/social/connectors/social_base.go b/pkg/login/social/connectors/social_base.go index b57d3c0dd5e..afe64097c4e 100644 --- a/pkg/login/social/connectors/social_base.go +++ b/pkg/login/social/connectors/social_base.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) type SocialBase struct { @@ -112,13 +113,13 @@ func (s *SocialBase) extractRoleAndAdmin(rawJSON []byte, groups []string) (org.R } func (s *SocialBase) searchRole(rawJSON []byte, groups []string) (org.RoleType, bool) { - role, err := s.searchJSONForStringAttr(s.info.RoleAttributePath, rawJSON) + role, err := util.SearchJSONForStringAttr(s.info.RoleAttributePath, rawJSON) if err == nil && role != "" { return getRoleFromSearch(role) } if groupBytes, err := json.Marshal(groupStruct{groups}); err == nil { - role, err := s.searchJSONForStringAttr(s.info.RoleAttributePath, groupBytes) + role, err := util.SearchJSONForStringAttr(s.info.RoleAttributePath, groupBytes) if err == nil && role != "" { return getRoleFromSearch(role) } diff --git a/pkg/services/auth/jwt/auth.go b/pkg/services/auth/jwt/auth.go index c2d317a056e..37e0a7fa373 100644 --- a/pkg/services/auth/jwt/auth.go +++ b/pkg/services/auth/jwt/auth.go @@ -33,7 +33,7 @@ func newService(cfg *setting.Cfg, remoteCache *remotecache.RemoteCache) *AuthSer } func (s *AuthService) init() error { - if !s.Cfg.JWTAuthEnabled { + if !s.Cfg.JWTAuth.Enabled { return nil } diff --git a/pkg/services/auth/jwt/auth_test.go b/pkg/services/auth/jwt/auth_test.go index fee5ac05b25..73d2106b72d 100644 --- a/pkg/services/auth/jwt/auth_test.go +++ b/pkg/services/auth/jwt/auth_test.go @@ -75,7 +75,7 @@ func TestVerifyUsingPKIXPublicKeyFile(t *testing.T) { assert.Equal(t, verifiedClaims["sub"], subject) }, configurePKIXPublicKeyFile, func(t *testing.T, cfg *setting.Cfg) { t.Helper() - cfg.JWTAuthKeyID = publicKeyID + cfg.JWTAuth.KeyID = publicKeyID }) } @@ -94,7 +94,7 @@ func TestVerifyUsingJWKSetFile(t *testing.T) { require.NoError(t, json.NewEncoder(file).Encode(jwksPublic)) require.NoError(t, file.Close()) - cfg.JWTAuthJWKSetFile = file.Name() + cfg.JWTAuth.JWKSetFile = file.Name() } scenario(t, "verifies a token signed with a key from the set", func(t *testing.T, sc scenarioContext) { @@ -123,18 +123,18 @@ func TestVerifyUsingJWKSetURL(t *testing.T) { var err error _, err = initAuthService(t, func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthJWKSetURL = "https://example.com/.well-known/jwks.json" + cfg.JWTAuth.JWKSetURL = "https://example.com/.well-known/jwks.json" }) require.NoError(t, err) _, err = initAuthService(t, func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthJWKSetURL = "http://example.com/.well-known/jwks.json" + cfg.JWTAuth.JWKSetURL = "http://example.com/.well-known/jwks.json" }) require.NoError(t, err) _, err = initAuthService(t, func(t *testing.T, cfg *setting.Cfg) { cfg.Env = setting.Prod - cfg.JWTAuthJWKSetURL = "http://example.com/.well-known/jwks.json" + cfg.JWTAuth.JWKSetURL = "http://example.com/.well-known/jwks.json" }) require.Error(t, err) }) @@ -185,7 +185,7 @@ func TestCachingJWKHTTPResponse(t *testing.T) { assert.Equal(t, 1, *sc.reqCount) }, func(t *testing.T, cfg *setting.Cfg) { // Arbitrary high value, several times what the test should take. - cfg.JWTAuthCacheTTL = time.Minute + cfg.JWTAuth.CacheTTL = time.Minute }) jwkCachingScenario(t, "does not cache the response when TTL is zero", func(t *testing.T, sc cachingScenarioContext) { @@ -196,7 +196,7 @@ func TestCachingJWKHTTPResponse(t *testing.T) { assert.Equal(t, 2, *sc.reqCount) }, func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthCacheTTL = 0 + cfg.JWTAuth.CacheTTL = 0 }) } @@ -221,7 +221,7 @@ func TestClaimValidation(t *testing.T) { _, err = sc.authJWTSvc.Verify(sc.ctx, tokenInvalid) require.Error(t, err) }, configurePKIXPublicKeyFile, func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthExpectClaims = `{"iss": "http://foo"}` + cfg.JWTAuth.ExpectClaims = `{"iss": "http://foo"}` }) scenario(t, "validates sub field for equality", func(t *testing.T, sc scenarioContext) { @@ -236,7 +236,7 @@ func TestClaimValidation(t *testing.T) { _, err = sc.authJWTSvc.Verify(sc.ctx, tokenInvalid) require.Error(t, err) }, configurePKIXPublicKeyFile, func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthExpectClaims = `{"sub": "foo"}` + cfg.JWTAuth.ExpectClaims = `{"sub": "foo"}` }) scenario(t, "validates aud field for inclusion", func(t *testing.T, sc scenarioContext) { @@ -257,7 +257,7 @@ func TestClaimValidation(t *testing.T) { _, err = sc.authJWTSvc.Verify(sc.ctx, sign(t, key, jwt.Claims{Audience: []string{"baz"}}, nil)) require.Error(t, err) }, configurePKIXPublicKeyFile, func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthExpectClaims = `{"aud": ["foo", "bar"]}` + cfg.JWTAuth.ExpectClaims = `{"aud": ["foo", "bar"]}` }) scenario(t, "validates non-registered (custom) claims for equality", func(t *testing.T, sc scenarioContext) { @@ -278,7 +278,7 @@ func TestClaimValidation(t *testing.T) { _, err = sc.authJWTSvc.Verify(sc.ctx, sign(t, key, map[string]any{"my-number": 123}, nil)) require.Error(t, err) }, configurePKIXPublicKeyFile, func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthExpectClaims = `{"my-str": "foo", "my-number": 123}` + cfg.JWTAuth.ExpectClaims = `{"my-str": "foo", "my-number": 123}` }) scenario(t, "validates exp claim of the token", func(t *testing.T, sc scenarioContext) { @@ -323,7 +323,7 @@ func jwkHTTPScenario(t *testing.T, desc string, fn scenarioFunc, cbs ...configur t.Cleanup(ts.Close) configure := func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthJWKSetURL = ts.URL + cfg.JWTAuth.JWKSetURL = ts.URL } runner := scenarioRunner(func(t *testing.T, sc scenarioContext) { keySet := sc.authJWTSvc.keySet.(*keySetHTTP) @@ -355,8 +355,8 @@ func jwkCachingScenario(t *testing.T, desc string, fn cachingScenarioFunc, cbs . t.Cleanup(ts.Close) configure := func(t *testing.T, cfg *setting.Cfg) { - cfg.JWTAuthJWKSetURL = ts.URL - cfg.JWTAuthCacheTTL = time.Hour + cfg.JWTAuth.JWKSetURL = ts.URL + cfg.JWTAuth.CacheTTL = time.Hour } runner := scenarioRunner(func(t *testing.T, sc scenarioContext) { keySet := sc.authJWTSvc.keySet.(*keySetHTTP) @@ -397,8 +397,8 @@ func initAuthService(t *testing.T, cbs ...configureFunc) (*AuthService, error) { t.Helper() cfg := setting.NewCfg() - cfg.JWTAuthEnabled = true - cfg.JWTAuthExpectClaims = "{}" + cfg.JWTAuth.Enabled = true + cfg.JWTAuth.ExpectClaims = "{}" for _, cb := range cbs { cb(t, cfg) @@ -442,5 +442,5 @@ func configurePKIXPublicKeyFile(t *testing.T, cfg *setting.Cfg) { })) require.NoError(t, file.Close()) - cfg.JWTAuthKeyFile = file.Name() + cfg.JWTAuth.KeyFile = file.Name() } diff --git a/pkg/services/auth/jwt/jwt.go b/pkg/services/auth/jwt/jwt.go index c0c7ddd4292..c1175b5c72d 100644 --- a/pkg/services/auth/jwt/jwt.go +++ b/pkg/services/auth/jwt/jwt.go @@ -2,9 +2,11 @@ package jwt import ( "context" + + "github.com/grafana/grafana/pkg/util" ) -type JWTClaims map[string]any +type JWTClaims util.DynMap type JWTService interface { Verify(ctx context.Context, strToken string) (JWTClaims, error) diff --git a/pkg/services/auth/jwt/key_sets.go b/pkg/services/auth/jwt/key_sets.go index f981b21a591..5361f3b3fbb 100644 --- a/pkg/services/auth/jwt/key_sets.go +++ b/pkg/services/auth/jwt/key_sets.go @@ -49,13 +49,13 @@ type keySetHTTP struct { func (s *AuthService) checkKeySetConfiguration() error { var count int - if s.Cfg.JWTAuthKeyFile != "" { + if s.Cfg.JWTAuth.KeyFile != "" { count++ } - if s.Cfg.JWTAuthJWKSetFile != "" { + if s.Cfg.JWTAuth.JWKSetFile != "" { count++ } - if s.Cfg.JWTAuthJWKSetURL != "" { + if s.Cfg.JWTAuth.JWKSetURL != "" { count++ } @@ -75,7 +75,7 @@ func (s *AuthService) initKeySet() error { return err } - if keyFilePath := s.Cfg.JWTAuthKeyFile; keyFilePath != "" { + if keyFilePath := s.Cfg.JWTAuth.KeyFile; keyFilePath != "" { // nolint:gosec // We can ignore the gosec G304 warning on this one because `fileName` comes from grafana configuration file file, err := os.Open(keyFilePath) @@ -125,10 +125,10 @@ func (s *AuthService) initKeySet() error { s.keySet = &keySetJWKS{ jose.JSONWebKeySet{ - Keys: []jose.JSONWebKey{{Key: key, KeyID: s.Cfg.JWTAuthKeyID}}, + Keys: []jose.JSONWebKey{{Key: key, KeyID: s.Cfg.JWTAuth.KeyID}}, }, } - } else if keyFilePath := s.Cfg.JWTAuthJWKSetFile; keyFilePath != "" { + } else if keyFilePath := s.Cfg.JWTAuth.JWKSetFile; keyFilePath != "" { // nolint:gosec // We can ignore the gosec G304 warning on this one because `fileName` comes from grafana configuration file file, err := os.Open(keyFilePath) @@ -147,7 +147,7 @@ func (s *AuthService) initKeySet() error { } s.keySet = &keySetJWKS{jwks} - } else if urlStr := s.Cfg.JWTAuthJWKSetURL; urlStr != "" { + } else if urlStr := s.Cfg.JWTAuth.JWKSetURL; urlStr != "" { urlParsed, err := url.Parse(urlStr) if err != nil { return err @@ -176,7 +176,7 @@ func (s *AuthService) initKeySet() error { Timeout: time.Second * 30, }, cacheKey: fmt.Sprintf("auth-jwt:jwk-%s", urlStr), - cacheExpiration: s.Cfg.JWTAuthCacheTTL, + cacheExpiration: s.Cfg.JWTAuth.CacheTTL, cache: s.RemoteCache, } } diff --git a/pkg/services/auth/jwt/validation.go b/pkg/services/auth/jwt/validation.go index 260aa964f29..4fe5b88119f 100644 --- a/pkg/services/auth/jwt/validation.go +++ b/pkg/services/auth/jwt/validation.go @@ -10,7 +10,7 @@ import ( ) func (s *AuthService) initClaimExpectations() error { - if err := json.Unmarshal([]byte(s.Cfg.JWTAuthExpectClaims), &s.expect); err != nil { + if err := json.Unmarshal([]byte(s.Cfg.JWTAuth.ExpectClaims), &s.expect); err != nil { return err } diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index a6bc4d451cd..fe65c436739 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -131,7 +131,7 @@ func ProvideService( } } - if s.cfg.JWTAuthEnabled { + if s.cfg.JWTAuth.Enabled { s.RegisterClient(clients.ProvideJWT(jwtService, cfg)) } diff --git a/pkg/services/authn/authnimpl/usage_stats.go b/pkg/services/authn/authnimpl/usage_stats.go index e2b15cf1ab4..b6ea1a95c6d 100644 --- a/pkg/services/authn/authnimpl/usage_stats.go +++ b/pkg/services/authn/authnimpl/usage_stats.go @@ -15,7 +15,7 @@ func (s *Service) getUsageStats(ctx context.Context) (map[string]any, error) { authTypes["ldap"] = s.cfg.LDAPAuthEnabled authTypes["auth_proxy"] = s.cfg.AuthProxyEnabled authTypes["anonymous"] = s.cfg.AnonymousEnabled - authTypes["jwt"] = s.cfg.JWTAuthEnabled + authTypes["jwt"] = s.cfg.JWTAuth.Enabled authTypes["grafana_password"] = !s.cfg.DisableLogin authTypes["login_form"] = !s.cfg.DisableLoginForm diff --git a/pkg/services/authn/authnimpl/usage_stats_test.go b/pkg/services/authn/authnimpl/usage_stats_test.go index ba59c1f1818..cded65c117e 100644 --- a/pkg/services/authn/authnimpl/usage_stats_test.go +++ b/pkg/services/authn/authnimpl/usage_stats_test.go @@ -21,7 +21,7 @@ func TestService_getUsageStats(t *testing.T) { svc.cfg.DisableLogin = false svc.cfg.BasicAuthEnabled = true svc.cfg.AuthProxyEnabled = true - svc.cfg.JWTAuthEnabled = true + svc.cfg.JWTAuth.Enabled = true svc.cfg.LDAPAuthEnabled = true svc.cfg.EditorsCanAdmin = true svc.cfg.ViewersCanEdit = true diff --git a/pkg/services/authn/clients/jwt.go b/pkg/services/authn/clients/jwt.go index 9c628906baf..e789f9153a0 100644 --- a/pkg/services/authn/clients/jwt.go +++ b/pkg/services/authn/clients/jwt.go @@ -2,13 +2,9 @@ package clients import ( "context" - "errors" - "fmt" "net/http" "strings" - "github.com/jmespath/go-jmespath" - "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/auth" authJWT "github.com/grafana/grafana/pkg/services/auth/jwt" @@ -16,6 +12,7 @@ import ( "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util/errutil" ) @@ -73,15 +70,16 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi SyncUser: true, FetchSyncedUser: true, SyncPermissions: true, - SyncOrgRoles: !s.cfg.JWTAuthSkipOrgRoleSync, - AllowSignUp: s.cfg.JWTAuthAutoSignUp, + SyncOrgRoles: !s.cfg.JWTAuth.SkipOrgRoleSync, + AllowSignUp: s.cfg.JWTAuth.AutoSignUp, + SyncTeams: s.cfg.JWTAuth.GroupsAttributePath != "", }} - if key := s.cfg.JWTAuthUsernameClaim; key != "" { + if key := s.cfg.JWTAuth.UsernameClaim; key != "" { id.Login, _ = claims[key].(string) id.ClientParams.LookUpParams.Login = &id.Login } - if key := s.cfg.JWTAuthEmailClaim; key != "" { + if key := s.cfg.JWTAuth.EmailClaim; key != "" { id.Email, _ = claims[key].(string) id.ClientParams.LookUpParams.Email = &id.Email } @@ -91,16 +89,16 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi } orgRoles, isGrafanaAdmin, err := getRoles(s.cfg, func() (org.RoleType, *bool, error) { - if s.cfg.JWTAuthSkipOrgRoleSync { + if s.cfg.JWTAuth.SkipOrgRoleSync { return "", nil, nil } role, grafanaAdmin := s.extractRoleAndAdmin(claims) - if s.cfg.JWTAuthRoleAttributeStrict && !role.IsValid() { + if s.cfg.JWTAuth.RoleAttributeStrict && !role.IsValid() { return "", nil, errJWTInvalidRole.Errorf("invalid role claim in JWT: %s", role) } - if !s.cfg.JWTAuthAllowAssignGrafanaAdmin { + if !s.cfg.JWTAuth.AllowAssignGrafanaAdmin { return role, nil, nil } @@ -114,6 +112,11 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi id.OrgRoles = orgRoles id.IsGrafanaAdmin = isGrafanaAdmin + id.Groups, err = s.extractGroups(claims) + if err != nil { + return nil, err + } + if id.Login == "" && id.Email == "" { s.log.FromContext(ctx).Debug("Failed to get an authentication claim from JWT", "login", id.Login, "email", id.Email) @@ -126,7 +129,7 @@ func (s *JWT) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identi // remove sensitive query param // avoid JWT URL login passing auth_token in URL func (s *JWT) stripSensitiveParam(httpRequest *http.Request) { - if s.cfg.JWTAuthURLLogin { + if s.cfg.JWTAuth.URLLogin { params := httpRequest.URL.Query() if params.Has(authQueryParamName) { params.Del(authQueryParamName) @@ -137,8 +140,8 @@ func (s *JWT) stripSensitiveParam(httpRequest *http.Request) { // retrieveToken retrieves the JWT token from the request. func (s *JWT) retrieveToken(httpRequest *http.Request) string { - jwtToken := httpRequest.Header.Get(s.cfg.JWTAuthHeaderName) - if jwtToken == "" && s.cfg.JWTAuthURLLogin { + jwtToken := httpRequest.Header.Get(s.cfg.JWTAuth.HeaderName) + if jwtToken == "" && s.cfg.JWTAuth.URLLogin { jwtToken = httpRequest.URL.Query().Get("auth_token") } // Strip the 'Bearer' prefix if it exists. @@ -146,7 +149,7 @@ func (s *JWT) retrieveToken(httpRequest *http.Request) string { } func (s *JWT) Test(ctx context.Context, r *authn.Request) bool { - if !s.cfg.JWTAuthEnabled || s.cfg.JWTAuthHeaderName == "" { + if !s.cfg.JWTAuth.Enabled || s.cfg.JWTAuth.HeaderName == "" { return false } @@ -171,11 +174,11 @@ func (s *JWT) Priority() uint { const roleGrafanaAdmin = "GrafanaAdmin" func (s *JWT) extractRoleAndAdmin(claims map[string]any) (org.RoleType, bool) { - if s.cfg.JWTAuthRoleAttributePath == "" { + if s.cfg.JWTAuth.RoleAttributePath == "" { return "", false } - role, err := searchClaimsForStringAttr(s.cfg.JWTAuthRoleAttributePath, claims) + role, err := util.SearchJSONForStringAttr(s.cfg.JWTAuth.RoleAttributePath, claims) if err != nil || role == "" { return "", false } @@ -186,33 +189,10 @@ func (s *JWT) extractRoleAndAdmin(claims map[string]any) (org.RoleType, bool) { return org.RoleType(role), false } -func searchClaimsForStringAttr(attributePath string, claims map[string]any) (string, error) { - val, err := searchClaimsForAttr(attributePath, claims) - if err != nil { - return "", err +func (s *JWT) extractGroups(claims map[string]any) ([]string, error) { + if s.cfg.JWTAuth.GroupsAttributePath == "" { + return []string{}, nil } - strVal, ok := val.(string) - if ok { - return strVal, nil - } - - return "", nil -} - -func searchClaimsForAttr(attributePath string, claims map[string]any) (any, error) { - if attributePath == "" { - return "", errors.New("no attribute path specified") - } - - if len(claims) == 0 { - return "", errors.New("empty claims provided") - } - - val, err := jmespath.Search(attributePath, claims) - if err != nil { - return "", fmt.Errorf("failed to search claims with provided path: %q: %w", attributePath, err) - } - - return val, nil + return util.SearchJSONForStringSliceAttr(s.cfg.JWTAuth.GroupsAttributePath, claims) } diff --git a/pkg/services/authn/clients/jwt_test.go b/pkg/services/authn/clients/jwt_test.go index 95ce08467a1..f704a691189 100644 --- a/pkg/services/authn/clients/jwt_test.go +++ b/pkg/services/authn/clients/jwt_test.go @@ -15,6 +15,7 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) func stringPtr(s string) *string { @@ -22,72 +23,153 @@ func stringPtr(s string) *string { } func TestAuthenticateJWT(t *testing.T) { - jwtService := &jwt.FakeJWTService{ - VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) { - return jwt.JWTClaims{ - "sub": "1234567890", - "email": "eai.doe@cor.po", - "preferred_username": "eai-doe", - "name": "Eai Doe", - "roles": "Admin", - }, nil - }, - } + t.Parallel() + jwtHeaderName := "X-Forwarded-User" - wantID := &authn.Identity{ - OrgID: 0, - OrgName: "", - OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, - ID: "", - Login: "eai-doe", - Name: "Eai Doe", - Email: "eai.doe@cor.po", - IsGrafanaAdmin: boolPtr(false), - AuthenticatedBy: login.JWTModule, - AuthID: "1234567890", - IsDisabled: false, - HelpFlags1: 0, - ClientParams: authn.ClientParams{ - SyncUser: true, - AllowSignUp: true, - FetchSyncedUser: true, - SyncOrgRoles: true, - SyncPermissions: true, - LookUpParams: login.UserLookupParams{ - UserID: nil, - Email: stringPtr("eai.doe@cor.po"), - Login: stringPtr("eai-doe"), + + testCases := []struct { + name string + wantID *authn.Identity + verifyProvider func(context.Context, string) (jwt.JWTClaims, error) + cfg *setting.Cfg + }{ + { + name: "Valid Use case with group path", + wantID: &authn.Identity{ + OrgID: 0, + OrgName: "", + OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, + Groups: []string{"foo", "bar"}, + ID: "", + Login: "eai-doe", + Name: "Eai Doe", + Email: "eai.doe@cor.po", + IsGrafanaAdmin: boolPtr(false), + AuthenticatedBy: login.JWTModule, + AuthID: "1234567890", + IsDisabled: false, + HelpFlags1: 0, + ClientParams: authn.ClientParams{ + SyncUser: true, + AllowSignUp: true, + FetchSyncedUser: true, + SyncOrgRoles: true, + SyncPermissions: true, + SyncTeams: true, + LookUpParams: login.UserLookupParams{ + UserID: nil, + Email: stringPtr("eai.doe@cor.po"), + Login: stringPtr("eai-doe"), + }, + }, + }, + verifyProvider: func(context.Context, string) (jwt.JWTClaims, error) { + return jwt.JWTClaims{ + "sub": "1234567890", + "email": "eai.doe@cor.po", + "preferred_username": "eai-doe", + "name": "Eai Doe", + "roles": "Admin", + "groups": []string{"foo", "bar"}, + }, nil + }, + cfg: &setting.Cfg{ + JWTAuth: setting.AuthJWTSettings{ + Enabled: true, + HeaderName: jwtHeaderName, + EmailClaim: "email", + UsernameClaim: "preferred_username", + AutoSignUp: true, + AllowAssignGrafanaAdmin: true, + RoleAttributeStrict: true, + RoleAttributePath: "roles", + GroupsAttributePath: "groups[]", + }, + }, + }, + { + name: "Valid Use case without group path", + wantID: &authn.Identity{ + OrgID: 0, + OrgName: "", + OrgRoles: map[int64]roletype.RoleType{1: roletype.RoleAdmin}, + ID: "", + Login: "eai-doe", + Groups: []string{}, + Name: "Eai Doe", + Email: "eai.doe@cor.po", + IsGrafanaAdmin: boolPtr(false), + AuthenticatedBy: login.JWTModule, + AuthID: "1234567890", + IsDisabled: false, + HelpFlags1: 0, + ClientParams: authn.ClientParams{ + SyncUser: true, + AllowSignUp: true, + FetchSyncedUser: true, + SyncOrgRoles: true, + SyncPermissions: true, + SyncTeams: false, + LookUpParams: login.UserLookupParams{ + UserID: nil, + Email: stringPtr("eai.doe@cor.po"), + Login: stringPtr("eai-doe"), + }, + }, + }, + verifyProvider: func(context.Context, string) (jwt.JWTClaims, error) { + return jwt.JWTClaims{ + "sub": "1234567890", + "email": "eai.doe@cor.po", + "preferred_username": "eai-doe", + "name": "Eai Doe", + "roles": "Admin", + "groups": []string{"foo", "bar"}, + }, nil + }, + cfg: &setting.Cfg{ + JWTAuth: setting.AuthJWTSettings{ + Enabled: true, + HeaderName: jwtHeaderName, + EmailClaim: "email", + UsernameClaim: "preferred_username", + AutoSignUp: true, + AllowAssignGrafanaAdmin: true, + RoleAttributeStrict: true, + RoleAttributePath: "roles", + }, }, }, } - cfg := &setting.Cfg{ - JWTAuthEnabled: true, - JWTAuthHeaderName: jwtHeaderName, - JWTAuthEmailClaim: "email", - JWTAuthUsernameClaim: "preferred_username", - JWTAuthAutoSignUp: true, - JWTAuthAllowAssignGrafanaAdmin: true, - JWTAuthRoleAttributeStrict: true, - JWTAuthRoleAttributePath: "roles", - } - jwtClient := ProvideJWT(jwtService, cfg) - validHTTPReq := &http.Request{ - Header: map[string][]string{ - jwtHeaderName: {"sample-token"}}, - } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + jwtService := &jwt.FakeJWTService{ + VerifyProvider: tc.verifyProvider, + } - id, err := jwtClient.Authenticate(context.Background(), &authn.Request{ - OrgID: 1, - HTTPRequest: validHTTPReq, - Resp: nil, - }) - require.NoError(t, err) + jwtClient := ProvideJWT(jwtService, tc.cfg) + validHTTPReq := &http.Request{ + Header: map[string][]string{ + jwtHeaderName: {"sample-token"}}, + } - assert.EqualValues(t, wantID, id, fmt.Sprintf("%+v", id)) + id, err := jwtClient.Authenticate(context.Background(), &authn.Request{ + OrgID: 1, + HTTPRequest: validHTTPReq, + Resp: nil, + }) + require.NoError(t, err) + + assert.EqualValues(t, tc.wantID, id, fmt.Sprintf("%+v", id)) + }) + } } func TestJWTClaimConfig(t *testing.T) { + t.Parallel() jwtService := &jwt.FakeJWTService{ VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) { return jwt.JWTClaims{ @@ -102,30 +184,19 @@ func TestJWTClaimConfig(t *testing.T) { jwtHeaderName := "X-Forwarded-User" - cfg := &setting.Cfg{ - JWTAuthEnabled: true, - JWTAuthHeaderName: jwtHeaderName, - JWTAuthAutoSignUp: true, - JWTAuthAllowAssignGrafanaAdmin: true, - JWTAuthRoleAttributeStrict: true, - JWTAuthRoleAttributePath: "roles", - } - // #nosec G101 -- This is a dummy/test token token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o" - type Dictionary map[string]any - type testCase struct { desc string - claimsConfigurations []Dictionary + claimsConfigurations []util.DynMap valid bool } testCases := []testCase{ { desc: "JWT configuration with email and username claims", - claimsConfigurations: []Dictionary{ + claimsConfigurations: []util.DynMap{ { "JWTAuthEmailClaim": true, "JWTAuthUsernameClaim": true, @@ -135,7 +206,7 @@ func TestJWTClaimConfig(t *testing.T) { }, { desc: "JWT configuration with email claim", - claimsConfigurations: []Dictionary{ + claimsConfigurations: []util.DynMap{ { "JWTAuthEmailClaim": true, "JWTAuthUsernameClaim": false, @@ -145,7 +216,7 @@ func TestJWTClaimConfig(t *testing.T) { }, { desc: "JWT configuration with username claim", - claimsConfigurations: []Dictionary{ + claimsConfigurations: []util.DynMap{ { "JWTAuthEmailClaim": false, "JWTAuthUsernameClaim": true, @@ -155,7 +226,7 @@ func TestJWTClaimConfig(t *testing.T) { }, { desc: "JWT configuration without email and username claims", - claimsConfigurations: []Dictionary{ + claimsConfigurations: []util.DynMap{ { "JWTAuthEmailClaim": false, "JWTAuthUsernameClaim": false, @@ -166,39 +237,53 @@ func TestJWTClaimConfig(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + cfg := &setting.Cfg{ + JWTAuth: setting.AuthJWTSettings{ + Enabled: true, + HeaderName: jwtHeaderName, + AutoSignUp: true, + AllowAssignGrafanaAdmin: true, + RoleAttributeStrict: true, + RoleAttributePath: "roles", + }, + } for _, claims := range tc.claimsConfigurations { - cfg.JWTAuthEmailClaim = "" - cfg.JWTAuthUsernameClaim = "" + cfg.JWTAuth.EmailClaim = "" + cfg.JWTAuth.UsernameClaim = "" if claims["JWTAuthEmailClaim"] == true { - cfg.JWTAuthEmailClaim = "email" + cfg.JWTAuth.EmailClaim = "email" } if claims["JWTAuthUsernameClaim"] == true { - cfg.JWTAuthUsernameClaim = "preferred_username" + cfg.JWTAuth.UsernameClaim = "preferred_username" } } + + httpReq := &http.Request{ + URL: &url.URL{RawQuery: "auth_token=" + token}, + Header: map[string][]string{ + jwtHeaderName: {token}}, + } + jwtClient := ProvideJWT(jwtService, cfg) + _, err := jwtClient.Authenticate(context.Background(), &authn.Request{ + OrgID: 1, + HTTPRequest: httpReq, + Resp: nil, + }) + if tc.valid { + require.NoError(t, err) + } else { + require.Error(t, err) + } }) - httpReq := &http.Request{ - URL: &url.URL{RawQuery: "auth_token=" + token}, - Header: map[string][]string{ - jwtHeaderName: {token}}, - } - jwtClient := ProvideJWT(jwtService, cfg) - _, err := jwtClient.Authenticate(context.Background(), &authn.Request{ - OrgID: 1, - HTTPRequest: httpReq, - Resp: nil, - }) - if tc.valid { - require.NoError(t, err) - } else { - require.Error(t, err) - } } } func TestJWTTest(t *testing.T) { + t.Parallel() jwtService := &jwt.FakeJWTService{} jwtHeaderName := "X-Forwarded-User" // #nosec G101 -- This is dummy/test token @@ -280,14 +365,18 @@ func TestJWTTest(t *testing.T) { } for _, tc := range testCases { + tc := tc t.Run(tc.desc, func(t *testing.T) { + t.Parallel() cfg := &setting.Cfg{ - JWTAuthEnabled: true, - JWTAuthURLLogin: tc.urlLogin, - JWTAuthHeaderName: tc.cfgHeaderName, - JWTAuthAutoSignUp: true, - JWTAuthAllowAssignGrafanaAdmin: true, - JWTAuthRoleAttributeStrict: true, + JWTAuth: setting.AuthJWTSettings{ + Enabled: true, + URLLogin: tc.urlLogin, + HeaderName: tc.cfgHeaderName, + AutoSignUp: true, + AllowAssignGrafanaAdmin: true, + RoleAttributeStrict: true, + }, } jwtClient := ProvideJWT(jwtService, cfg) httpReq := &http.Request{ @@ -308,6 +397,7 @@ func TestJWTTest(t *testing.T) { } func TestJWTStripParam(t *testing.T) { + t.Parallel() jwtService := &jwt.FakeJWTService{ VerifyProvider: func(context.Context, string) (jwt.JWTClaims, error) { return jwt.JWTClaims{ @@ -323,15 +413,17 @@ func TestJWTStripParam(t *testing.T) { jwtHeaderName := "X-Forwarded-User" cfg := &setting.Cfg{ - JWTAuthEnabled: true, - JWTAuthHeaderName: jwtHeaderName, - JWTAuthAutoSignUp: true, - JWTAuthAllowAssignGrafanaAdmin: true, - JWTAuthURLLogin: true, - JWTAuthRoleAttributeStrict: false, - JWTAuthRoleAttributePath: "roles", - JWTAuthEmailClaim: "email", - JWTAuthUsernameClaim: "preferred_username", + JWTAuth: setting.AuthJWTSettings{ + Enabled: true, + HeaderName: jwtHeaderName, + AutoSignUp: true, + AllowAssignGrafanaAdmin: true, + URLLogin: true, + RoleAttributeStrict: false, + RoleAttributePath: "roles", + EmailClaim: "email", + UsernameClaim: "preferred_username", + }, } // #nosec G101 -- This is a dummy/test token diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index 6ea987300f6..7bb582c970e 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -213,8 +213,8 @@ func WithAuthHTTPHeaders(ctx context.Context, cfg *setting.Cfg) context.Context list.Items = append(list.Items, "X-Grafana-Device-Id") // if jwt is enabled we add it to the list. We can ignore in case it is set to Authorization - if cfg.JWTAuthEnabled && cfg.JWTAuthHeaderName != "" && cfg.JWTAuthHeaderName != "Authorization" { - list.Items = append(list.Items, cfg.JWTAuthHeaderName) + if cfg.JWTAuth.Enabled && cfg.JWTAuth.HeaderName != "" && cfg.JWTAuth.HeaderName != "Authorization" { + list.Items = append(list.Items, cfg.JWTAuth.HeaderName) } // if auth proxy is enabled add the main proxy header and all configured headers diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index 47f5adc4816..cd6b1e1663b 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -153,8 +153,8 @@ func TestContextHandler(t *testing.T) { t.Run("should store auth header in context", func(t *testing.T) { cfg := setting.NewCfg() - cfg.JWTAuthEnabled = true - cfg.JWTAuthHeaderName = "jwt-header" + cfg.JWTAuth.Enabled = true + cfg.JWTAuth.HeaderName = "jwt-header" cfg.AuthProxyEnabled = true cfg.AuthProxyHeaderName = "proxy-header" cfg.AuthProxyHeaders = map[string]string{ diff --git a/pkg/services/login/authinfo.go b/pkg/services/login/authinfo.go index f70dae8c869..4f3d95bd931 100644 --- a/pkg/services/login/authinfo.go +++ b/pkg/services/login/authinfo.go @@ -76,7 +76,7 @@ func IsExternallySynced(cfg *setting.Cfg, authModule string, oauthInfo *social.O case LDAPAuthModule: return !cfg.LDAPSkipOrgRoleSync case JWTModule: - return !cfg.JWTAuthSkipOrgRoleSync + return !cfg.JWTAuth.SkipOrgRoleSync } // then check the rest of the oauth providers // FIXME: remove this once we remove the setting @@ -104,7 +104,7 @@ func IsGrafanaAdminExternallySynced(cfg *setting.Cfg, oauthInfo *social.OAuthInf switch authModule { case JWTModule: - return cfg.JWTAuthAllowAssignGrafanaAdmin + return cfg.JWTAuth.AllowAssignGrafanaAdmin case SAMLAuthModule: return cfg.SAMLRoleValuesGrafanaAdmin != "" case LDAPAuthModule: @@ -121,7 +121,7 @@ func IsProviderEnabled(cfg *setting.Cfg, authModule string, oauthInfo *social.OA case LDAPAuthModule: return cfg.LDAPAuthEnabled case JWTModule: - return cfg.JWTAuthEnabled + return cfg.JWTAuth.Enabled case GoogleAuthModule, OktaAuthModule, AzureADAuthModule, GitLabAuthModule, GithubAuthModule, GrafanaComAuthModule, GenericOAuthModule: if oauthInfo == nil { return false diff --git a/pkg/services/login/authinfo_test.go b/pkg/services/login/authinfo_test.go index bd84e471e83..39746ef8745 100644 --- a/pkg/services/login/authinfo_test.go +++ b/pkg/services/login/authinfo_test.go @@ -3,9 +3,10 @@ package login import ( "testing" + "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" ) func TestIsExternallySynced(t *testing.T) { @@ -82,20 +83,20 @@ func TestIsExternallySynced(t *testing.T) { // jwt { name: "JWT synced user should return that it is externally synced", - cfg: &setting.Cfg{JWTAuthEnabled: true, JWTAuthSkipOrgRoleSync: false}, + cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: true, SkipOrgRoleSync: false}}, provider: JWTModule, expected: true, }, { name: "JWT synced user should return that it is not externally synced when org role sync is set", - cfg: &setting.Cfg{JWTAuthEnabled: true, JWTAuthSkipOrgRoleSync: true}, + cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: true, SkipOrgRoleSync: true}}, provider: JWTModule, expected: false, }, // IsProvider test { name: "If no provider enabled should return false", - cfg: &setting.Cfg{JWTAuthSkipOrgRoleSync: true}, + cfg: &setting.Cfg{JWTAuth: setting.AuthJWTSettings{Enabled: false, SkipOrgRoleSync: true}}, provider: JWTModule, expected: false, }, diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 92127cc6314..5d6ed89392d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -267,24 +267,7 @@ type Cfg struct { OAuthCookieMaxAge int OAuthAllowInsecureEmailLookup bool - // JWT Auth - JWTAuthEnabled bool - JWTAuthHeaderName string - JWTAuthURLLogin bool - JWTAuthEmailClaim string - JWTAuthUsernameClaim string - JWTAuthExpectClaims string - JWTAuthJWKSetURL string - JWTAuthCacheTTL time.Duration - JWTAuthKeyFile string - JWTAuthKeyID string - JWTAuthJWKSetFile string - JWTAuthAutoSignUp bool - JWTAuthRoleAttributePath string - JWTAuthRoleAttributeStrict bool - JWTAuthAllowAssignGrafanaAdmin bool - JWTAuthSkipOrgRoleSync bool - + JWTAuth AuthJWTSettings // Extended JWT Auth ExtendedJWTAuthEnabled bool ExtendedJWTExpectIssuer string @@ -1195,6 +1178,7 @@ func (cfg *Cfg) parseINIFile(iniFile *ini.File) error { cfg.readLDAPConfig() cfg.handleAWSConfig() cfg.readAzureSettings() + cfg.readAuthJWTSettings() cfg.readSessionConfig() if err := cfg.readSmtpSettings(); err != nil { return err @@ -1608,25 +1592,6 @@ func readAuthSettings(iniFile *ini.File, cfg *Cfg) (err error) { authBasic := iniFile.Section("auth.basic") cfg.BasicAuthEnabled = authBasic.Key("enabled").MustBool(true) - // JWT auth - authJWT := iniFile.Section("auth.jwt") - cfg.JWTAuthEnabled = authJWT.Key("enabled").MustBool(false) - cfg.JWTAuthHeaderName = valueAsString(authJWT, "header_name", "") - cfg.JWTAuthURLLogin = authJWT.Key("url_login").MustBool(false) - cfg.JWTAuthEmailClaim = valueAsString(authJWT, "email_claim", "") - cfg.JWTAuthUsernameClaim = valueAsString(authJWT, "username_claim", "") - cfg.JWTAuthExpectClaims = valueAsString(authJWT, "expect_claims", "{}") - cfg.JWTAuthJWKSetURL = valueAsString(authJWT, "jwk_set_url", "") - cfg.JWTAuthCacheTTL = authJWT.Key("cache_ttl").MustDuration(time.Minute * 60) - cfg.JWTAuthKeyFile = valueAsString(authJWT, "key_file", "") - cfg.JWTAuthKeyID = authJWT.Key("key_id").MustString("") - cfg.JWTAuthJWKSetFile = valueAsString(authJWT, "jwk_set_file", "") - cfg.JWTAuthAutoSignUp = authJWT.Key("auto_sign_up").MustBool(false) - cfg.JWTAuthRoleAttributePath = valueAsString(authJWT, "role_attribute_path", "") - cfg.JWTAuthRoleAttributeStrict = authJWT.Key("role_attribute_strict").MustBool(false) - cfg.JWTAuthAllowAssignGrafanaAdmin = authJWT.Key("allow_assign_grafana_admin").MustBool(false) - cfg.JWTAuthSkipOrgRoleSync = authJWT.Key("skip_org_role_sync").MustBool(false) - // Extended JWT auth authExtendedJWT := cfg.SectionWithEnvOverrides("auth.extended_jwt") cfg.ExtendedJWTAuthEnabled = authExtendedJWT.Key("enabled").MustBool(false) diff --git a/pkg/setting/setting_jwt.go b/pkg/setting/setting_jwt.go new file mode 100644 index 00000000000..1f6a672e526 --- /dev/null +++ b/pkg/setting/setting_jwt.go @@ -0,0 +1,48 @@ +package setting + +import "time" + +type AuthJWTSettings struct { + // JWT Auth + Enabled bool + HeaderName string + URLLogin bool + EmailClaim string + UsernameClaim string + ExpectClaims string + JWKSetURL string + CacheTTL time.Duration + KeyFile string + KeyID string + JWKSetFile string + AutoSignUp bool + RoleAttributePath string + RoleAttributeStrict bool + AllowAssignGrafanaAdmin bool + SkipOrgRoleSync bool + GroupsAttributePath string +} + +func (cfg *Cfg) readAuthJWTSettings() { + jwtSettings := AuthJWTSettings{} + authJWT := cfg.Raw.Section("auth.jwt") + jwtSettings.Enabled = authJWT.Key("enabled").MustBool(false) + jwtSettings.HeaderName = valueAsString(authJWT, "header_name", "") + jwtSettings.URLLogin = authJWT.Key("url_login").MustBool(false) + jwtSettings.EmailClaim = valueAsString(authJWT, "email_claim", "") + jwtSettings.UsernameClaim = valueAsString(authJWT, "username_claim", "") + jwtSettings.ExpectClaims = valueAsString(authJWT, "expect_claims", "{}") + jwtSettings.JWKSetURL = valueAsString(authJWT, "jwk_set_url", "") + jwtSettings.CacheTTL = authJWT.Key("cache_ttl").MustDuration(time.Minute * 60) + jwtSettings.KeyFile = valueAsString(authJWT, "key_file", "") + jwtSettings.KeyID = authJWT.Key("key_id").MustString("") + jwtSettings.JWKSetFile = valueAsString(authJWT, "jwk_set_file", "") + jwtSettings.AutoSignUp = authJWT.Key("auto_sign_up").MustBool(false) + jwtSettings.RoleAttributePath = valueAsString(authJWT, "role_attribute_path", "") + jwtSettings.RoleAttributeStrict = authJWT.Key("role_attribute_strict").MustBool(false) + jwtSettings.AllowAssignGrafanaAdmin = authJWT.Key("allow_assign_grafana_admin").MustBool(false) + jwtSettings.SkipOrgRoleSync = authJWT.Key("skip_org_role_sync").MustBool(false) + jwtSettings.GroupsAttributePath = valueAsString(authJWT, "groups_attribute_path", "") + + cfg.JWTAuth = jwtSettings +} diff --git a/pkg/util/json.go b/pkg/util/json.go index 7268ff93798..b1e7503bf51 100644 --- a/pkg/util/json.go +++ b/pkg/util/json.go @@ -1,4 +1,112 @@ package util +import ( + "encoding/json" + + "github.com/jmespath/go-jmespath" + + "github.com/grafana/grafana/pkg/util/errutil" +) + // DynMap defines a dynamic map interface. type DynMap map[string]any + +var ( + // ErrEmptyJSON is an error for empty attribute in JSON. + ErrEmptyJSON = errutil.NewBase(errutil.StatusBadRequest, + "json-missing-body", errutil.WithPublicMessage("Empty JSON provided")) + + // ErrNoAttributePathSpecified is an error for no attribute path specified. + ErrNoAttributePathSpecified = errutil.NewBase(errutil.StatusBadRequest, + "json-no-attribute-path-specified", errutil.WithPublicMessage("No attribute path specified")) + + // ErrFailedToUnmarshalJSON is an error for failure in unmarshalling JSON. + ErrFailedToUnmarshalJSON = errutil.NewBase(errutil.StatusBadRequest, + "json-failed-to-unmarshal", errutil.WithPublicMessage("Failed to unmarshal JSON")) + + // ErrFailedToSearchJSON is an error for failure in searching JSON. + ErrFailedToSearchJSON = errutil.NewBase(errutil.StatusBadRequest, + "json-failed-to-search", errutil.WithPublicMessage("Failed to search JSON with provided path")) +) + +// SearchJSONForStringSliceAttr searches for a slice attribute in a JSON object and returns a string slice. +// The attributePath parameter is a string that specifies the path to the attribute. +// The data parameter is the JSON object that we're searching. It can be a byte slice or a go type. +func SearchJSONForStringSliceAttr(attributePath string, data any) ([]string, error) { + val, err := searchJSONForAttr(attributePath, data) + if err != nil { + return []string{}, err + } + + ifArr, ok := val.([]any) + if !ok { + return []string{}, nil + } + + result := []string{} + for _, v := range ifArr { + if strVal, ok := v.(string); ok { + result = append(result, strVal) + } + } + + return result, nil +} + +// SearchJSONForStringAttr searches for a specific attribute in a JSON object and returns a string. +// The attributePath parameter is a string that specifies the path to the attribute. +// The data parameter is the JSON object that we're searching. It can be a byte slice or a go type. +func SearchJSONForStringAttr(attributePath string, data any) (string, error) { + val, err := searchJSONForAttr(attributePath, data) + if err != nil { + return "", err + } + + strVal, ok := val.(string) + if ok { + return strVal, nil + } + + return "", nil +} + +// searchJSONForAttr searches for a specific attribute in a JSON object. +// The attributePath parameter is a string that specifies the path to the attribute. +// The data parameter is the JSON object that we're searching. +// The function returns the value of the attribute and an error if one occurred. +func searchJSONForAttr(attributePath string, data any) (any, error) { + // If no attribute path is specified, return an error + if attributePath == "" { + return "", ErrNoAttributePathSpecified.Errorf("attribute path: %q", attributePath) + } + + // If the data is nil, return an error + if data == nil { + return "", ErrEmptyJSON.Errorf("empty json, attribute path: %q", attributePath) + } + + // Copy the data to a new variable + var jsonData = data + + // If the data is a byte slice, try to unmarshal it into a JSON object + if dataBytes, ok := data.([]byte); ok { + // If the byte slice is empty, return an error + if len(dataBytes) == 0 { + return "", ErrEmptyJSON.Errorf("empty json, attribute path: %q", attributePath) + } + + // Try to unmarshal the byte slice + if err := json.Unmarshal(dataBytes, &jsonData); err != nil { + return "", ErrFailedToUnmarshalJSON.Errorf("%v: %w", "failed to unmarshal user info JSON response", err) + } + } + + // Search for the attribute in the JSON object + value, err := jmespath.Search(attributePath, jsonData) + if err != nil { + return "", ErrFailedToSearchJSON.Errorf("failed to search user info JSON response with provided path: %q: %w", attributePath, err) + } + + // Return the value and nil error + return value, nil +} diff --git a/pkg/util/json_test.go b/pkg/util/json_test.go new file mode 100644 index 00000000000..232043a05f5 --- /dev/null +++ b/pkg/util/json_test.go @@ -0,0 +1,155 @@ +package util_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/util" +) + +func TestSearchJSONForGroups(t *testing.T) { + t.Parallel() + tests := []struct { + Name string + searchObject any + GroupsAttributePath string + ExpectedResult []string + ExpectedError error + }{ + { + Name: "Given an invalid user info JSON response", + searchObject: []byte("{"), + GroupsAttributePath: "attributes.groups", + ExpectedResult: []string{}, + ExpectedError: util.ErrFailedToUnmarshalJSON, + }, + { + Name: "Given an empty user info JSON response and empty JMES path", + searchObject: []byte{}, + GroupsAttributePath: "", + ExpectedResult: []string{}, + ExpectedError: util.ErrNoAttributePathSpecified, + }, + { + Name: "Given an empty user info JSON response and valid JMES path", + searchObject: []byte{}, + GroupsAttributePath: "attributes.groups", + ExpectedResult: []string{}, + ExpectedError: util.ErrEmptyJSON, + }, + { + Name: "Given a nil JSON and valid JMES path", + searchObject: []byte{}, + GroupsAttributePath: "attributes.groups", + ExpectedResult: []string{}, + ExpectedError: util.ErrEmptyJSON, + }, + { + Name: "Given a simple user info JSON response and valid JMES path", + searchObject: []byte(`{ + "attributes": { + "groups": ["foo", "bar"] + } +}`), + GroupsAttributePath: "attributes.groups[]", + ExpectedResult: []string{"foo", "bar"}, + }, + { + Name: "Given a simple object and valid JMES path", + searchObject: map[string]any{ + "attributes": map[string]any{ + "groups": []string{"foo", "bar"}, + }, + }, + GroupsAttributePath: "attributes.groups[]", + ExpectedResult: []string{"foo", "bar"}, + }, + } + + for _, test := range tests { + test := test + t.Run(test.Name, func(t *testing.T) { + t.Parallel() + actualResult, err := util.SearchJSONForStringSliceAttr( + test.GroupsAttributePath, test.searchObject) + if test.ExpectedError == nil { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, test.ExpectedError) + } + require.Equal(t, test.ExpectedResult, actualResult) + }) + } +} + +func TestSearchJSONForEmail(t *testing.T) { + t.Parallel() + tests := []struct { + Name string + UserInfoJSONResponse any + EmailAttributePath string + ExpectedResult string + ExpectedError error + }{ + { + Name: "Given a simple user info JSON response and valid JMES path", + UserInfoJSONResponse: []byte(`{ + "attributes": { + "email": "grafana@localhost" + } +}`), + EmailAttributePath: "attributes.email", + ExpectedResult: "grafana@localhost", + }, + { + Name: "Given a simple object and valid JMES path", + UserInfoJSONResponse: map[string]any{ + "attributes": map[string]any{ + "email": "grafana@localhost", + }, + }, + EmailAttributePath: "attributes.email", + ExpectedResult: "grafana@localhost", + }, + { + Name: "Given a user info JSON response with e-mails array and valid JMES path", + UserInfoJSONResponse: []byte(`{ + "attributes": { + "emails": ["grafana@localhost", "admin@localhost"] + } +}`), + EmailAttributePath: "attributes.emails[0]", + ExpectedResult: "grafana@localhost", + }, + { + Name: "Given a nested user info JSON response and valid JMES path", + UserInfoJSONResponse: []byte(`{ + "identities": [ + { + "userId": "grafana@localhost" + }, + { + "userId": "admin@localhost" + } + ] +}`), + EmailAttributePath: "identities[0].userId", + ExpectedResult: "grafana@localhost", + }, + } + + for _, test := range tests { + test := test + t.Run(test.Name, func(t *testing.T) { + t.Parallel() + actualResult, err := util.SearchJSONForStringAttr(test.EmailAttributePath, test.UserInfoJSONResponse) + if test.ExpectedError != nil { + require.NoError(t, err) + } else { + require.ErrorIs(t, err, test.ExpectedError) + } + require.Equal(t, test.ExpectedResult, actualResult) + }) + } +} From 9bc3517617c591bce6395fb6918d964a43621550 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Sat, 10 Feb 2024 01:37:55 +0900 Subject: [PATCH 20/50] Snapshots: Fix issue where off-screen panels are not included in snapshots (#82135) --- public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx index 5e1823cd701..32cd4ba0fff 100644 --- a/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx +++ b/public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx @@ -340,7 +340,7 @@ export class PanelStateWrapper extends PureComponent { onRefresh = () => { const { dashboard, panel, isInView, width } = this.props; - if (!isInView) { + if (!dashboard.snapshot && !isInView) { panel.refreshWhenInView = true; return; } From 54a77fa55e311d8059e6249ceb116564bebbc1a2 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Fri, 9 Feb 2024 09:39:58 -0700 Subject: [PATCH 21/50] K8s: StackIDs can be single digits (#82267) --- pkg/services/apiserver/endpoints/request/namespace.go | 2 +- pkg/services/apiserver/endpoints/request/namespace_test.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/apiserver/endpoints/request/namespace.go b/pkg/services/apiserver/endpoints/request/namespace.go index ccec8680f81..34f8a00bceb 100644 --- a/pkg/services/apiserver/endpoints/request/namespace.go +++ b/pkg/services/apiserver/endpoints/request/namespace.go @@ -68,7 +68,7 @@ func ParseNamespace(ns string) (NamespaceInfo, error) { if strings.HasPrefix(ns, "stack-") { info.StackID = ns[6:] - if len(info.StackID) < 2 { + if len(info.StackID) < 1 { return info, fmt.Errorf("invalid stack id") } info.OrgID = 1 diff --git a/pkg/services/apiserver/endpoints/request/namespace_test.go b/pkg/services/apiserver/endpoints/request/namespace_test.go index cd5a8e58394..86c863e3ef6 100644 --- a/pkg/services/apiserver/endpoints/request/namespace_test.go +++ b/pkg/services/apiserver/endpoints/request/namespace_test.go @@ -94,11 +94,11 @@ func TestParseNamespace(t *testing.T) { }, { name: "invalid stack id (too short)", - namespace: "stack-1", + namespace: "stack-", expectErr: true, expected: request.NamespaceInfo{ OrgID: -1, - StackID: "1", + StackID: "", }, }, { From 1f208cd8aed53b0a508b5104155fe34aed937314 Mon Sep 17 00:00:00 2001 From: Alex Khomenko Date: Fri, 9 Feb 2024 17:42:17 +0100 Subject: [PATCH 22/50] Icons: Update observability icon (#82266) Update observability icon --- public/img/icons/unicons/frontend-observability.svg | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/public/img/icons/unicons/frontend-observability.svg b/public/img/icons/unicons/frontend-observability.svg index 38ffbd70502..aaac7f2447b 100644 --- a/public/img/icons/unicons/frontend-observability.svg +++ b/public/img/icons/unicons/frontend-observability.svg @@ -1,4 +1,6 @@ - - - + + + + + From f60b5ecec49e578484286df00ebd59c94eef7e3a Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 9 Feb 2024 08:48:11 -0800 Subject: [PATCH 23/50] Chore: Avoid NPE with annotations query (#82216) --- public/app/plugins/datasource/grafana/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 934eadf8708..698db42459d 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -216,7 +216,7 @@ export class GrafanaDatasource extends DataSourceWithBackend { if (target.type === GrafanaAnnotationType.Dashboard) { // if no dashboard id yet return - if (!options.dashboard.uid) { + if (!options.dashboard?.uid) { return Promise.resolve({ data: [] }); } // filter by dashboard id From 42d6e176bcbda4783df52e77b62bc7190bbda83e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Calisto?= Date: Fri, 9 Feb 2024 17:48:56 +0100 Subject: [PATCH 24/50] Feature Toggle Management: allow editing PublicPreview toggles (#81562) * Feature Toggle Management: allow editing PublicPreview toggles * lint * fix a bunch of tests * tests are passing * add permissions unit tests back * fix display * close dialog after submit * use reload method after submit * make local development easier * always show editing alert in the UI * fix readme --------- Co-authored-by: Michael Mandrus --- pkg/apis/featuretoggle/v0alpha1/types.go | 3 + pkg/registry/apis/featuretoggle/README.md | 5 + pkg/registry/apis/featuretoggle/current.go | 42 +- .../apis/featuretoggle/current_test.go | 460 ++++++++++++++++++ pkg/registry/apis/featuretoggle/register.go | 9 +- pkg/services/apiserver/standalone/factory.go | 1 + pkg/services/featuremgmt/manager.go | 1 + pkg/services/featuremgmt/models.go | 2 +- .../features/admin/AdminFeatureTogglesAPI.ts | 3 + .../admin/AdminFeatureTogglesPage.tsx | 10 +- .../admin/AdminFeatureTogglesTable.tsx | 73 ++- 11 files changed, 587 insertions(+), 22 deletions(-) create mode 100644 pkg/registry/apis/featuretoggle/README.md create mode 100644 pkg/registry/apis/featuretoggle/current_test.go diff --git a/pkg/apis/featuretoggle/v0alpha1/types.go b/pkg/apis/featuretoggle/v0alpha1/types.go index 4fa25c5135d..e10c7f2b080 100644 --- a/pkg/apis/featuretoggle/v0alpha1/types.go +++ b/pkg/apis/featuretoggle/v0alpha1/types.go @@ -98,6 +98,9 @@ type ToggleStatus struct { // The flag description Description string `json:"description,omitempty"` + // The feature toggle stage + Stage string `json:"stage"` + // Is the flag enabled Enabled bool `json:"enabled"` diff --git a/pkg/registry/apis/featuretoggle/README.md b/pkg/registry/apis/featuretoggle/README.md new file mode 100644 index 00000000000..7267e2e2819 --- /dev/null +++ b/pkg/registry/apis/featuretoggle/README.md @@ -0,0 +1,5 @@ +This package supports the [Feature toggle admin page](https://grafana.com/docs/grafana/latest/administration/feature-toggles/) feature. + +In order to update feature toggles through the app, the PATCH handler calls a webhook that should update Grafana's configuration and restarts the instance. + +For local development, set the app mode to `development` by adding `app_mode = development` to the top level of your Grafana .ini file. \ No newline at end of file diff --git a/pkg/registry/apis/featuretoggle/current.go b/pkg/registry/apis/featuretoggle/current.go index 7bc5661c75b..aa8204fc4aa 100644 --- a/pkg/registry/apis/featuretoggle/current.go +++ b/pkg/registry/apis/featuretoggle/current.go @@ -51,6 +51,7 @@ func (b *FeatureFlagAPIBuilder) getResolvedToggleState(ctx context.Context) v0al toggle := v0alpha1.ToggleStatus{ Name: name, Description: f.Description, // simplify the UI changes + Stage: f.Stage.String(), Enabled: state.Enabled[name], Writeable: b.features.IsEditableFromAdminPage(name), Source: startupRef, @@ -76,6 +77,17 @@ func (b *FeatureFlagAPIBuilder) getResolvedToggleState(ctx context.Context) v0al return state } +func (b *FeatureFlagAPIBuilder) userCanRead(ctx context.Context, u *user.SignedInUser) bool { + if u == nil { + u, _ = appcontext.User(ctx) + if u == nil { + return false + } + } + ok, err := b.accessControl.Evaluate(ctx, u, ac.EvalPermission(ac.ActionFeatureManagementRead)) + return ok && err == nil +} + func (b *FeatureFlagAPIBuilder) userCanWrite(ctx context.Context, u *user.SignedInUser) bool { if u == nil { u, _ = appcontext.User(ctx) @@ -93,7 +105,24 @@ func (b *FeatureFlagAPIBuilder) handleCurrentStatus(w http.ResponseWriter, r *ht return } + // Check if the user can access toggle info + ctx := r.Context() + user, err := appcontext.User(ctx) + if err != nil { + errhttp.Write(ctx, err, w) + return + } + + if !b.userCanRead(ctx, user) { + err = errutil.Unauthorized("featuretoggle.canNotRead", + errutil.WithPublicMessage("missing read permission")).Errorf("user %s does not have read permissions", user.Login) + errhttp.Write(ctx, err, w) + return + } + + // Write the state to the response body state := b.getResolvedToggleState(r.Context()) + w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(state) } @@ -101,7 +130,9 @@ func (b *FeatureFlagAPIBuilder) handleCurrentStatus(w http.ResponseWriter, r *ht func (b *FeatureFlagAPIBuilder) handlePatchCurrent(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if !b.features.IsFeatureEditingAllowed() { - errhttp.Write(ctx, fmt.Errorf("feature editing is not enabled"), w) + err := errutil.Forbidden("featuretoggle.disabled", + errutil.WithPublicMessage("feature toggles are read-only")).Errorf("feature toggles are not writeable due to missing configuration") + errhttp.Write(ctx, err, w) return } @@ -113,7 +144,7 @@ func (b *FeatureFlagAPIBuilder) handlePatchCurrent(w http.ResponseWriter, r *htt if !b.userCanWrite(ctx, user) { err = errutil.Unauthorized("featuretoggle.canNotWrite", - errutil.WithPublicMessage("missing write permission")) + errutil.WithPublicMessage("missing write permission")).Errorf("user %s does not have write permissions", user.Login) errhttp.Write(ctx, err, w) return } @@ -127,7 +158,7 @@ func (b *FeatureFlagAPIBuilder) handlePatchCurrent(w http.ResponseWriter, r *htt if len(request.Toggles) > 0 { err = errutil.BadRequest("featuretoggle.badRequest", - errutil.WithPublicMessage("can only path the enabled section")) + errutil.WithPublicMessage("can only patch the enabled section")).Errorf("request payload included properties in the read-only Toggles section") errhttp.Write(ctx, err, w) return } @@ -138,7 +169,7 @@ func (b *FeatureFlagAPIBuilder) handlePatchCurrent(w http.ResponseWriter, r *htt if current != v { if !b.features.IsEditableFromAdminPage(k) { err = errutil.BadRequest("featuretoggle.badRequest", - errutil.WithPublicMessage("can not edit toggle: "+k)) + errutil.WithPublicMessage("invalid toggle passed in")).Errorf("can not edit toggle %s", k) errhttp.Write(ctx, err, w) w.WriteHeader(http.StatusBadRequest) return @@ -158,7 +189,8 @@ func (b *FeatureFlagAPIBuilder) handlePatchCurrent(w http.ResponseWriter, r *htt } err = sendWebhookUpdate(b.features.Settings, payload) - if err != nil { + if err != nil && b.cfg.Env != setting.Dev { + err = errutil.Internal("featuretoggle.webhookFailure", errutil.WithPublicMessage("an error occurred while updating feeature toggles")).Errorf("webhook error: %w", err) errhttp.Write(ctx, err, w) return } diff --git a/pkg/registry/apis/featuretoggle/current_test.go b/pkg/registry/apis/featuretoggle/current_test.go new file mode 100644 index 00000000000..7b1b24c52bb --- /dev/null +++ b/pkg/registry/apis/featuretoggle/current_test.go @@ -0,0 +1,460 @@ +package featuretoggle + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" + "github.com/grafana/grafana/pkg/infra/appcontext" + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +func TestGetFeatureToggles(t *testing.T) { + t.Run("fails without adequate permissions", func(t *testing.T) { + features := featuremgmt.WithFeatureManager(setting.FeatureMgmtSettings{}, []*featuremgmt.FeatureFlag{{ + // Add this here to ensure the feature works as expected during tests + Name: featuremgmt.FlagFeatureToggleAdminPage, + Stage: featuremgmt.FeatureStageGeneralAvailability, + }}) + + b := NewFeatureFlagAPIBuilder(features, actest.FakeAccessControl{ExpectedEvaluate: false}, &setting.Cfg{}) + + callGetWith(t, b, http.StatusUnauthorized) + }) + + t.Run("should be able to get feature toggles", func(t *testing.T) { + features := []*featuremgmt.FeatureFlag{ + { + Name: "toggle1", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, { + Name: "toggle2", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, + } + disabled := []string{"toggle2"} + + b := newTestAPIBuilder(t, features, disabled, setting.FeatureMgmtSettings{}) + result := callGetWith(t, b, http.StatusOK) + assert.Len(t, result.Toggles, 2) + t1, _ := findResult(t, result, "toggle1") + assert.True(t, t1.Enabled) + t2, _ := findResult(t, result, "toggle2") + assert.False(t, t2.Enabled) + }) + + t.Run("toggles hidden by config are not present in the response", func(t *testing.T) { + features := []*featuremgmt.FeatureFlag{ + { + Name: "toggle1", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, { + Name: "toggle2", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, + } + settings := setting.FeatureMgmtSettings{ + HiddenToggles: map[string]struct{}{"toggle1": {}}, + } + + b := newTestAPIBuilder(t, features, []string{}, settings) + result := callGetWith(t, b, http.StatusOK) + + assert.Len(t, result.Toggles, 1) + assert.Equal(t, "toggle2", result.Toggles[0].Name) + }) + + t.Run("toggles that are read-only by config have the readOnly field set", func(t *testing.T) { + features := []*featuremgmt.FeatureFlag{ + { + Name: "toggle1", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, { + Name: "toggle2", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, + } + disabled := []string{"toggle2"} + settings := setting.FeatureMgmtSettings{ + HiddenToggles: map[string]struct{}{"toggle1": {}}, + ReadOnlyToggles: map[string]struct{}{"toggle2": {}}, + AllowEditing: true, + UpdateWebhook: "bogus", + } + + b := newTestAPIBuilder(t, features, disabled, settings) + result := callGetWith(t, b, http.StatusOK) + + assert.Len(t, result.Toggles, 1) + assert.Equal(t, "toggle2", result.Toggles[0].Name) + assert.False(t, result.Toggles[0].Writeable) + }) + + t.Run("feature toggle defailts", func(t *testing.T) { + features := []*featuremgmt.FeatureFlag{ + { + Name: "toggle1", + Stage: featuremgmt.FeatureStageUnknown, + }, { + Name: "toggle2", + Stage: featuremgmt.FeatureStageExperimental, + }, { + Name: "toggle3", + Stage: featuremgmt.FeatureStagePrivatePreview, + }, { + Name: "toggle4", + Stage: featuremgmt.FeatureStagePublicPreview, + AllowSelfServe: true, + }, { + Name: "toggle5", + Stage: featuremgmt.FeatureStageGeneralAvailability, + AllowSelfServe: true, + }, { + Name: "toggle6", + Stage: featuremgmt.FeatureStageDeprecated, + AllowSelfServe: true, + }, { + Name: "toggle7", + Stage: featuremgmt.FeatureStageGeneralAvailability, + AllowSelfServe: false, + }, + } + + t.Run("unknown, experimental, and private preview toggles are hidden by default", func(t *testing.T) { + b := newTestAPIBuilder(t, features, []string{}, setting.FeatureMgmtSettings{}) + result := callGetWith(t, b, http.StatusOK) + + assert.Len(t, result.Toggles, 4) + + _, ok := findResult(t, result, "toggle1") + assert.False(t, ok) + _, ok = findResult(t, result, "toggle2") + assert.False(t, ok) + _, ok = findResult(t, result, "toggle3") + assert.False(t, ok) + }) + + t.Run("only public preview and GA with AllowSelfServe are writeable", func(t *testing.T) { + settings := setting.FeatureMgmtSettings{ + AllowEditing: true, + UpdateWebhook: "bogus", + } + + b := newTestAPIBuilder(t, features, []string{}, settings) + result := callGetWith(t, b, http.StatusOK) + + t4, ok := findResult(t, result, "toggle4") + assert.True(t, ok) + assert.True(t, t4.Writeable) + t5, ok := findResult(t, result, "toggle5") + assert.True(t, ok) + assert.True(t, t5.Writeable) + t6, ok := findResult(t, result, "toggle6") + assert.True(t, ok) + assert.True(t, t6.Writeable) + }) + + t.Run("all toggles are read-only when server is misconfigured", func(t *testing.T) { + settings := setting.FeatureMgmtSettings{ + AllowEditing: false, + UpdateWebhook: "", + } + b := newTestAPIBuilder(t, features, []string{}, settings) + result := callGetWith(t, b, http.StatusOK) + + assert.Len(t, result.Toggles, 4) + + t4, ok := findResult(t, result, "toggle4") + assert.True(t, ok) + assert.False(t, t4.Writeable) + t5, ok := findResult(t, result, "toggle5") + assert.True(t, ok) + assert.False(t, t5.Writeable) + t6, ok := findResult(t, result, "toggle6") + assert.True(t, ok) + assert.False(t, t6.Writeable) + }) + }) +} + +func TestSetFeatureToggles(t *testing.T) { + t.Run("fails when the user doesn't have write permissions", func(t *testing.T) { + s := setting.FeatureMgmtSettings{ + AllowEditing: true, + UpdateWebhook: "random", + } + features := featuremgmt.WithFeatureManager(s, []*featuremgmt.FeatureFlag{{ + // Add this here to ensure the feature works as expected during tests + Name: featuremgmt.FlagFeatureToggleAdminPage, + Stage: featuremgmt.FeatureStageGeneralAvailability, + }}) + + b := NewFeatureFlagAPIBuilder(features, actest.FakeAccessControl{ExpectedEvaluate: false}, &setting.Cfg{}) + msg := callPatchWith(t, b, v0alpha1.ResolvedToggleState{}, http.StatusUnauthorized) + assert.Equal(t, "missing write permission", msg) + }) + + t.Run("fails when update toggle url is not set", func(t *testing.T) { + s := setting.FeatureMgmtSettings{ + AllowEditing: true, + } + b := newTestAPIBuilder(t, nil, []string{}, s) + msg := callPatchWith(t, b, v0alpha1.ResolvedToggleState{}, http.StatusForbidden) + assert.Equal(t, "feature toggles are read-only", msg) + }) + + t.Run("fails with non-existent toggle", func(t *testing.T) { + features := []*featuremgmt.FeatureFlag{ + { + Name: "toggle1", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, { + Name: "toggle2", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, + } + disabled := []string{"toggle2"} + update := v0alpha1.ResolvedToggleState{ + Enabled: map[string]bool{ + "toggle3": true, + }, + } + + s := setting.FeatureMgmtSettings{ + AllowEditing: true, + UpdateWebhook: "random", + } + b := newTestAPIBuilder(t, features, disabled, s) + msg := callPatchWith(t, b, update, http.StatusBadRequest) + assert.Equal(t, "invalid toggle passed in", msg) + }) + + t.Run("fails with read-only toggles", func(t *testing.T) { + features := []*featuremgmt.FeatureFlag{ + { + Name: featuremgmt.FlagFeatureToggleAdminPage, + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, { + Name: "toggle2", + Stage: featuremgmt.FeatureStagePublicPreview, + }, { + Name: "toggle3", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, + } + disabled := []string{"toggle2", "toggle3"} + + s := setting.FeatureMgmtSettings{ + AllowEditing: true, + UpdateWebhook: "random", + ReadOnlyToggles: map[string]struct{}{ + "toggle3": {}, + }, + } + + t.Run("because it is the feature toggle admin page toggle", func(t *testing.T) { + update := v0alpha1.ResolvedToggleState{ + Enabled: map[string]bool{ + featuremgmt.FlagFeatureToggleAdminPage: true, + }, + } + b := newTestAPIBuilder(t, features, disabled, s) + callPatchWith(t, b, update, http.StatusNotModified) + }) + + t.Run("because it is not GA or Deprecated", func(t *testing.T) { + update := v0alpha1.ResolvedToggleState{ + Enabled: map[string]bool{ + "toggle2": true, + }, + } + b := newTestAPIBuilder(t, features, disabled, s) + msg := callPatchWith(t, b, update, http.StatusBadRequest) + assert.Equal(t, "invalid toggle passed in", msg) + }) + + t.Run("because it is configured to be read-only", func(t *testing.T) { + update := v0alpha1.ResolvedToggleState{ + Enabled: map[string]bool{ + "toggle2": true, + }, + } + b := newTestAPIBuilder(t, features, disabled, s) + msg := callPatchWith(t, b, update, http.StatusBadRequest) + assert.Equal(t, "invalid toggle passed in", msg) + }) + }) + + t.Run("when all conditions met", func(t *testing.T) { + features := []*featuremgmt.FeatureFlag{ + { + Name: featuremgmt.FlagFeatureToggleAdminPage, + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, { + Name: "toggle2", + Stage: featuremgmt.FeatureStagePublicPreview, + }, { + Name: "toggle3", + Stage: featuremgmt.FeatureStageGeneralAvailability, + }, { + Name: "toggle4", + Stage: featuremgmt.FeatureStageGeneralAvailability, + AllowSelfServe: true, + }, { + Name: "toggle5", + Stage: featuremgmt.FeatureStageDeprecated, + AllowSelfServe: true, + }, + } + disabled := []string{"toggle2", "toggle3", "toggle4"} + + s := setting.FeatureMgmtSettings{ + AllowEditing: true, + UpdateWebhook: "random", + UpdateWebhookToken: "token", + ReadOnlyToggles: map[string]struct{}{ + "toggle3": {}, + }, + } + + update := v0alpha1.ResolvedToggleState{ + Enabled: map[string]bool{ + "toggle4": true, + "toggle5": false, + }, + } + t.Run("fail when webhook request is not successful", func(t *testing.T) { + webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer webhookServer.Close() + s.UpdateWebhook = webhookServer.URL + + b := newTestAPIBuilder(t, features, disabled, s) + msg := callPatchWith(t, b, update, http.StatusInternalServerError) + assert.Equal(t, "an error occurred while updating feeature toggles", msg) + }) + + t.Run("succeed when webhook request is not successful but app is in dev mode", func(t *testing.T) { + webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + })) + defer webhookServer.Close() + s.UpdateWebhook = webhookServer.URL + + b := newTestAPIBuilder(t, features, disabled, s) + b.cfg.Env = setting.Dev + callPatchWith(t, b, update, http.StatusOK) + }) + + t.Run("succeed when webhook request is successful", func(t *testing.T) { + webhookServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer "+s.UpdateWebhookToken, r.Header.Get("Authorization")) + + var req featuremgmt.FeatureToggleWebhookPayload + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + + assert.Equal(t, "true", req.FeatureToggles["toggle4"]) + assert.Equal(t, "false", req.FeatureToggles["toggle5"]) + w.WriteHeader(http.StatusOK) + })) + defer webhookServer.Close() + s.UpdateWebhook = webhookServer.URL + + b := newTestAPIBuilder(t, features, disabled, s) + msg := callPatchWith(t, b, update, http.StatusOK) + assert.Equal(t, "feature toggles updated successfully", msg) + }) + }) +} + +func findResult(t *testing.T, result v0alpha1.ResolvedToggleState, name string) (v0alpha1.ToggleStatus, bool) { + t.Helper() + + for _, t := range result.Toggles { + if t.Name == name { + return t, true + } + } + return v0alpha1.ToggleStatus{}, false +} + +func callGetWith(t *testing.T, b *FeatureFlagAPIBuilder, expectedCode int) v0alpha1.ResolvedToggleState { + w := response.CreateNormalResponse(http.Header{}, []byte{}, 0) + req := &http.Request{ + Method: "GET", + Header: http.Header{}, + } + req.Header.Add("content-type", "application/json") + req = req.WithContext(appcontext.WithUser(req.Context(), &user.SignedInUser{})) + b.handleCurrentStatus(w, req) + + rts := v0alpha1.ResolvedToggleState{} + require.NoError(t, json.Unmarshal(w.Body(), &rts)) + require.Equal(t, expectedCode, w.Status()) + + // Tests don't expect the feature toggle admin page feature to be present, so remove them from the resolved toggle state + for i, t := range rts.Toggles { + if t.Name == "featureToggleAdminPage" { + rts.Toggles = append(rts.Toggles[0:i], rts.Toggles[i+1:]...) + } + } + + return rts +} + +func callPatchWith(t *testing.T, b *FeatureFlagAPIBuilder, update v0alpha1.ResolvedToggleState, expectedCode int) string { + w := response.CreateNormalResponse(http.Header{}, []byte{}, 0) + + body, err := json.Marshal(update) + require.NoError(t, err) + + req := &http.Request{ + Method: "PATCH", + Body: io.NopCloser(bytes.NewReader(body)), + Header: http.Header{}, + } + req.Header.Add("content-type", "application/json") + req = req.WithContext(appcontext.WithUser(req.Context(), &user.SignedInUser{})) + b.handleCurrentStatus(w, req) + + require.NotNil(t, w.Body()) + require.Equal(t, expectedCode, w.Status()) + + // Extract the public facing message if this is an error + if w.Status() > 399 { + res := map[string]any{} + require.NoError(t, json.Unmarshal(w.Body(), &res)) + + return res["message"].(string) + } + + return string(w.Body()) +} + +func newTestAPIBuilder( + t *testing.T, + serverFeatures []*featuremgmt.FeatureFlag, + disabled []string, // the flags that are disabled + settings setting.FeatureMgmtSettings, +) *FeatureFlagAPIBuilder { + t.Helper() + features := featuremgmt.WithFeatureManager(settings, append([]*featuremgmt.FeatureFlag{{ + // Add this here to ensure the feature works as expected during tests + Name: featuremgmt.FlagFeatureToggleAdminPage, + Stage: featuremgmt.FeatureStageGeneralAvailability, + }}, serverFeatures...), disabled...) + + return NewFeatureFlagAPIBuilder(features, actest.FakeAccessControl{ExpectedEvaluate: true}, &setting.Cfg{}) +} diff --git a/pkg/registry/apis/featuretoggle/register.go b/pkg/registry/apis/featuretoggle/register.go index f5ae2466c42..f04eb82bbec 100644 --- a/pkg/registry/apis/featuretoggle/register.go +++ b/pkg/registry/apis/featuretoggle/register.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/apiserver/builder" "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/setting" ) var _ builder.APIGroupBuilder = (*FeatureFlagAPIBuilder)(nil) @@ -27,17 +28,19 @@ var gv = v0alpha1.SchemeGroupVersion type FeatureFlagAPIBuilder struct { features *featuremgmt.FeatureManager accessControl accesscontrol.AccessControl + cfg *setting.Cfg } -func NewFeatureFlagAPIBuilder(features *featuremgmt.FeatureManager, accessControl accesscontrol.AccessControl) *FeatureFlagAPIBuilder { - return &FeatureFlagAPIBuilder{features, accessControl} +func NewFeatureFlagAPIBuilder(features *featuremgmt.FeatureManager, accessControl accesscontrol.AccessControl, cfg *setting.Cfg) *FeatureFlagAPIBuilder { + return &FeatureFlagAPIBuilder{features, accessControl, cfg} } func RegisterAPIService(features *featuremgmt.FeatureManager, accessControl accesscontrol.AccessControl, apiregistration builder.APIRegistrar, + cfg *setting.Cfg, ) *FeatureFlagAPIBuilder { - builder := NewFeatureFlagAPIBuilder(features, accessControl) + builder := NewFeatureFlagAPIBuilder(features, accessControl, cfg) apiregistration.RegisterAPI(builder) return builder } diff --git a/pkg/services/apiserver/standalone/factory.go b/pkg/services/apiserver/standalone/factory.go index 0768109a0d1..eec8ddacb1a 100644 --- a/pkg/services/apiserver/standalone/factory.go +++ b/pkg/services/apiserver/standalone/factory.go @@ -79,6 +79,7 @@ func (p *DummyAPIFactory) MakeAPIServer(gv schema.GroupVersion) (builder.APIGrou return featuretoggle.NewFeatureFlagAPIBuilder( featuremgmt.WithFeatureManager(setting.FeatureMgmtSettings{}, nil), // none... for now &actest.FakeAccessControl{ExpectedEvaluate: false}, + &setting.Cfg{}, ), nil case "testdata.datasource.grafana.app": diff --git a/pkg/services/featuremgmt/manager.go b/pkg/services/featuremgmt/manager.go index 74ea3350412..457436790a5 100644 --- a/pkg/services/featuremgmt/manager.go +++ b/pkg/services/featuremgmt/manager.go @@ -151,6 +151,7 @@ func (fm *FeatureManager) IsEditableFromAdminPage(key string) bool { return false } return flag.Stage == FeatureStageGeneralAvailability || + flag.Stage == FeatureStagePublicPreview || flag.Stage == FeatureStageDeprecated } diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index b9c8b9ff7ea..cb22273ec35 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -119,7 +119,7 @@ type FeatureFlag struct { Owner codeowner `json:"-"` // Owner person or team that owns this feature flag // Recommended properties - control behavior of the feature toggle management page in the UI - AllowSelfServe bool `json:"allowSelfServe,omitempty"` // allow users with the right privileges to toggle this from the UI (GeneralAvailability and Deprecated toggles only) + AllowSelfServe bool `json:"allowSelfServe,omitempty"` // allow users with the right privileges to toggle this from the UI (GeneralAvailability, PublicPreview, and Deprecated toggles only) HideFromAdminPage bool `json:"hideFromAdminPage,omitempty"` // GA, Deprecated, and PublicPreview toggles only: don't display this feature in the UI; if this is a GA toggle, add a comment with the reasoning // CEL-GO expression. Using the value "true" will mean this is on by default diff --git a/public/app/features/admin/AdminFeatureTogglesAPI.ts b/public/app/features/admin/AdminFeatureTogglesAPI.ts index 62f0c8b5f26..6ecba0c8b7d 100644 --- a/public/app/features/admin/AdminFeatureTogglesAPI.ts +++ b/public/app/features/admin/AdminFeatureTogglesAPI.ts @@ -4,6 +4,7 @@ export type FeatureToggle = { name: string; description?: string; enabled: boolean; + stage: string; readOnly?: boolean; hidden?: boolean; }; @@ -28,6 +29,7 @@ interface K8sToggleSpec { enabled: boolean; writeable: boolean; source: K8sToggleSource; + stage: string; } interface K8sToggleSource { @@ -53,6 +55,7 @@ class K8sAPI implements FeatureTogglesAPI { description: t.description!, enabled: t.enabled, readOnly: !Boolean(t.writeable), + stage: t.stage, hidden: false, // only return visible things })), }; diff --git a/public/app/features/admin/AdminFeatureTogglesPage.tsx b/public/app/features/admin/AdminFeatureTogglesPage.tsx index 4d231b644fd..0684405b3ee 100644 --- a/public/app/features/admin/AdminFeatureTogglesPage.tsx +++ b/public/app/features/admin/AdminFeatureTogglesPage.tsx @@ -10,15 +10,13 @@ import { getTogglesAPI } from './AdminFeatureTogglesAPI'; import { AdminFeatureTogglesTable } from './AdminFeatureTogglesTable'; export default function AdminFeatureTogglesPage() { - const [reload] = useState(1); + const [reload, setReload] = useState(1); const togglesApi = getTogglesAPI(); const featureState = useAsync(() => togglesApi.getFeatureToggles(), [reload]); - const [updateSuccessful, setUpdateSuccessful] = useState(false); const styles = useStyles2(getStyles); const handleUpdateSuccess = () => { - setUpdateSuccessful(true); - // setReload(reload+1); << would trigger updating the server state! + setReload(reload + 1); }; const EditingAlert = () => { @@ -28,7 +26,7 @@ export default function AdminFeatureTogglesPage() {
- {featureState.value?.restartRequired || updateSuccessful + {featureState.value?.restartRequired ? 'A restart is pending for your Grafana instance to apply the latest feature toggle changes' : 'Saving feature toggle changes will prompt a restart of the instance, which may take a few minutes'} @@ -57,7 +55,7 @@ export default function AdminFeatureTogglesPage() { {featureState.error} {featureState.loading && 'Fetching feature toggles'} - {featureState.value?.restartRequired && } + {featureState.value && ( (featureToggles); const [localToggles, setLocalToggles] = useState(featureToggles); const [isSaving, setIsSaving] = useState(false); + const [showSaveModel, setShowSaveModal] = useState(false); const togglesApi = getTogglesAPI(); const handleToggleChange = (toggle: FeatureToggle, newValue: boolean) => { @@ -58,6 +59,14 @@ export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdat } }; + const saveButtonRef = useRef(null); + const showSaveChangesModal = (show: boolean) => () => { + setShowSaveModal(show); + if (!show && saveButtonRef.current) { + saveButtonRef.current.focus(); + } + }; + const getModifiedToggles = (): FeatureToggle[] => { return localToggles.filter((toggle, index) => toggle.enabled !== serverToggles.current[index].enabled); }; @@ -72,11 +81,30 @@ export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdat return 'Feature management is not configured for editing'; } if (readOnlyToggle) { - return 'Preview features are not editable'; + return 'This is a non-editable feature'; } return ''; }; + const getStageCell = (stage: string) => { + switch (stage) { + case 'GA': + return ( + +
GA
+
+ ); + case 'privatePreview': + case 'preview': + case 'experimental': + return 'Beta'; + case 'deprecated': + return 'Deprecated'; + default: + return stage; + } + }; + const columns = [ { id: 'name', @@ -90,20 +118,32 @@ export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdat cell: ({ cell: { value } }: CellProps) =>
{value}
, sortType: sortByDescription, }, + { + id: 'stage', + header: 'Stage', + cell: ({ cell: { value } }: CellProps) =>
{getStageCell(value)}
, + }, { id: 'enabled', header: 'State', - cell: ({ row }: CellProps) => ( - + cell: ({ row }: CellProps) => { + const renderStateSwitch = (
handleToggleChange(row.original, e.currentTarget.checked)} + transparent={row.original.readOnly} />
-
- ), + ); + + return row.original.readOnly ? ( + {renderStateSwitch} + ) : ( + renderStateSwitch + ); + }, sortType: sortByEnabled, }, ]; @@ -112,9 +152,28 @@ export function AdminFeatureTogglesTable({ featureToggles, allowEditing, onUpdat <> {allowEditing && (
- + +

+ Some features are stable (GA) and enabled by default, whereas some are currently in their preliminary + Beta phase, available for early adoption. +

+

We advise understanding the implications of each feature change before making modifications.

+
+ } + confirmText="Save changes" + onConfirm={async () => { + showSaveChangesModal(false)(); + handleSaveChanges(); + }} + onDismiss={showSaveChangesModal(false)} + /> )} featureToggle.name} /> From 7f109c885d673e11718c37a43bc2128823bbda0e Mon Sep 17 00:00:00 2001 From: Kevin Yu Date: Fri, 9 Feb 2024 09:44:29 -0800 Subject: [PATCH 25/50] CloudWatch: Fix code editor not resizing on mount when content height is > 200px (#81911) --- .../MetricsQueryEditor/MathExpressionQueryField.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MathExpressionQueryField.tsx b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MathExpressionQueryField.tsx index 110d8171c10..5ac7ca8e226 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MathExpressionQueryField.tsx +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/MathExpressionQueryField.tsx @@ -29,8 +29,9 @@ export function MathExpressionQueryField({ expression: query, onChange, datasour // We may wish to consider abstracting it into the grafana/ui repo in the future const updateElementHeight = () => { const containerDiv = containerRef.current; - if (containerDiv !== null && editor.getContentHeight() < 200) { - const pixelHeight = Math.max(32, editor.getContentHeight()); + if (containerDiv !== null) { + const maxPixelHeight = Math.min(200, editor.getContentHeight()); + const pixelHeight = Math.max(32, maxPixelHeight); containerDiv.style.height = `${pixelHeight}px`; containerDiv.style.width = '100%'; const pixelWidth = containerDiv.clientWidth; From 77111a07149ded181c0b072b80b609cc3cddbefc Mon Sep 17 00:00:00 2001 From: Alyssa Bull <58453566+alyssabull@users.noreply.github.com> Date: Fri, 9 Feb 2024 11:22:44 -0700 Subject: [PATCH 26/50] Cloud Monitoring: Fix naming and warnings (#82271) --- .../core-plugins-build-and-release.yml | 2 +- package.json | 2 +- pkg/plugins/plugindef/plugindef.cue | 2 +- pkg/tsdb/cloud-monitoring/standalone/main.go | 2 +- .../app/features/plugins/built_in_plugins.ts | 4 +- .../datasource/cloud-monitoring/package.json | 2 +- .../cloud-monitoring/types/query.ts | 12 +-- yarn.lock | 92 +++++++++---------- 8 files changed, 56 insertions(+), 62 deletions(-) diff --git a/.github/workflows/core-plugins-build-and-release.yml b/.github/workflows/core-plugins-build-and-release.yml index a385637e9b8..b8659a726f0 100644 --- a/.github/workflows/core-plugins-build-and-release.yml +++ b/.github/workflows/core-plugins-build-and-release.yml @@ -9,9 +9,9 @@ on: type: choice options: - grafana-azure-monitor-datasource - - grafana-cloud-monitoring-datasource - grafana-testdata-datasource - parca + - stackdriver - tempo concurrency: diff --git a/package.json b/package.json index 2954485b2ff..5e5c5ecdf79 100644 --- a/package.json +++ b/package.json @@ -228,10 +228,10 @@ "@floating-ui/react": "0.26.9", "@glideapps/glide-data-grid": "^6.0.0", "@grafana-plugins/grafana-azure-monitor-datasource": "workspace:*", - "@grafana-plugins/grafana-cloud-monitoring-datasource": "workspace:*", "@grafana-plugins/grafana-pyroscope-datasource": "workspace:*", "@grafana-plugins/grafana-testdata-datasource": "workspace:*", "@grafana-plugins/parca": "workspace:*", + "@grafana-plugins/stackdriver": "workspace:*", "@grafana-plugins/tempo": "workspace:*", "@grafana/aws-sdk": "0.3.1", "@grafana/data": "workspace:*", diff --git a/pkg/plugins/plugindef/plugindef.cue b/pkg/plugins/plugindef/plugindef.cue index 1ddcd19d251..40ab7c25752 100644 --- a/pkg/plugins/plugindef/plugindef.cue +++ b/pkg/plugins/plugindef/plugindef.cue @@ -16,7 +16,7 @@ schemas: [{ // grafana.com, then the plugin `id` has to follow the naming // conventions. id: string & strings.MinRunes(1) - id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|datagrid|gauge|geomap|gettingstarted|graph|heatmap|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|trend|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|grafana-cloud-monitoring-datasource|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|grafana-testdata-datasource|zipkin|phlare|parca)$" + id: =~"^([0-9a-z]+\\-([0-9a-z]+\\-)?(\(strings.Join([ for t in _types {t}], "|"))))|(alertGroups|alertlist|annolist|barchart|bargauge|candlestick|canvas|dashlist|debug|datagrid|gauge|geomap|gettingstarted|graph|heatmap|histogram|icon|live|logs|news|nodeGraph|piechart|pluginlist|stat|state-timeline|status-history|table|table-old|text|timeseries|trend|traces|welcome|xychart|alertmanager|cloudwatch|dashboard|elasticsearch|grafana|grafana-azure-monitor-datasource|stackdriver|graphite|influxdb|jaeger|loki|mixed|mssql|mysql|opentsdb|postgres|prometheus|stackdriver|tempo|grafana-testdata-datasource|zipkin|phlare|parca)$" // An alias is useful when migrating from one plugin id to another (rebranding etc) // This should be used sparingly, and is currently only supported though a hardcoded checklist diff --git a/pkg/tsdb/cloud-monitoring/standalone/main.go b/pkg/tsdb/cloud-monitoring/standalone/main.go index 9d85afc8fb4..c1acbcad13e 100644 --- a/pkg/tsdb/cloud-monitoring/standalone/main.go +++ b/pkg/tsdb/cloud-monitoring/standalone/main.go @@ -16,7 +16,7 @@ func main() { // from Grafana to create different instances of SampleDatasource (per datasource // ID). When datasource configuration changed Dispose method will be called and // new datasource instance created using NewSampleDatasource factory. - if err := datasource.Manage("grafana-cloud-monitoring-datasource", NewDatasource, datasource.ManageOpts{}); err != nil { + if err := datasource.Manage("stackdriver", NewDatasource, datasource.ManageOpts{}); err != nil { log.DefaultLogger.Error(err.Error()) os.Exit(1) } diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index 0287e6686de..d28c68ed1e7 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -30,9 +30,7 @@ const mssqlPlugin = async () => const testDataDSPlugin = async () => await import(/* webpackChunkName: "testDataDSPlugin" */ '@grafana-plugins/grafana-testdata-datasource/module'); const cloudMonitoringPlugin = async () => - await import( - /* webpackChunkName: "cloudMonitoringPlugin" */ '@grafana-plugins/grafana-cloud-monitoring-datasource/module' - ); + await import(/* webpackChunkName: "cloudMonitoringPlugin" */ '@grafana-plugins/stackdriver/module'); const azureMonitorPlugin = async () => await import(/* webpackChunkName: "azureMonitorPlugin" */ '@grafana-plugins/grafana-azure-monitor-datasource/module'); const tempoPlugin = async () => await import(/* webpackChunkName: "tempoPlugin" */ '@grafana-plugins/tempo/module'); diff --git a/public/app/plugins/datasource/cloud-monitoring/package.json b/public/app/plugins/datasource/cloud-monitoring/package.json index 045a67c3c72..80bf3ebabba 100644 --- a/public/app/plugins/datasource/cloud-monitoring/package.json +++ b/public/app/plugins/datasource/cloud-monitoring/package.json @@ -1,5 +1,5 @@ { - "name": "@grafana-plugins/grafana-cloud-monitoring-datasource", + "name": "@grafana-plugins/stackdriver", "description": "Grafana data source for Google Cloud Monitoring", "private": true, "version": "10.4.0-pre", diff --git a/public/app/plugins/datasource/cloud-monitoring/types/query.ts b/public/app/plugins/datasource/cloud-monitoring/types/query.ts index a1e5a2abe29..a202c85bd3e 100644 --- a/public/app/plugins/datasource/cloud-monitoring/types/query.ts +++ b/public/app/plugins/datasource/cloud-monitoring/types/query.ts @@ -1,19 +1,15 @@ import { CloudMonitoringQuery as CloudMonitoringQueryBase, QueryType } from '../dataquery.gen'; export { QueryType }; -export { - TimeSeriesList, - PreprocessorType, +export { PreprocessorType, MetricKind, AlignmentTypes, ValueTypes, MetricFindQueryTypes } from '../dataquery.gen'; +export type { TimeSeriesQuery, SLOQuery, + TimeSeriesList, MetricQuery, - MetricKind, + PromQLQuery, LegacyCloudMonitoringAnnotationQuery, Filter, - AlignmentTypes, - ValueTypes, - MetricFindQueryTypes, - PromQLQuery, } from '../dataquery.gen'; /** diff --git a/yarn.lock b/yarn.lock index e56cafb4d1c..d3316a9625a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3236,51 +3236,6 @@ __metadata: languageName: unknown linkType: soft -"@grafana-plugins/grafana-cloud-monitoring-datasource@workspace:*, @grafana-plugins/grafana-cloud-monitoring-datasource@workspace:public/app/plugins/datasource/cloud-monitoring": - version: 0.0.0-use.local - resolution: "@grafana-plugins/grafana-cloud-monitoring-datasource@workspace:public/app/plugins/datasource/cloud-monitoring" - dependencies: - "@emotion/css": "npm:11.11.2" - "@grafana/data": "npm:10.4.0-pre" - "@grafana/e2e-selectors": "npm:10.4.0-pre" - "@grafana/experimental": "npm:1.7.10" - "@grafana/google-sdk": "npm:0.1.2" - "@grafana/plugin-configs": "npm:10.4.0-pre" - "@grafana/runtime": "npm:10.4.0-pre" - "@grafana/schema": "npm:10.4.0-pre" - "@grafana/ui": "npm:10.4.0-pre" - "@kusto/monaco-kusto": "npm:^7.4.0" - "@testing-library/react": "npm:14.2.1" - "@testing-library/user-event": "npm:14.5.2" - "@types/debounce-promise": "npm:3.1.9" - "@types/jest": "npm:29.5.12" - "@types/lodash": "npm:4.14.202" - "@types/node": "npm:20.11.17" - "@types/prismjs": "npm:1.26.3" - "@types/react": "npm:18.2.55" - "@types/react-test-renderer": "npm:18.0.7" - "@types/testing-library__jest-dom": "npm:5.14.9" - debounce-promise: "npm:3.1.2" - fast-deep-equal: "npm:^3.1.3" - i18next: "npm:^23.0.0" - immer: "npm:10.0.3" - lodash: "npm:4.17.21" - monaco-editor: "npm:0.34.0" - prismjs: "npm:1.29.0" - react: "npm:18.2.0" - react-select-event: "npm:5.5.1" - react-test-renderer: "npm:18.2.0" - react-use: "npm:17.5.0" - rxjs: "npm:7.8.1" - ts-node: "npm:10.9.2" - tslib: "npm:2.6.2" - typescript: "npm:5.3.3" - webpack: "npm:5.90.1" - peerDependencies: - "@grafana/runtime": "*" - languageName: unknown - linkType: soft - "@grafana-plugins/grafana-pyroscope-datasource@workspace:*, @grafana-plugins/grafana-pyroscope-datasource@workspace:public/app/plugins/datasource/grafana-pyroscope-datasource": version: 0.0.0-use.local resolution: "@grafana-plugins/grafana-pyroscope-datasource@workspace:public/app/plugins/datasource/grafana-pyroscope-datasource" @@ -3406,6 +3361,51 @@ __metadata: languageName: unknown linkType: soft +"@grafana-plugins/stackdriver@workspace:*, @grafana-plugins/stackdriver@workspace:public/app/plugins/datasource/cloud-monitoring": + version: 0.0.0-use.local + resolution: "@grafana-plugins/stackdriver@workspace:public/app/plugins/datasource/cloud-monitoring" + dependencies: + "@emotion/css": "npm:11.11.2" + "@grafana/data": "npm:10.4.0-pre" + "@grafana/e2e-selectors": "npm:10.4.0-pre" + "@grafana/experimental": "npm:1.7.10" + "@grafana/google-sdk": "npm:0.1.2" + "@grafana/plugin-configs": "npm:10.4.0-pre" + "@grafana/runtime": "npm:10.4.0-pre" + "@grafana/schema": "npm:10.4.0-pre" + "@grafana/ui": "npm:10.4.0-pre" + "@kusto/monaco-kusto": "npm:^7.4.0" + "@testing-library/react": "npm:14.2.1" + "@testing-library/user-event": "npm:14.5.2" + "@types/debounce-promise": "npm:3.1.9" + "@types/jest": "npm:29.5.12" + "@types/lodash": "npm:4.14.202" + "@types/node": "npm:20.11.17" + "@types/prismjs": "npm:1.26.3" + "@types/react": "npm:18.2.55" + "@types/react-test-renderer": "npm:18.0.7" + "@types/testing-library__jest-dom": "npm:5.14.9" + debounce-promise: "npm:3.1.2" + fast-deep-equal: "npm:^3.1.3" + i18next: "npm:^23.0.0" + immer: "npm:10.0.3" + lodash: "npm:4.17.21" + monaco-editor: "npm:0.34.0" + prismjs: "npm:1.29.0" + react: "npm:18.2.0" + react-select-event: "npm:5.5.1" + react-test-renderer: "npm:18.2.0" + react-use: "npm:17.5.0" + rxjs: "npm:7.8.1" + ts-node: "npm:10.9.2" + tslib: "npm:2.6.2" + typescript: "npm:5.3.3" + webpack: "npm:5.90.1" + peerDependencies: + "@grafana/runtime": "*" + languageName: unknown + linkType: soft + "@grafana-plugins/tempo@workspace:*, @grafana-plugins/tempo@workspace:public/app/plugins/datasource/tempo": version: 0.0.0-use.local resolution: "@grafana-plugins/tempo@workspace:public/app/plugins/datasource/tempo" @@ -17971,10 +17971,10 @@ __metadata: "@floating-ui/react": "npm:0.26.9" "@glideapps/glide-data-grid": "npm:^6.0.0" "@grafana-plugins/grafana-azure-monitor-datasource": "workspace:*" - "@grafana-plugins/grafana-cloud-monitoring-datasource": "workspace:*" "@grafana-plugins/grafana-pyroscope-datasource": "workspace:*" "@grafana-plugins/grafana-testdata-datasource": "workspace:*" "@grafana-plugins/parca": "workspace:*" + "@grafana-plugins/stackdriver": "workspace:*" "@grafana-plugins/tempo": "workspace:*" "@grafana/aws-sdk": "npm:0.3.1" "@grafana/data": "workspace:*" From c9593531ef939f6eac39b7f6d037b67545d2983b Mon Sep 17 00:00:00 2001 From: Matias Chomicki Date: Fri, 9 Feb 2024 19:48:10 +0100 Subject: [PATCH 27/50] Loki query builder: force click in e2e test (#82051) --- e2e/various-suite/loki-query-builder.spec.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/e2e/various-suite/loki-query-builder.spec.ts b/e2e/various-suite/loki-query-builder.spec.ts index a635fff2567..1707a32dd85 100644 --- a/e2e/various-suite/loki-query-builder.spec.ts +++ b/e2e/various-suite/loki-query-builder.spec.ts @@ -43,6 +43,10 @@ describe('Loki query builder', () => { req.reply({ status: 'success', data: ['instance1', 'instance2'] }); }).as('valuesRequest'); + cy.intercept(/index\/stats/, (req) => { + req.reply({ streams: 2, chunks: 2660, bytes: 2721792, entries: 14408 }); + }); + // Go to Explore and choose Loki data source e2e.pages.Explore.visit(); e2e.components.DataSourcePicker.container().should('be.visible').click(); @@ -68,21 +72,20 @@ describe('Loki query builder', () => { // Add labels to remove error e2e.components.QueryBuilder.labelSelect().should('be.visible').click(); // wait until labels are loaded and set on the component before starting to type + e2e.components.QueryBuilder.labelSelect().children('div').children('input').type('i'); cy.wait('@labelsRequest'); - e2e.components.QueryBuilder.labelSelect().children('div').children('input').type('instance{enter}'); + e2e.components.QueryBuilder.labelSelect().children('div').children('input').type('nstance{enter}'); e2e.components.QueryBuilder.matchOperatorSelect() .should('be.visible') - .click() + .click({ force: true }) .children('div') .children('input') .type('=~{enter}', { force: true }); e2e.components.QueryBuilder.valueSelect().should('be.visible').click(); + e2e.components.QueryBuilder.valueSelect().children('div').children('input').type('instance1{enter}'); cy.wait('@valuesRequest'); - e2e.components.QueryBuilder.valueSelect() - .children('div') - .children('input') - .type('instance1{enter}') - .type('instance2{enter}'); + e2e.components.QueryBuilder.valueSelect().children('div').children('input').type('instance2{enter}'); + cy.contains(MISSING_LABEL_FILTER_ERROR_MESSAGE).should('not.exist'); cy.contains(finalQuery).should('be.visible'); From 87c3d0fb6a30508da198228101f45c2dcac30642 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Fri, 9 Feb 2024 13:51:00 -0600 Subject: [PATCH 28/50] K8s: Update stack id validation (#82275) --- .../apiserver/endpoints/request/namespace.go | 6 +++-- .../endpoints/request/namespace_test.go | 23 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/pkg/services/apiserver/endpoints/request/namespace.go b/pkg/services/apiserver/endpoints/request/namespace.go index 34f8a00bceb..899e342dc9f 100644 --- a/pkg/services/apiserver/endpoints/request/namespace.go +++ b/pkg/services/apiserver/endpoints/request/namespace.go @@ -67,10 +67,12 @@ func ParseNamespace(ns string) (NamespaceInfo, error) { } if strings.HasPrefix(ns, "stack-") { - info.StackID = ns[6:] - if len(info.StackID) < 1 { + stackIDStr := ns[6:] + stackID, err := strconv.Atoi(stackIDStr) + if err != nil || stackID < 1 { return info, fmt.Errorf("invalid stack id") } + info.StackID = stackIDStr info.OrgID = 1 return info, nil } diff --git a/pkg/services/apiserver/endpoints/request/namespace_test.go b/pkg/services/apiserver/endpoints/request/namespace_test.go index 86c863e3ef6..f5665ff2f5b 100644 --- a/pkg/services/apiserver/endpoints/request/namespace_test.go +++ b/pkg/services/apiserver/endpoints/request/namespace_test.go @@ -77,15 +77,15 @@ func TestParseNamespace(t *testing.T) { }, }, { - name: "valid stack", + name: "invalid stack id (must be an int)", + expectErr: true, namespace: "stack-abcdef", expected: request.NamespaceInfo{ - OrgID: 1, - StackID: "abcdef", + OrgID: -1, }, }, { - name: "invalid stack id", + name: "invalid stack id (must be provided)", namespace: "stack-", expectErr: true, expected: request.NamespaceInfo{ @@ -93,12 +93,19 @@ func TestParseNamespace(t *testing.T) { }, }, { - name: "invalid stack id (too short)", - namespace: "stack-", + name: "invalid stack id (cannot be 0)", + namespace: "stack-0", expectErr: true, expected: request.NamespaceInfo{ - OrgID: -1, - StackID: "", + OrgID: -1, + }, + }, + { + name: "valid stack", + namespace: "stack-1", + expected: request.NamespaceInfo{ + OrgID: 1, + StackID: "1", }, }, { From b5d14d03d7388894bbee47a93f25707539ff1519 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 9 Feb 2024 21:44:44 +0100 Subject: [PATCH 29/50] Scenes: Upgrade to 2.6.5 and Add query controller DashboardScene (#82232) * Update scenes and add query controller * Update * Update --- package.json | 2 +- .../transformSceneToSaveModel.test.ts.snap | 4 +- .../transformSaveModelToScene.test.ts | 6 +-- .../transformSaveModelToScene.ts | 4 +- .../transformSceneToSaveModel.ts | 8 ++- yarn.lock | 54 +++++++++++++++---- 6 files changed, 60 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 5e5c5ecdf79..5a46e33cfac 100644 --- a/package.json +++ b/package.json @@ -246,7 +246,7 @@ "@grafana/o11y-ds-frontend": "workspace:*", "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", - "@grafana/scenes": "2.6.6", + "@grafana/scenes": "^2.6.5", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap index 60da71a31c6..2dcb29ae4db 100644 --- a/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap +++ b/public/app/features/dashboard-scene/serialization/__snapshots__/transformSceneToSaveModel.test.ts.snap @@ -307,7 +307,7 @@ exports[`transformSceneToSaveModel Given a simple scene with custom settings Sho "description": "My custom description", "editable": false, "fiscalYearStartMonth": 1, - "graphTooltip": 0, + "graphTooltip": 1, "id": 1351, "links": [ { @@ -621,7 +621,7 @@ exports[`transformSceneToSaveModel Given a simple scene with variables Should tr }, "editable": true, "fiscalYearStartMonth": 1, - "graphTooltip": 0, + "graphTooltip": 1, "id": 1351, "links": [], "panels": [ diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts index e0d1a1e6c4f..8e288c8b429 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.test.ts @@ -143,9 +143,9 @@ describe('transformSaveModelToScene', () => { const scene = createDashboardSceneFromDashboardModel(oldModel); - expect(scene.state.$behaviors).toHaveLength(4); - expect(scene.state.$behaviors![1]).toBeInstanceOf(behaviors.CursorSync); - expect((scene.state.$behaviors![1] as behaviors.CursorSync).state.sync).toEqual(DashboardCursorSync.Crosshair); + expect(scene.state.$behaviors).toHaveLength(5); + expect(scene.state.$behaviors![0]).toBeInstanceOf(behaviors.CursorSync); + expect((scene.state.$behaviors![0] as behaviors.CursorSync).state.sync).toEqual(DashboardCursorSync.Crosshair); }); it('should initialize the Dashboard Scene with empty template variables', () => { diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index 1abe51664a8..b1dd16f8499 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -266,10 +266,11 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) }), $variables: variables, $behaviors: [ - registerDashboardMacro, new behaviors.CursorSync({ sync: oldModel.graphTooltip, }), + new behaviors.SceneQueryController(), + registerDashboardMacro, registerDashboardSceneTracking(oldModel), registerPanelInteractionsReporter, ], @@ -287,6 +288,7 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel) new SceneRefreshPicker({ refresh: oldModel.refresh, intervals: oldModel.timepicker.refresh_intervals, + withText: true, }), ], linkControls: new DashboardLinksControls({}), diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts index ee8ee8c3654..1b9e94cd8a3 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts @@ -114,8 +114,12 @@ export function transformSceneToSaveModel(scene: DashboardScene, isSnapshot = fa } } - if (state.$behaviors && state.$behaviors[0] instanceof behaviors.CursorSync) { - graphTooltip = state.$behaviors[0].state.sync; + if (state.$behaviors) { + for (const behavior of state.$behaviors!) { + if (behavior instanceof behaviors.CursorSync) { + graphTooltip = behavior.state.sync; + } + } } const timePickerWithoutDefaults = removeDefaults( diff --git a/yarn.lock b/yarn.lock index d3316a9625a..d42266b7a73 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4011,9 +4011,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:2.6.6": - version: 2.6.6 - resolution: "@grafana/scenes@npm:2.6.6" +"@grafana/scenes@npm:^2.6.5": + version: 2.6.5 + resolution: "@grafana/scenes@npm:2.6.5" dependencies: "@grafana/e2e-selectors": "npm:10.0.2" react-grid-layout: "npm:1.3.4" @@ -4025,7 +4025,7 @@ __metadata: "@grafana/runtime": 10.0.3 "@grafana/schema": 10.0.3 "@grafana/ui": 10.0.3 - checksum: 10/13f5a7d77892ab68af00ee16887bbd101f00e6e2dbb547f697b0f472a296b8030a96f1596c416c24800367f83a469bc10d975d689a0ca8a7cd533efb7ef1bfc3 + checksum: 10/68fe91a5a0c8f80b679126f3525b74b29ce3f9ad92bc558eaaf39693235137348b90796c2b02aa7c1c7929586e60df6024e139993a416ef37d4c875e548dc855 languageName: node linkType: hard @@ -11183,7 +11183,17 @@ __metadata: languageName: node linkType: hard -"anymatch@npm:^3.0.3, anymatch@npm:^3.1.3, anymatch@npm:~3.1.2": +"anymatch@npm:^3.0.3, anymatch@npm:~3.1.2": + version: 3.1.2 + resolution: "anymatch@npm:3.1.2" + dependencies: + normalize-path: "npm:^3.0.0" + picomatch: "npm:^2.0.4" + checksum: 10/985163db2292fac9e5a1e072bf99f1b5baccf196e4de25a0b0b81865ebddeb3b3eb4480734ef0a2ac8c002845396b91aa89121f5b84f93981a4658164a9ec6e9 + languageName: node + linkType: hard + +"anymatch@npm:^3.1.3": version: 3.1.3 resolution: "anymatch@npm:3.1.3" dependencies: @@ -16678,6 +16688,13 @@ __metadata: languageName: node linkType: hard +"fast-fifo@npm:^1.0.0": + version: 1.1.0 + resolution: "fast-fifo@npm:1.1.0" + checksum: 10/895f4c9873a4d5059dfa244aa0dde2b22ee563fd673d85b638869715f92244f9d6469bc0873bcb40554d28c51cbc7590045718462cfda1da503b1c6985815209 + languageName: node + linkType: hard + "fast-fifo@npm:^1.1.0": version: 1.3.2 resolution: "fast-fifo@npm:1.3.2" @@ -16768,7 +16785,7 @@ __metadata: languageName: node linkType: hard -"fastq@npm:^1.13.0, fastq@npm:^1.6.0": +"fastq@npm:^1.13.0": version: 1.17.1 resolution: "fastq@npm:1.17.1" dependencies: @@ -16777,6 +16794,15 @@ __metadata: languageName: node linkType: hard +"fastq@npm:^1.6.0": + version: 1.13.0 + resolution: "fastq@npm:1.13.0" + dependencies: + reusify: "npm:^1.0.4" + checksum: 10/0902cb9b81accf34e5542612c8a1df6c6ea47674f85bcc9cdc38795a28b53e4a096f751cfcf4fb25d2ea42fee5447499ba6cf5af5d0209297e1d1fd4dd551bb6 + languageName: node + linkType: hard + "fault@npm:^1.0.0": version: 1.0.4 resolution: "fault@npm:1.0.4" @@ -17991,7 +18017,7 @@ __metadata: "@grafana/o11y-ds-frontend": "workspace:*" "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" - "@grafana/scenes": "npm:2.6.6" + "@grafana/scenes": "npm:^2.6.5" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^1.3.0-rc1" @@ -25073,7 +25099,7 @@ __metadata: languageName: node linkType: hard -"queue-tick@npm:^1.0.1": +"queue-tick@npm:^1.0.0, queue-tick@npm:^1.0.1": version: 1.0.1 resolution: "queue-tick@npm:1.0.1" checksum: 10/f447926c513b64a857906f017a3b350f7d11277e3c8d2a21a42b7998fa1a613d7a829091e12d142bb668905c8f68d8103416c7197856efb0c72fa835b8e254b5 @@ -28447,7 +28473,7 @@ __metadata: languageName: node linkType: hard -"streamx@npm:^2.12.0, streamx@npm:^2.12.5, streamx@npm:^2.13.2, streamx@npm:^2.14.0": +"streamx@npm:^2.12.0, streamx@npm:^2.13.2, streamx@npm:^2.14.0": version: 2.15.7 resolution: "streamx@npm:2.15.7" dependencies: @@ -28457,6 +28483,16 @@ __metadata: languageName: node linkType: hard +"streamx@npm:^2.12.5": + version: 2.12.5 + resolution: "streamx@npm:2.12.5" + dependencies: + fast-fifo: "npm:^1.0.0" + queue-tick: "npm:^1.0.0" + checksum: 10/daa5789ca31101684d9266f7ea77294908bd3e55607805ac1657f0cef1ee0a1966bc3988d2ec12c5f68a718d481147fa3ace2525486a1e39ca7155c598917cd1 + languageName: node + linkType: hard + "strict-event-emitter@npm:^0.2.4": version: 0.2.8 resolution: "strict-event-emitter@npm:0.2.8" From 5bbe9c6e6117ab5fa585133fdb9a37a736ee9d4a Mon Sep 17 00:00:00 2001 From: Alexander Weaver Date: Fri, 9 Feb 2024 15:53:58 -0600 Subject: [PATCH 30/50] Alerting: Enable group-level rule evaluation jittering by default, remove feature toggle (#82212) * remove jitter feature flag * Add an out so users can manually disable jitter * Pass in cfg * Add TODO to remove knob in future --- conf/defaults.ini | 4 ++++ conf/sample.ini | 4 ++++ .../configure-grafana/feature-toggles/index.md | 1 - .../grafana-data/src/types/featureToggles.gen.ts | 1 - pkg/services/featuremgmt/registry.go | 12 ------------ pkg/services/featuremgmt/toggles_gen.csv | 1 - pkg/services/featuremgmt/toggles_gen.go | 4 ---- pkg/services/ngalert/ngalert.go | 2 +- pkg/services/ngalert/schedule/jitter.go | 11 ++++++----- pkg/setting/setting_unified_alerting.go | 5 +++++ 10 files changed, 20 insertions(+), 25 deletions(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index c15a21eb6e6..8bcb30a11e5 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -1203,6 +1203,10 @@ max_state_save_concurrency = 1 # The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. state_periodic_save_interval = 5m +# Disables the smoothing of alert evaluations across their evaluation window. +# Rules will evaluate in sync. +disable_jitter = false + [unified_alerting.screenshots] # Enable screenshots in notifications. You must have either installed the Grafana image rendering # plugin, or set up Grafana to use a remote rendering service. diff --git a/conf/sample.ini b/conf/sample.ini index 17ad690d76e..e938b40ff58 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -1130,6 +1130,10 @@ # The interval string is a possibly signed sequence of decimal numbers, followed by a unit suffix (ms, s, m, h, d), e.g. 30s or 1m. ;state_periodic_save_interval = 5m +# Disables the smoothing of alert evaluations across their evaluation window. +# Rules will evaluate in sync. +;disable_jitter = false + [unified_alerting.reserved_labels] # Comma-separated list of reserved labels added by the Grafana Alerting engine that should be disabled. # For example: `disabled_labels=grafana_folder` diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index d55ddae6997..6cda1d14fe9 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -55,7 +55,6 @@ Some features are enabled by default. You can disable these feature by setting t | `lokiQueryHints` | Enables query hints for Loki | Yes | | `alertingPreviewUpgrade` | Show Unified Alerting preview and upgrade page in legacy alerting | Yes | | `alertingQueryOptimization` | Optimizes eligible queries in order to reduce load on datasources | | -| `jitterAlertRules` | Distributes alert rule evaluations more evenly over time, by rule group | | ## Preview feature toggles diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 90dfc96b506..b87e766734a 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -168,7 +168,6 @@ export interface FeatureToggles { cloudRBACRoles?: boolean; alertingQueryOptimization?: boolean; newFolderPicker?: boolean; - jitterAlertRules?: boolean; jitterAlertRulesWithinGroups?: boolean; onPremToCloudMigrations?: boolean; alertingSaveStatePeriodic?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 97b62d848ee..56db4acd0dd 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1265,18 +1265,6 @@ var ( FrontendOnly: true, Created: time.Date(2024, time.January, 12, 12, 0, 0, 0, time.UTC), }, - { - Name: "jitterAlertRules", - Description: "Distributes alert rule evaluations more evenly over time, by rule group", - FrontendOnly: false, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaAlertingSquad, - AllowSelfServe: false, - HideFromDocs: false, - HideFromAdminPage: false, - RequiresRestart: true, - Created: time.Date(2024, time.January, 17, 12, 0, 0, 0, time.UTC), - }, { Name: "jitterAlertRulesWithinGroups", Description: "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 40fdc057987..bcd896e222a 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -149,7 +149,6 @@ enablePluginsTracingByDefault,experimental,@grafana/plugins-platform-backend,202 cloudRBACRoles,experimental,@grafana/identity-access-team,2024-01-10,false,true,false alertingQueryOptimization,GA,@grafana/alerting-squad,2024-01-10,false,false,false newFolderPicker,experimental,@grafana/grafana-frontend-platform,2024-01-12,false,false,true -jitterAlertRules,GA,@grafana/alerting-squad,2024-01-17,false,true,false jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,2024-01-17,false,true,false onPremToCloudMigrations,experimental,@grafana/grafana-operator-experience-squad,2024-01-22,false,false,false alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,2024-01-22,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 5be1a1a1cc7..4707f9d8afb 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -607,10 +607,6 @@ const ( // Enables the nested folder picker without having nested folders enabled FlagNewFolderPicker = "newFolderPicker" - // FlagJitterAlertRules - // Distributes alert rule evaluations more evenly over time, by rule group - FlagJitterAlertRules = "jitterAlertRules" - // FlagJitterAlertRulesWithinGroups // Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group FlagJitterAlertRulesWithinGroups = "jitterAlertRulesWithinGroups" diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 1b9b1596235..d75f5d91960 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -274,7 +274,7 @@ func (ng *AlertNG) init() error { BaseInterval: ng.Cfg.UnifiedAlerting.BaseInterval, MinRuleInterval: ng.Cfg.UnifiedAlerting.MinInterval, DisableGrafanaFolder: ng.Cfg.UnifiedAlerting.ReservedLabels.IsReservedLabelDisabled(models.FolderTitleLabel), - JitterEvaluations: schedule.JitterStrategyFrom(ng.FeatureToggles), + JitterEvaluations: schedule.JitterStrategyFrom(ng.Cfg.UnifiedAlerting, ng.FeatureToggles), AppURL: appUrl, EvaluatorFactory: evalFactory, RuleStore: ng.store, diff --git a/pkg/services/ngalert/schedule/jitter.go b/pkg/services/ngalert/schedule/jitter.go index 0db59e567ee..61adadb1286 100644 --- a/pkg/services/ngalert/schedule/jitter.go +++ b/pkg/services/ngalert/schedule/jitter.go @@ -7,6 +7,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/services/featuremgmt" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/setting" ) // JitterStrategy represents a modifier to alert rule timing that affects how evaluations are distributed. @@ -19,14 +20,14 @@ const ( ) // JitterStrategyFrom returns the JitterStrategy indicated by the current Grafana feature toggles. -func JitterStrategyFrom(toggles featuremgmt.FeatureToggles) JitterStrategy { - strategy := JitterNever +func JitterStrategyFrom(cfg setting.UnifiedAlertingSettings, toggles featuremgmt.FeatureToggles) JitterStrategy { + strategy := JitterByGroup + if cfg.DisableJitter { + return JitterNever + } if toggles == nil { return strategy } - if toggles.IsEnabledGlobally(featuremgmt.FlagJitterAlertRules) { - strategy = JitterByGroup - } if toggles.IsEnabledGlobally(featuremgmt.FlagJitterAlertRulesWithinGroups) { strategy = JitterByRule } diff --git a/pkg/setting/setting_unified_alerting.go b/pkg/setting/setting_unified_alerting.go index ca27fb228ca..b42f0869fee 100644 --- a/pkg/setting/setting_unified_alerting.go +++ b/pkg/setting/setting_unified_alerting.go @@ -83,6 +83,7 @@ type UnifiedAlertingSettings struct { MaxAttempts int64 MinInterval time.Duration EvaluationTimeout time.Duration + DisableJitter bool ExecuteAlerts bool DefaultConfiguration string Enabled *bool // determines whether unified alerting is enabled. If it is nil then user did not define it and therefore its value will be determined during migration. Services should not use it directly. @@ -300,6 +301,10 @@ func (cfg *Cfg) ReadUnifiedAlertingSettings(iniFile *ini.File) error { uaCfg.BaseInterval = SchedulerBaseInterval + // TODO: This was promoted from a feature toggle and is now the default behavior. + // We can consider removing the knob entirely in a release after 10.4. + uaCfg.DisableJitter = ua.Key("disable_jitter").MustBool(false) + // The base interval of the scheduler for evaluating alerts. // 1. It is used by the internal scheduler's timer to tick at this interval. // 2. to spread evaluations of rules that need to be evaluated at the current tick T. In other words, the evaluation of rules at the tick T will be evenly spread in the interval from T to T+scheduler_tick_interval. From 14869cc4004a70eb61e5cc62c415972738610d6f Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Fri, 9 Feb 2024 17:25:15 -0500 Subject: [PATCH 31/50] Docs: Update developer dependencies (#82034) --- contribute/developer-guide.md | 1 + 1 file changed, 1 insertion(+) diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index c07a242caff..52923577d1a 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -9,6 +9,7 @@ Make sure you have the following dependencies installed before setting up your d - [Git](https://git-scm.com/) - [Go](https://golang.org/dl/) (see [go.mod](../go.mod#L3) for minimum required version) - [Node.js (Long Term Support)](https://nodejs.org), with [corepack enabled](https://nodejs.org/api/corepack.html#enabling-the-feature) +- GCC (required for Cgo dependencies) ### macOS From ce910a7eb243d3a88fbec60b5b3212e4edb8ba63 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 9 Feb 2024 15:34:12 -0800 Subject: [PATCH 32/50] FeatureFlags: manage creation/modification times automatically (#82131) --- pkg/registry/apis/featuretoggle/features.go | 65 +- pkg/registry/apis/featuretoggle/register.go | 2 +- pkg/services/featuremgmt/models.go | 4 +- pkg/services/featuremgmt/registry.go | 175 +- pkg/services/featuremgmt/toggles_gen.csv | 316 +-- pkg/services/featuremgmt/toggles_gen.json | 2028 ++++++++++++++++++ pkg/services/featuremgmt/toggles_gen_test.go | 140 +- 7 files changed, 2336 insertions(+), 394 deletions(-) create mode 100644 pkg/services/featuremgmt/toggles_gen.json diff --git a/pkg/registry/apis/featuretoggle/features.go b/pkg/registry/apis/featuretoggle/features.go index 23808272b73..37d8352a0c3 100644 --- a/pkg/registry/apis/featuretoggle/features.go +++ b/pkg/registry/apis/featuretoggle/features.go @@ -3,7 +3,7 @@ package featuretoggle import ( "context" "fmt" - "time" + "sync" "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -27,18 +27,16 @@ var ( type featuresStorage struct { resource *common.ResourceInfo tableConverter rest.TableConvertor - features []featuremgmt.FeatureFlag - startup int64 + features *v0alpha1.FeatureList + featuresOnce sync.Once } // NOTE! this does not depend on config or any system state! // In the future, the existence of features (and their properties) can be defined dynamically -func NewFeaturesStorage(features []featuremgmt.FeatureFlag) *featuresStorage { +func NewFeaturesStorage() *featuresStorage { resourceInfo := v0alpha1.FeatureResourceInfo return &featuresStorage{ - startup: time.Now().UnixMilli(), resource: &resourceInfo, - features: features, tableConverter: utils.NewTableConverter( resourceInfo.GroupResource(), []metav1.TableColumnDefinition{ @@ -82,44 +80,35 @@ func (s *featuresStorage) ConvertToTable(ctx context.Context, object runtime.Obj return s.tableConverter.ConvertToTable(ctx, object, tableOptions) } +func (s *featuresStorage) init() { + s.featuresOnce.Do(func() { + rv := "1" + features, _ := featuremgmt.GetEmbeddedFeatureList() + for _, feature := range features.Items { + if feature.ResourceVersion > rv { + rv = feature.ResourceVersion + } + } + features.ResourceVersion = rv + s.features = &features + }) +} + func (s *featuresStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) { - flags := &v0alpha1.FeatureList{ - ListMeta: metav1.ListMeta{ - ResourceVersion: fmt.Sprintf("%d", s.startup), - }, + s.init() + if s.features == nil { + return nil, fmt.Errorf("error loading embedded features") } - for _, flag := range s.features { - flags.Items = append(flags.Items, toK8sForm(flag)) - } - return flags, nil + return s.features, nil } func (s *featuresStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { - for _, flag := range s.features { - if name == flag.Name { - obj := toK8sForm(flag) - return &obj, nil + s.init() + + for idx, flag := range s.features.Items { + if flag.Name == name { + return &s.features.Items[idx], nil } } return nil, fmt.Errorf("not found") } - -func toK8sForm(flag featuremgmt.FeatureFlag) v0alpha1.Feature { - return v0alpha1.Feature{ - ObjectMeta: metav1.ObjectMeta{ - Name: flag.Name, - CreationTimestamp: metav1.NewTime(flag.Created), - }, - Spec: v0alpha1.FeatureSpec{ - Description: flag.Description, - Stage: flag.Stage.String(), - Owner: string(flag.Owner), - AllowSelfServe: flag.AllowSelfServe, - HideFromAdminPage: flag.HideFromAdminPage, - HideFromDocs: flag.HideFromDocs, - FrontendOnly: flag.FrontendOnly, - RequiresDevMode: flag.RequiresDevMode, - RequiresRestart: flag.RequiresRestart, - }, - } -} diff --git a/pkg/registry/apis/featuretoggle/register.go b/pkg/registry/apis/featuretoggle/register.go index f04eb82bbec..205f3a25e0a 100644 --- a/pkg/registry/apis/featuretoggle/register.go +++ b/pkg/registry/apis/featuretoggle/register.go @@ -86,7 +86,7 @@ func (b *FeatureFlagAPIBuilder) GetAPIGroupInfo( ) (*genericapiserver.APIGroupInfo, error) { apiGroupInfo := genericapiserver.NewDefaultAPIGroupInfo(v0alpha1.GROUP, scheme, metav1.ParameterCodec, codecs) - featureStore := NewFeaturesStorage(b.features.GetFlags()) + featureStore := NewFeaturesStorage() toggleStore := NewTogglesStorage(b.features) storage := map[string]rest.Storage{} diff --git a/pkg/services/featuremgmt/models.go b/pkg/services/featuremgmt/models.go index cb22273ec35..6260547585b 100644 --- a/pkg/services/featuremgmt/models.go +++ b/pkg/services/featuremgmt/models.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "time" ) type FeatureToggles interface { @@ -115,8 +114,7 @@ type FeatureFlag struct { Name string `json:"name" yaml:"name"` // Unique name Description string `json:"description"` Stage FeatureFlagStage `json:"stage,omitempty"` - Created time.Time `json:"created,omitempty"` // when the flag was introduced - Owner codeowner `json:"-"` // Owner person or team that owns this feature flag + Owner codeowner `json:"-"` // Owner person or team that owns this feature flag // Recommended properties - control behavior of the feature toggle management page in the UI AllowSelfServe bool `json:"allowSelfServe,omitempty"` // allow users with the right privileges to toggle this from the UI (GeneralAvailability, PublicPreview, and Deprecated toggles only) diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 56db4acd0dd..6d13d0bf0a0 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -7,7 +7,10 @@ package featuremgmt import ( - "time" + "embed" + "encoding/json" + + featuretoggle "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" ) var ( @@ -20,7 +23,6 @@ var ( Owner: grafanaAsCodeSquad, HideFromAdminPage: true, AllowSelfServe: false, - Created: time.Date(2022, time.May, 24, 12, 0, 0, 0, time.UTC), }, { Name: "live-service-web-worker", @@ -28,7 +30,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaAppPlatformSquad, - Created: time.Date(2021, time.November, 9, 12, 0, 0, 0, time.UTC), }, { Name: "queryOverLive", @@ -36,7 +37,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaAppPlatformSquad, - Created: time.Date(2022, time.January, 5, 12, 0, 0, 0, time.UTC), }, { Name: "panelTitleSearch", @@ -44,7 +44,6 @@ var ( Stage: FeatureStagePublicPreview, Owner: grafanaAppPlatformSquad, HideFromAdminPage: true, - Created: time.Date(2022, time.February, 15, 12, 0, 0, 0, time.UTC), }, { Name: "publicDashboards", @@ -53,7 +52,6 @@ var ( Owner: grafanaSharingSquad, Expression: "true", // enabled by default AllowSelfServe: true, - Created: time.Date(2022, time.April, 7, 12, 0, 0, 0, time.UTC), }, { Name: "publicDashboardsEmailSharing", @@ -62,14 +60,12 @@ var ( Owner: grafanaSharingSquad, HideFromDocs: true, HideFromAdminPage: true, - Created: time.Date(2022, time.December, 21, 12, 0, 0, 0, time.UTC), }, { Name: "lokiExperimentalStreaming", Description: "Support new streaming approach for loki (prototype, needs special loki build)", Stage: FeatureStageExperimental, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.June, 19, 12, 0, 0, 0, time.UTC), }, { Name: "featureHighlights", @@ -77,21 +73,18 @@ var ( Stage: FeatureStageGeneralAvailability, Owner: grafanaAsCodeSquad, AllowSelfServe: true, - Created: time.Date(2022, time.February, 3, 12, 0, 0, 0, time.UTC), }, { Name: "migrationLocking", Description: "Lock database during migrations", Stage: FeatureStagePublicPreview, Owner: grafanaBackendPlatformSquad, - Created: time.Date(2022, time.February, 15, 12, 0, 0, 0, time.UTC), }, { Name: "storage", Description: "Configurable storage for dashboards, datasources, and resources", Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, - Created: time.Date(2022, time.March, 17, 12, 0, 0, 0, time.UTC), }, { Name: "correlations", @@ -99,7 +92,6 @@ var ( Stage: FeatureStageGeneralAvailability, Owner: grafanaExploreSquad, AllowSelfServe: true, - Created: time.Date(2022, time.September, 16, 12, 0, 0, 0, time.UTC), }, { Name: "exploreContentOutline", @@ -109,14 +101,12 @@ var ( Expression: "true", // enabled by default FrontendOnly: true, AllowSelfServe: true, - Created: time.Date(2023, time.November, 3, 12, 0, 0, 0, time.UTC), }, { Name: "datasourceQueryMultiStatus", Description: "Introduce HTTP 207 Multi Status for api/ds/query", Stage: FeatureStageExperimental, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2022, time.May, 3, 12, 0, 0, 0, time.UTC), }, { Name: "traceToMetrics", @@ -124,7 +114,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityTracesAndProfilingSquad, - Created: time.Date(2022, time.March, 7, 12, 0, 0, 0, time.UTC), }, { Name: "autoMigrateOldPanels", @@ -132,7 +121,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaDatavizSquad, - Created: time.Date(2022, time.June, 11, 12, 0, 0, 0, time.UTC), }, { Name: "autoMigrateGraphPanel", @@ -140,7 +128,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaDatavizSquad, - Created: time.Date(2023, time.December, 11, 7, 0, 0, 0, time.UTC), }, { Name: "disableAngular", @@ -149,7 +136,6 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, HideFromAdminPage: true, - Created: time.Date(2023, time.March, 23, 12, 0, 0, 0, time.UTC), }, { Name: "canvasPanelNesting", @@ -158,7 +144,6 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, HideFromAdminPage: true, - Created: time.Date(2022, time.May, 31, 12, 0, 0, 0, time.UTC), }, { Name: "newVizTooltips", @@ -167,7 +152,6 @@ var ( FrontendOnly: true, Owner: grafanaDatavizSquad, AllowSelfServe: false, - Created: time.Date(2023, time.November, 3, 12, 0, 0, 0, time.UTC), }, { Name: "scenes", @@ -175,7 +159,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaDashboardsSquad, - Created: time.Date(2022, time.July, 7, 12, 0, 0, 0, time.UTC), }, { Name: "disableSecretsCompatibility", @@ -183,14 +166,12 @@ var ( Stage: FeatureStageExperimental, RequiresRestart: true, Owner: hostedGrafanaTeam, - Created: time.Date(2022, time.July, 13, 12, 0, 0, 0, time.UTC), }, { Name: "logRequestsInstrumentedAsUnknown", Description: "Logs the path for requests that are instrumented as unknown", Stage: FeatureStageExperimental, Owner: hostedGrafanaTeam, - Created: time.Date(2022, time.June, 10, 12, 0, 0, 0, time.UTC), }, { Name: "dataConnectionsConsole", @@ -199,7 +180,6 @@ var ( Expression: "true", // turned on by default Owner: grafanaPluginsPlatformSquad, AllowSelfServe: true, - Created: time.Date(2022, time.June, 1, 12, 0, 0, 0, time.UTC), }, { // Some plugins rely on topnav feature flag being enabled, so we cannot remove this until we @@ -209,7 +189,6 @@ var ( Stage: FeatureStageDeprecated, Expression: "true", // enabled by default Owner: grafanaFrontendPlatformSquad, - Created: time.Date(2022, time.June, 20, 12, 0, 0, 0, time.UTC), }, { Name: "returnToPrevious", @@ -217,7 +196,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaFrontendPlatformSquad, - Created: time.Date(2024, time.January, 9, 11, 0, 0, 0, time.UTC), }, { Name: "grpcServer", @@ -225,7 +203,6 @@ var ( Stage: FeatureStagePublicPreview, Owner: grafanaAppPlatformSquad, HideFromAdminPage: true, - Created: time.Date(2022, time.September, 27, 12, 0, 0, 0, time.UTC), }, { Name: "unifiedStorage", @@ -234,7 +211,6 @@ var ( RequiresDevMode: true, RequiresRestart: true, // new SQL tables created Owner: grafanaAppPlatformSquad, - Created: time.Date(2022, time.December, 1, 12, 0, 0, 0, time.UTC), }, { Name: "cloudWatchCrossAccountQuerying", @@ -243,7 +219,6 @@ var ( Expression: "true", // enabled by default Owner: awsDatasourcesSquad, AllowSelfServe: true, - Created: time.Date(2022, time.November, 28, 12, 0, 0, 0, time.UTC), }, { Name: "redshiftAsyncQueryDataSupport", @@ -252,7 +227,6 @@ var ( Expression: "true", // enabled by default Owner: awsDatasourcesSquad, AllowSelfServe: false, - Created: time.Date(2022, time.August, 27, 12, 0, 0, 0, time.UTC), }, { Name: "athenaAsyncQueryDataSupport", @@ -262,21 +236,18 @@ var ( FrontendOnly: true, Owner: awsDatasourcesSquad, AllowSelfServe: false, - Created: time.Date(2022, time.August, 27, 12, 0, 0, 0, time.UTC), }, { Name: "showDashboardValidationWarnings", Description: "Show warnings when dashboards do not validate against the schema", Stage: FeatureStageExperimental, Owner: grafanaDashboardsSquad, - Created: time.Date(2022, time.October, 14, 12, 0, 0, 0, time.UTC), }, { Name: "mysqlAnsiQuotes", Description: "Use double quotes to escape keyword in a MySQL query", Stage: FeatureStageExperimental, Owner: grafanaBackendPlatformSquad, - Created: time.Date(2022, time.October, 12, 12, 0, 0, 0, time.UTC), }, { Name: "accessControlOnCall", @@ -284,14 +255,12 @@ var ( Stage: FeatureStagePublicPreview, Owner: identityAccessTeam, HideFromAdminPage: true, - Created: time.Date(2022, time.October, 19, 12, 0, 0, 0, time.UTC), }, { Name: "nestedFolders", Description: "Enable folder nesting", Stage: FeatureStagePublicPreview, Owner: grafanaBackendPlatformSquad, - Created: time.Date(2022, time.October, 22, 12, 0, 0, 0, time.UTC), }, { Name: "nestedFolderPicker", @@ -301,14 +270,12 @@ var ( FrontendOnly: true, Expression: "true", // enabled by default AllowSelfServe: true, - Created: time.Date(2023, time.July, 24, 12, 0, 0, 0, time.UTC), }, { Name: "alertingBacktesting", Description: "Rule backtesting API for alerting", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - Created: time.Date(2022, time.October, 20, 12, 0, 0, 0, time.UTC), }, { Name: "editPanelCSVDragAndDrop", @@ -316,7 +283,6 @@ var ( FrontendOnly: true, Stage: FeatureStageExperimental, Owner: grafanaBiSquad, - Created: time.Date(2022, time.December, 20, 12, 0, 0, 0, time.UTC), }, { Name: "alertingNoNormalState", @@ -325,7 +291,6 @@ var ( RequiresRestart: false, Owner: grafanaAlertingSquad, HideFromAdminPage: true, - Created: time.Date(2023, time.January, 14, 12, 0, 0, 0, time.UTC), }, { Name: "logsContextDatasourceUi", @@ -335,7 +300,6 @@ var ( Owner: grafanaObservabilityLogsSquad, Expression: "true", // turned on by default AllowSelfServe: true, - Created: time.Date(2023, time.January, 27, 12, 0, 0, 0, time.UTC), }, { Name: "lokiQuerySplitting", @@ -345,7 +309,6 @@ var ( Owner: grafanaObservabilityLogsSquad, Expression: "true", // turned on by default AllowSelfServe: true, - Created: time.Date(2023, time.February, 9, 12, 0, 0, 0, time.UTC), }, { Name: "lokiQuerySplittingConfig", @@ -353,14 +316,12 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.March, 20, 12, 0, 0, 0, time.UTC), }, { Name: "individualCookiePreferences", Description: "Support overriding cookie preferences per user", Stage: FeatureStageExperimental, Owner: grafanaBackendPlatformSquad, - Created: time.Date(2023, time.February, 23, 12, 0, 0, 0, time.UTC), }, { Name: "prometheusMetricEncyclopedia", @@ -370,7 +331,6 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityMetricsSquad, AllowSelfServe: true, - Created: time.Date(2023, time.March, 7, 12, 0, 0, 0, time.UTC), }, { Name: "influxdbBackendMigration", @@ -380,14 +340,12 @@ var ( Owner: grafanaObservabilityMetricsSquad, Expression: "true", // enabled by default AllowSelfServe: false, - Created: time.Date(2023, time.March, 15, 12, 0, 0, 0, time.UTC), }, { Name: "influxqlStreamingParser", Description: "Enable streaming JSON parser for InfluxDB datasource InfluxQL query language", Stage: FeatureStageExperimental, Owner: grafanaObservabilityMetricsSquad, - Created: time.Date(2023, time.November, 29, 12, 0, 0, 0, time.UTC), }, { Name: "influxdbRunQueriesInParallel", @@ -395,7 +353,6 @@ var ( Stage: FeatureStagePrivatePreview, FrontendOnly: false, Owner: grafanaObservabilityMetricsSquad, - Created: time.Date(2024, time.January, 29, 12, 0, 0, 0, time.UTC), }, { Name: "clientTokenRotation", @@ -404,7 +361,6 @@ var ( Expression: "true", Owner: identityAccessTeam, AllowSelfServe: false, - Created: time.Date(2023, time.March, 23, 12, 0, 0, 0, time.UTC), }, { Name: "prometheusDataplane", @@ -413,7 +369,6 @@ var ( Stage: FeatureStageGeneralAvailability, Owner: grafanaObservabilityMetricsSquad, AllowSelfServe: true, - Created: time.Date(2023, time.March, 29, 12, 0, 0, 0, time.UTC), }, { Name: "lokiMetricDataplane", @@ -422,14 +377,12 @@ var ( Expression: "true", Owner: grafanaObservabilityLogsSquad, AllowSelfServe: true, - Created: time.Date(2023, time.April, 13, 12, 0, 0, 0, time.UTC), }, { Name: "lokiLogsDataplane", Description: "Changes logs responses from Loki to be compliant with the dataplane specification.", Stage: FeatureStageExperimental, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.July, 13, 12, 0, 0, 0, time.UTC), }, { Name: "dataplaneFrontendFallback", @@ -439,42 +392,36 @@ var ( Expression: "true", Owner: grafanaObservabilityMetricsSquad, AllowSelfServe: true, - Created: time.Date(2023, time.April, 24, 12, 0, 0, 0, time.UTC), }, { Name: "disableSSEDataplane", Description: "Disables dataplane specific processing in server side expressions.", Stage: FeatureStageExperimental, Owner: grafanaObservabilityMetricsSquad, - Created: time.Date(2023, time.April, 24, 12, 0, 0, 0, time.UTC), }, { Name: "alertStateHistoryLokiSecondary", Description: "Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations.", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - Created: time.Date(2023, time.March, 30, 12, 0, 0, 0, time.UTC), }, { Name: "alertStateHistoryLokiPrimary", Description: "Enable a remote Loki instance as the primary source for state history reads.", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - Created: time.Date(2023, time.March, 30, 12, 0, 0, 0, time.UTC), }, { Name: "alertStateHistoryLokiOnly", Description: "Disable Grafana alerts from emitting annotations when a remote Loki instance is available.", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - Created: time.Date(2023, time.March, 30, 12, 0, 0, 0, time.UTC), }, { Name: "unifiedRequestLog", Description: "Writes error logs to the request logger", Stage: FeatureStageExperimental, Owner: grafanaBackendPlatformSquad, - Created: time.Date(2023, time.March, 31, 12, 0, 0, 0, time.UTC), }, { Name: "renderAuthJWT", @@ -482,7 +429,6 @@ var ( Stage: FeatureStagePublicPreview, Owner: grafanaAsCodeSquad, HideFromAdminPage: true, - Created: time.Date(2023, time.April, 3, 12, 0, 0, 0, time.UTC), }, { Name: "externalServiceAuth", @@ -490,7 +436,6 @@ var ( Stage: FeatureStageExperimental, RequiresDevMode: true, Owner: identityAccessTeam, - Created: time.Date(2023, time.April, 11, 12, 0, 0, 0, time.UTC), }, { Name: "refactorVariablesTimeRange", @@ -498,7 +443,6 @@ var ( Stage: FeatureStagePublicPreview, Owner: grafanaDashboardsSquad, HideFromAdminPage: true, // Non-feature, used to test out a bug fix that impacts the performance of template variables. - Created: time.Date(2023, time.June, 6, 12, 0, 0, 0, time.UTC), }, { Name: "enableElasticsearchBackendQuerying", @@ -507,7 +451,6 @@ var ( Owner: grafanaObservabilityLogsSquad, Expression: "true", // enabled by default AllowSelfServe: true, - Created: time.Date(2023, time.April, 14, 12, 0, 0, 0, time.UTC), }, { Name: "faroDatasourceSelector", @@ -515,7 +458,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: appO11ySquad, - Created: time.Date(2023, time.May, 4, 12, 0, 0, 0, time.UTC), }, { Name: "enableDatagridEditing", @@ -523,7 +465,6 @@ var ( FrontendOnly: true, Stage: FeatureStagePublicPreview, Owner: grafanaBiSquad, - Created: time.Date(2023, time.April, 24, 12, 0, 0, 0, time.UTC), }, { Name: "extraThemes", @@ -531,7 +472,6 @@ var ( FrontendOnly: true, Stage: FeatureStageExperimental, Owner: grafanaFrontendPlatformSquad, - Created: time.Date(2023, time.May, 10, 12, 0, 0, 0, time.UTC), }, { Name: "lokiPredefinedOperations", @@ -539,7 +479,6 @@ var ( FrontendOnly: true, Stage: FeatureStageExperimental, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.June, 2, 12, 0, 0, 0, time.UTC), }, { Name: "pluginsFrontendSandbox", @@ -547,7 +486,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.June, 5, 12, 0, 0, 0, time.UTC), }, { Name: "dashboardEmbed", @@ -555,7 +493,6 @@ var ( FrontendOnly: true, Stage: FeatureStageExperimental, Owner: grafanaAsCodeSquad, - Created: time.Date(2023, time.July, 6, 12, 0, 0, 0, time.UTC), }, { Name: "frontendSandboxMonitorOnly", @@ -563,7 +500,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.July, 5, 12, 0, 0, 0, time.UTC), }, { Name: "sqlDatasourceDatabaseSelection", @@ -572,7 +508,6 @@ var ( Stage: FeatureStagePublicPreview, Owner: grafanaBiSquad, HideFromAdminPage: true, - Created: time.Date(2023, time.June, 6, 12, 0, 0, 0, time.UTC), }, { Name: "lokiFormatQuery", @@ -580,7 +515,6 @@ var ( FrontendOnly: true, Stage: FeatureStageExperimental, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.June, 21, 12, 0, 0, 0, time.UTC), }, { Name: "cloudWatchLogsMonacoEditor", @@ -590,7 +524,6 @@ var ( Expression: "true", // enabled by default Owner: awsDatasourcesSquad, AllowSelfServe: true, - Created: time.Date(2023, time.June, 12, 12, 0, 0, 0, time.UTC), }, { Name: "exploreScrollableLogsContainer", @@ -598,7 +531,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.June, 15, 12, 0, 0, 0, time.UTC), }, { Name: "recordedQueriesMulti", @@ -607,7 +539,6 @@ var ( Expression: "true", Owner: grafanaObservabilityMetricsSquad, AllowSelfServe: false, - Created: time.Date(2023, time.June, 14, 12, 0, 0, 0, time.UTC), }, { Name: "pluginsDynamicAngularDetectionPatterns", @@ -615,7 +546,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: false, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.June, 26, 12, 0, 0, 0, time.UTC), }, { Name: "vizAndWidgetSplit", @@ -623,7 +553,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaDashboardsSquad, - Created: time.Date(2023, time.June, 27, 12, 0, 0, 0, time.UTC), }, { Name: "prometheusIncrementalQueryInstrumentation", @@ -631,7 +560,6 @@ var ( FrontendOnly: true, Stage: FeatureStageExperimental, Owner: grafanaObservabilityMetricsSquad, - Created: time.Date(2023, time.July, 5, 12, 0, 0, 0, time.UTC), }, { Name: "logsExploreTableVisualisation", @@ -639,14 +567,12 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.July, 12, 12, 0, 0, 0, time.UTC), }, { Name: "awsDatasourcesTempCredentials", Description: "Support temporary security credentials in AWS plugins for Grafana Cloud customers", Stage: FeatureStageExperimental, Owner: awsDatasourcesSquad, - Created: time.Date(2023, time.July, 6, 12, 0, 0, 0, time.UTC), }, { Name: "transformationsRedesign", @@ -656,7 +582,6 @@ var ( Expression: "true", // enabled by default Owner: grafanaObservabilityMetricsSquad, AllowSelfServe: true, - Created: time.Date(2023, time.July, 12, 12, 0, 0, 0, time.UTC), }, { Name: "mlExpressions", @@ -664,7 +589,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: false, Owner: grafanaAlertingSquad, - Created: time.Date(2023, time.July, 13, 12, 0, 0, 0, time.UTC), }, { Name: "traceQLStreaming", @@ -672,7 +596,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityTracesAndProfilingSquad, - Created: time.Date(2023, time.July, 26, 12, 0, 0, 0, time.UTC), }, { Name: "metricsSummary", @@ -680,7 +603,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityTracesAndProfilingSquad, - Created: time.Date(2023, time.August, 28, 12, 0, 0, 0, time.UTC), }, { Name: "grafanaAPIServerWithExperimentalAPIs", @@ -689,7 +611,6 @@ var ( RequiresRestart: true, RequiresDevMode: true, Owner: grafanaAppPlatformSquad, - Created: time.Date(2023, time.October, 6, 12, 0, 0, 0, time.UTC), }, { Name: "grafanaAPIServerEnsureKubectlAccess", @@ -698,7 +619,6 @@ var ( RequiresDevMode: true, RequiresRestart: true, Owner: grafanaAppPlatformSquad, - Created: time.Date(2023, time.December, 6, 12, 0, 0, 0, time.UTC), }, { Name: "featureToggleAdminPage", @@ -707,7 +627,6 @@ var ( FrontendOnly: false, Owner: grafanaOperatorExperienceSquad, RequiresRestart: true, - Created: time.Date(2023, time.July, 18, 12, 0, 0, 0, time.UTC), }, { Name: "awsAsyncQueryCaching", @@ -715,7 +634,6 @@ var ( Stage: FeatureStageGeneralAvailability, Expression: "true", // enabled by default Owner: awsDatasourcesSquad, - Created: time.Date(2023, time.July, 21, 12, 0, 0, 0, time.UTC), }, { Name: "splitScopes", @@ -726,14 +644,12 @@ var ( Owner: identityAccessTeam, RequiresRestart: true, HideFromAdminPage: true, // This is internal work to speed up dashboard search, and is not ready for wider use - Created: time.Date(2023, time.July, 21, 12, 0, 0, 0, time.UTC), }, { Name: "permissionsFilterRemoveSubquery", Description: "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", Stage: FeatureStageExperimental, Owner: grafanaBackendPlatformSquad, - Created: time.Date(2023, time.August, 2, 12, 0, 0, 0, time.UTC), }, { Name: "prometheusConfigOverhaulAuth", @@ -742,7 +658,6 @@ var ( Stage: FeatureStageGeneralAvailability, Expression: "true", // on by default AllowSelfServe: false, - Created: time.Date(2023, time.July, 21, 12, 0, 0, 0, time.UTC), }, { Name: "configurableSchedulerTick", @@ -752,7 +667,6 @@ var ( Owner: grafanaAlertingSquad, RequiresRestart: true, HideFromDocs: true, - Created: time.Date(2023, time.July, 26, 12, 0, 0, 0, time.UTC), }, { Name: "influxdbSqlSupport", @@ -763,7 +677,6 @@ var ( RequiresRestart: true, AllowSelfServe: true, Expression: "true", // enabled by default - Created: time.Date(2023, time.August, 2, 12, 0, 0, 0, time.UTC), }, { Name: "alertingNoDataErrorExecution", @@ -773,7 +686,6 @@ var ( Owner: grafanaAlertingSquad, RequiresRestart: true, Expression: "true", // enabled by default - Created: time.Date(2023, time.August, 15, 12, 0, 0, 0, time.UTC), }, { Name: "angularDeprecationUI", @@ -781,7 +693,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.August, 29, 12, 0, 0, 0, time.UTC), }, { Name: "dashgpt", @@ -789,7 +700,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaDashboardsSquad, - Created: time.Date(2023, time.November, 17, 12, 0, 0, 0, time.UTC), }, { Name: "reportingRetries", @@ -798,14 +708,12 @@ var ( FrontendOnly: false, Owner: grafanaSharingSquad, RequiresRestart: true, - Created: time.Date(2023, time.August, 31, 12, 0, 0, 0, time.UTC), }, { Name: "sseGroupByDatasource", Description: "Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch.", Stage: FeatureStageExperimental, Owner: grafanaObservabilityMetricsSquad, - Created: time.Date(2023, time.September, 7, 12, 0, 0, 0, time.UTC), }, { Name: "libraryPanelRBAC", @@ -814,7 +722,6 @@ var ( FrontendOnly: false, Owner: grafanaDashboardsSquad, RequiresRestart: true, - Created: time.Date(2023, time.October, 11, 12, 0, 0, 0, time.UTC), }, { Name: "lokiRunQueriesInParallel", @@ -822,7 +729,6 @@ var ( Stage: FeatureStagePrivatePreview, FrontendOnly: false, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.September, 19, 12, 0, 0, 0, time.UTC), }, { Name: "wargamesTesting", @@ -830,7 +736,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: false, Owner: hostedGrafanaTeam, - Created: time.Date(2023, time.September, 13, 12, 0, 0, 0, time.UTC), }, { Name: "alertingInsights", @@ -841,14 +746,12 @@ var ( Expression: "true", // enabled by default AllowSelfServe: false, HideFromAdminPage: true, // This is moving away from being a feature toggle. - Created: time.Date(2023, time.September, 14, 12, 0, 0, 0, time.UTC), }, { Name: "externalCorePlugins", Description: "Allow core plugins to be loaded as external", Stage: FeatureStageExperimental, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.September, 22, 12, 0, 0, 0, time.UTC), }, { Name: "pluginsAPIMetrics", @@ -856,14 +759,12 @@ var ( FrontendOnly: true, Stage: FeatureStageExperimental, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.September, 21, 12, 0, 0, 0, time.UTC), }, { Name: "idForwarding", Description: "Generate signed id token for identity that can be forwarded to plugins and external services", Stage: FeatureStageExperimental, Owner: identityAccessTeam, - Created: time.Date(2023, time.September, 25, 12, 0, 0, 0, time.UTC), }, { Name: "cloudWatchWildCardDimensionValues", @@ -872,7 +773,6 @@ var ( Expression: "true", // enabled by default Owner: awsDatasourcesSquad, AllowSelfServe: true, - Created: time.Date(2023, time.September, 27, 12, 0, 0, 0, time.UTC), }, { Name: "externalServiceAccounts", @@ -880,7 +780,6 @@ var ( HideFromAdminPage: true, Stage: FeatureStagePublicPreview, Owner: identityAccessTeam, - Created: time.Date(2023, time.September, 28, 12, 0, 0, 0, time.UTC), }, { Name: "panelMonitoring", @@ -888,7 +787,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaDatavizSquad, FrontendOnly: true, - Created: time.Date(2023, time.October, 8, 12, 0, 0, 0, time.UTC), }, { Name: "enableNativeHTTPHistogram", @@ -896,7 +794,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: false, Owner: hostedGrafanaTeam, - Created: time.Date(2023, time.October, 3, 12, 0, 0, 0, time.UTC), }, { Name: "formatString", @@ -904,7 +801,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaBiSquad, - Created: time.Date(2023, time.October, 13, 12, 0, 0, 0, time.UTC), }, { Name: "transformationsVariableSupport", @@ -912,7 +808,6 @@ var ( FrontendOnly: true, Stage: FeatureStagePublicPreview, Owner: grafanaBiSquad, - Created: time.Date(2023, time.October, 4, 12, 0, 0, 0, time.UTC), }, { Name: "kubernetesPlaylists", @@ -920,7 +815,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, RequiresRestart: true, // changes the API routing - Created: time.Date(2023, time.November, 8, 12, 0, 0, 0, time.UTC), }, { Name: "kubernetesSnapshots", @@ -928,7 +822,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaAppPlatformSquad, RequiresRestart: true, // changes the API routing - Created: time.Date(2023, time.December, 4, 12, 0, 0, 0, time.UTC), }, { Name: "kubernetesQueryServiceRewrite", @@ -937,14 +830,12 @@ var ( Owner: grafanaAppPlatformSquad, RequiresRestart: true, // changes the API routing RequiresDevMode: true, - Created: time.Date(2024, time.January, 28, 12, 0, 0, 0, time.UTC), }, { Name: "cloudWatchBatchQueries", Description: "Runs CloudWatch metrics queries as separate batches", Stage: FeatureStagePublicPreview, Owner: awsDatasourcesSquad, - Created: time.Date(2023, time.October, 20, 12, 0, 0, 0, time.UTC), }, { Name: "recoveryThreshold", @@ -953,7 +844,6 @@ var ( FrontendOnly: false, Owner: grafanaAlertingSquad, RequiresRestart: true, - Created: time.Date(2023, time.October, 10, 12, 0, 0, 0, time.UTC), Expression: "true", }, { @@ -962,7 +852,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: false, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.November, 16, 12, 0, 0, 0, time.UTC), }, { Name: "teamHttpHeaders", @@ -970,7 +859,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: false, Owner: identityAccessTeam, - Created: time.Date(2023, time.October, 17, 12, 0, 0, 0, time.UTC), }, { Name: "awsDatasourcesNewFormStyling", @@ -978,7 +866,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: awsDatasourcesSquad, - Created: time.Date(2023, time.October, 12, 12, 0, 0, 0, time.UTC), }, { Name: "cachingOptimizeSerializationMemoryUsage", @@ -986,7 +873,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaOperatorExperienceSquad, FrontendOnly: false, - Created: time.Date(2023, time.October, 12, 12, 0, 0, 0, time.UTC), }, { Name: "panelTitleSearchInV1", @@ -994,7 +880,6 @@ var ( RequiresDevMode: true, Stage: FeatureStageExperimental, Owner: grafanaBackendPlatformSquad, - Created: time.Date(2023, time.October, 13, 12, 0, 0, 0, time.UTC), }, { Name: "pluginsInstrumentationStatusSource", @@ -1002,7 +887,6 @@ var ( FrontendOnly: false, Stage: FeatureStageExperimental, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.October, 17, 12, 0, 0, 0, time.UTC), }, { Name: "managedPluginsInstall", @@ -1010,7 +894,6 @@ var ( Stage: FeatureStagePublicPreview, RequiresDevMode: false, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.October, 18, 12, 0, 0, 0, time.UTC), }, { Name: "prometheusPromQAIL", @@ -1018,7 +901,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityMetricsSquad, - Created: time.Date(2023, time.October, 19, 12, 0, 0, 0, time.UTC), }, { Name: "addFieldFromCalculationStatFunctions", @@ -1026,28 +908,24 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaBiSquad, - Created: time.Date(2023, time.November, 3, 12, 0, 0, 0, time.UTC), }, { Name: "alertmanagerRemoteSecondary", Description: "Enable Grafana to sync configuration and state with a remote Alertmanager.", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - Created: time.Date(2023, time.October, 30, 12, 0, 0, 0, time.UTC), }, { Name: "alertmanagerRemotePrimary", Description: "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - Created: time.Date(2023, time.October, 30, 12, 0, 0, 0, time.UTC), }, { Name: "alertmanagerRemoteOnly", Description: "Disable the internal Alertmanager and only use the external one defined.", Stage: FeatureStageExperimental, Owner: grafanaAlertingSquad, - Created: time.Date(2023, time.October, 30, 12, 0, 0, 0, time.UTC), }, { Name: "annotationPermissionUpdate", @@ -1055,7 +933,6 @@ var ( Stage: FeatureStageExperimental, RequiresDevMode: false, Owner: identityAccessTeam, - Created: time.Date(2023, time.October, 31, 12, 0, 0, 0, time.UTC), }, { Name: "extractFieldsNameDeduplication", @@ -1063,7 +940,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaBiSquad, - Created: time.Date(2023, time.November, 2, 12, 0, 0, 0, time.UTC), }, { Name: "dashboardSceneForViewers", @@ -1071,7 +947,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaDashboardsSquad, - Created: time.Date(2023, time.November, 2, 12, 0, 0, 0, time.UTC), }, { Name: "dashboardScene", @@ -1079,7 +954,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaDashboardsSquad, - Created: time.Date(2023, time.November, 13, 12, 0, 0, 0, time.UTC), }, { Name: "panelFilterVariable", @@ -1088,7 +962,6 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, HideFromDocs: true, - Created: time.Date(2023, time.November, 3, 12, 0, 0, 0, time.UTC), }, { Name: "pdfTables", @@ -1096,7 +969,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: false, Owner: grafanaSharingSquad, - Created: time.Date(2023, time.November, 6, 12, 0, 0, 0, time.UTC), }, { Name: "ssoSettingsApi", @@ -1104,7 +976,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: false, Owner: identityAccessTeam, - Created: time.Date(2023, time.November, 8, 12, 0, 0, 0, time.UTC), }, { Name: "canvasPanelPanZoom", @@ -1112,7 +983,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaDatavizSquad, - Created: time.Date(2023, time.December, 27, 12, 0, 0, 0, time.UTC), }, { Name: "logsInfiniteScrolling", @@ -1120,7 +990,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.November, 9, 12, 0, 0, 0, time.UTC), }, { Name: "flameGraphItemCollapsing", @@ -1128,7 +997,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityTracesAndProfilingSquad, - Created: time.Date(2023, time.November, 9, 12, 0, 0, 0, time.UTC), }, { Name: "alertingDetailsViewV2", @@ -1137,7 +1005,6 @@ var ( FrontendOnly: true, Owner: grafanaAlertingSquad, HideFromDocs: true, - Created: time.Date(2023, time.November, 9, 12, 0, 0, 0, time.UTC), }, { Name: "datatrails", @@ -1146,7 +1013,6 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, HideFromDocs: true, - Created: time.Date(2023, time.November, 15, 12, 0, 0, 0, time.UTC), }, { Name: "alertingSimplifiedRouting", @@ -1155,7 +1021,6 @@ var ( FrontendOnly: false, Owner: grafanaAlertingSquad, HideFromDocs: true, - Created: time.Date(2023, time.November, 10, 12, 0, 0, 0, time.UTC), }, { Name: "logRowsPopoverMenu", @@ -1164,7 +1029,6 @@ var ( FrontendOnly: true, Expression: "true", Owner: grafanaObservabilityLogsSquad, - Created: time.Date(2023, time.November, 16, 12, 0, 0, 0, time.UTC), }, { Name: "pluginsSkipHostEnvVars", @@ -1172,7 +1036,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: false, Owner: grafanaPluginsPlatformSquad, - Created: time.Date(2023, time.November, 15, 12, 0, 0, 0, time.UTC), }, { Name: "tableSharedCrosshair", @@ -1180,7 +1043,6 @@ var ( FrontendOnly: true, Stage: FeatureStageExperimental, Owner: grafanaBiSquad, - Created: time.Date(2023, time.December, 12, 12, 0, 0, 0, time.UTC), }, { Name: "regressionTransformation", @@ -1188,7 +1050,6 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaBiSquad, - Created: time.Date(2023, time.November, 24, 12, 0, 0, 0, time.UTC), }, { Name: "displayAnonymousStats", @@ -1196,7 +1057,6 @@ var ( Stage: FeatureStageGeneralAvailability, FrontendOnly: true, Owner: identityAccessTeam, - Created: time.Date(2023, time.November, 29, 12, 0, 0, 0, time.UTC), AllowSelfServe: false, Expression: "true", // enabled by default }, @@ -1209,7 +1069,6 @@ var ( Expression: "true", Owner: grafanaObservabilityLogsSquad, AllowSelfServe: false, - Created: time.Date(2023, time.December, 18, 12, 0, 0, 0, time.UTC), }, { Name: "kubernetesFeatureToggles", @@ -1218,7 +1077,6 @@ var ( FrontendOnly: true, Owner: grafanaOperatorExperienceSquad, AllowSelfServe: false, - Created: time.Date(2023, time.December, 22, 3, 43, 0, 0, time.UTC), HideFromAdminPage: true, }, { @@ -1228,7 +1086,6 @@ var ( Stage: FeatureStageGeneralAvailability, Owner: grafanaAlertingSquad, RequiresRestart: true, - Created: time.Date(2024, time.January, 3, 12, 0, 0, 0, time.UTC), Expression: "true", // enabled by default }, { @@ -1238,7 +1095,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaPluginsPlatformSquad, RequiresRestart: true, - Created: time.Date(2024, time.January, 9, 12, 0, 0, 0, time.UTC), }, { Name: "cloudRBACRoles", @@ -1247,7 +1103,6 @@ var ( Owner: identityAccessTeam, HideFromDocs: true, RequiresRestart: true, - Created: time.Date(2024, time.January, 10, 12, 0, 0, 0, time.UTC), }, { Name: "alertingQueryOptimization", @@ -1255,7 +1110,6 @@ var ( Stage: FeatureStageGeneralAvailability, Owner: grafanaAlertingSquad, AllowSelfServe: false, - Created: time.Date(2024, time.January, 10, 12, 0, 0, 0, time.UTC), }, { Name: "newFolderPicker", @@ -1263,7 +1117,6 @@ var ( Stage: FeatureStageExperimental, Owner: grafanaFrontendPlatformSquad, FrontendOnly: true, - Created: time.Date(2024, time.January, 12, 12, 0, 0, 0, time.UTC), }, { Name: "jitterAlertRulesWithinGroups", @@ -1275,14 +1128,12 @@ var ( HideFromDocs: true, HideFromAdminPage: false, RequiresRestart: true, - Created: time.Date(2024, time.January, 17, 12, 0, 0, 0, time.UTC), }, { Name: "onPremToCloudMigrations", Description: "In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud.", Stage: FeatureStageExperimental, Owner: grafanaOperatorExperienceSquad, - Created: time.Date(2024, time.January, 22, 3, 30, 00, 00, time.UTC), }, { Name: "alertingSaveStatePeriodic", @@ -1290,14 +1141,12 @@ var ( Stage: FeatureStagePrivatePreview, FrontendOnly: false, Owner: grafanaAlertingSquad, - Created: time.Date(2024, time.January, 22, 12, 0, 0, 0, time.UTC), }, { Name: "promQLScope", Description: "In-development feature that will allow injection of labels into prometheus queries.", Stage: FeatureStageExperimental, Owner: grafanaObservabilityMetricsSquad, - Created: time.Date(2024, time.January, 29, 0, 0, 0, 0, time.UTC), }, { Name: "nodeGraphDotLayout", @@ -1305,7 +1154,6 @@ var ( Stage: FeatureStageExperimental, FrontendOnly: true, Owner: grafanaObservabilityTracesAndProfilingSquad, - Created: time.Date(2024, time.January, 2, 12, 0, 0, 0, time.UTC), }, { Name: "groupToNestedTableTransformation", @@ -1313,14 +1161,25 @@ var ( Stage: FeatureStagePublicPreview, FrontendOnly: true, Owner: grafanaDatavizSquad, - Created: time.Date(2024, time.February, 5, 12, 0, 0, 0, time.UTC), }, { Name: "newPDFRendering", Description: "New implementation for the dashboard to PDF rendering", Stage: FeatureStageExperimental, Owner: grafanaSharingSquad, - Created: time.Date(2024, time.February, 8, 9, 51, 00, 00, time.UTC), }, } ) + +//go:embed toggles_gen.json +var f embed.FS + +// Get the cached feature list (exposed as a k8s resource) +func GetEmbeddedFeatureList() (featuretoggle.FeatureList, error) { + features := featuretoggle.FeatureList{} + body, err := f.ReadFile("toggles_gen.json") + if err == nil { + err = json.Unmarshal(body, &features) + } + return features, err +} diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index bcd896e222a..c88be17ca78 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -1,158 +1,158 @@ -Name,Stage,Owner,Created,requiresDevMode,RequiresRestart,FrontendOnly -disableEnvelopeEncryption,GA,@grafana/grafana-as-code,2022-05-24,false,false,false -live-service-web-worker,experimental,@grafana/grafana-app-platform-squad,2021-11-09,false,false,true -queryOverLive,experimental,@grafana/grafana-app-platform-squad,2022-01-05,false,false,true -panelTitleSearch,preview,@grafana/grafana-app-platform-squad,2022-02-15,false,false,false -publicDashboards,GA,@grafana/sharing-squad,2022-04-07,false,false,false -publicDashboardsEmailSharing,preview,@grafana/sharing-squad,2022-12-21,false,false,false -lokiExperimentalStreaming,experimental,@grafana/observability-logs,2023-06-19,false,false,false -featureHighlights,GA,@grafana/grafana-as-code,2022-02-03,false,false,false -migrationLocking,preview,@grafana/backend-platform,2022-02-15,false,false,false -storage,experimental,@grafana/grafana-app-platform-squad,2022-03-17,false,false,false -correlations,GA,@grafana/explore-squad,2022-09-16,false,false,false -exploreContentOutline,GA,@grafana/explore-squad,2023-11-03,false,false,true -datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,2022-05-03,false,false,false -traceToMetrics,experimental,@grafana/observability-traces-and-profiling,2022-03-07,false,false,true -autoMigrateOldPanels,preview,@grafana/dataviz-squad,2022-06-11,false,false,true -autoMigrateGraphPanel,preview,@grafana/dataviz-squad,2023-12-11,false,false,true -disableAngular,preview,@grafana/dataviz-squad,2023-03-23,false,false,true -canvasPanelNesting,experimental,@grafana/dataviz-squad,2022-05-31,false,false,true -newVizTooltips,preview,@grafana/dataviz-squad,2023-11-03,false,false,true -scenes,experimental,@grafana/dashboards-squad,2022-07-07,false,false,true -disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,2022-07-13,false,true,false -logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,2022-06-10,false,false,false -dataConnectionsConsole,GA,@grafana/plugins-platform-backend,2022-06-01,false,false,false -topnav,deprecated,@grafana/grafana-frontend-platform,2022-06-20,false,false,false -returnToPrevious,experimental,@grafana/grafana-frontend-platform,2024-01-09,false,false,true -grpcServer,preview,@grafana/grafana-app-platform-squad,2022-09-27,false,false,false -unifiedStorage,experimental,@grafana/grafana-app-platform-squad,2022-12-01,true,true,false -cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,2022-11-28,false,false,false -redshiftAsyncQueryDataSupport,GA,@grafana/aws-datasources,2022-08-27,false,false,false -athenaAsyncQueryDataSupport,GA,@grafana/aws-datasources,2022-08-27,false,false,true -showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,2022-10-14,false,false,false -mysqlAnsiQuotes,experimental,@grafana/backend-platform,2022-10-12,false,false,false -accessControlOnCall,preview,@grafana/identity-access-team,2022-10-19,false,false,false -nestedFolders,preview,@grafana/backend-platform,2022-10-22,false,false,false -nestedFolderPicker,GA,@grafana/grafana-frontend-platform,2023-07-24,false,false,true -alertingBacktesting,experimental,@grafana/alerting-squad,2022-10-20,false,false,false -editPanelCSVDragAndDrop,experimental,@grafana/grafana-bi-squad,2022-12-20,false,false,true -alertingNoNormalState,preview,@grafana/alerting-squad,2023-01-14,false,false,false -logsContextDatasourceUi,GA,@grafana/observability-logs,2023-01-27,false,false,true -lokiQuerySplitting,GA,@grafana/observability-logs,2023-02-09,false,false,true -lokiQuerySplittingConfig,experimental,@grafana/observability-logs,2023-03-20,false,false,true -individualCookiePreferences,experimental,@grafana/backend-platform,2023-02-23,false,false,false -prometheusMetricEncyclopedia,GA,@grafana/observability-metrics,2023-03-07,false,false,true -influxdbBackendMigration,GA,@grafana/observability-metrics,2023-03-15,false,false,true -influxqlStreamingParser,experimental,@grafana/observability-metrics,2023-11-29,false,false,false -influxdbRunQueriesInParallel,privatePreview,@grafana/observability-metrics,2024-01-29,false,false,false -clientTokenRotation,GA,@grafana/identity-access-team,2023-03-23,false,false,false -prometheusDataplane,GA,@grafana/observability-metrics,2023-03-29,false,false,false -lokiMetricDataplane,GA,@grafana/observability-logs,2023-04-13,false,false,false -lokiLogsDataplane,experimental,@grafana/observability-logs,2023-07-13,false,false,false -dataplaneFrontendFallback,GA,@grafana/observability-metrics,2023-04-24,false,false,true -disableSSEDataplane,experimental,@grafana/observability-metrics,2023-04-24,false,false,false -alertStateHistoryLokiSecondary,experimental,@grafana/alerting-squad,2023-03-30,false,false,false -alertStateHistoryLokiPrimary,experimental,@grafana/alerting-squad,2023-03-30,false,false,false -alertStateHistoryLokiOnly,experimental,@grafana/alerting-squad,2023-03-30,false,false,false -unifiedRequestLog,experimental,@grafana/backend-platform,2023-03-31,false,false,false -renderAuthJWT,preview,@grafana/grafana-as-code,2023-04-03,false,false,false -externalServiceAuth,experimental,@grafana/identity-access-team,2023-04-11,true,false,false -refactorVariablesTimeRange,preview,@grafana/dashboards-squad,2023-06-06,false,false,false -enableElasticsearchBackendQuerying,GA,@grafana/observability-logs,2023-04-14,false,false,false -faroDatasourceSelector,preview,@grafana/app-o11y,2023-05-04,false,false,true -enableDatagridEditing,preview,@grafana/grafana-bi-squad,2023-04-24,false,false,true -extraThemes,experimental,@grafana/grafana-frontend-platform,2023-05-10,false,false,true -lokiPredefinedOperations,experimental,@grafana/observability-logs,2023-06-02,false,false,true -pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,2023-06-05,false,false,true -dashboardEmbed,experimental,@grafana/grafana-as-code,2023-07-06,false,false,true -frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,2023-07-05,false,false,true -sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,2023-06-06,false,false,true -lokiFormatQuery,experimental,@grafana/observability-logs,2023-06-21,false,false,true -cloudWatchLogsMonacoEditor,GA,@grafana/aws-datasources,2023-06-12,false,false,true -exploreScrollableLogsContainer,experimental,@grafana/observability-logs,2023-06-15,false,false,true -recordedQueriesMulti,GA,@grafana/observability-metrics,2023-06-14,false,false,false -pluginsDynamicAngularDetectionPatterns,experimental,@grafana/plugins-platform-backend,2023-06-26,false,false,false -vizAndWidgetSplit,experimental,@grafana/dashboards-squad,2023-06-27,false,false,true -prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,2023-07-05,false,false,true -logsExploreTableVisualisation,experimental,@grafana/observability-logs,2023-07-12,false,false,true -awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,2023-07-06,false,false,false -transformationsRedesign,GA,@grafana/observability-metrics,2023-07-12,false,false,true -mlExpressions,experimental,@grafana/alerting-squad,2023-07-13,false,false,false -traceQLStreaming,experimental,@grafana/observability-traces-and-profiling,2023-07-26,false,false,true -metricsSummary,experimental,@grafana/observability-traces-and-profiling,2023-08-28,false,false,true -grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,2023-10-06,true,true,false -grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,2023-12-06,true,true,false -featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,2023-07-18,false,true,false -awsAsyncQueryCaching,GA,@grafana/aws-datasources,2023-07-21,false,false,false -splitScopes,deprecated,@grafana/identity-access-team,2023-07-21,false,true,false -permissionsFilterRemoveSubquery,experimental,@grafana/backend-platform,2023-08-02,false,false,false -prometheusConfigOverhaulAuth,GA,@grafana/observability-metrics,2023-07-21,false,false,false -configurableSchedulerTick,experimental,@grafana/alerting-squad,2023-07-26,false,true,false -influxdbSqlSupport,GA,@grafana/observability-metrics,2023-08-02,false,true,false -alertingNoDataErrorExecution,GA,@grafana/alerting-squad,2023-08-15,false,true,false -angularDeprecationUI,experimental,@grafana/plugins-platform-backend,2023-08-29,false,false,true -dashgpt,preview,@grafana/dashboards-squad,2023-11-17,false,false,true -reportingRetries,preview,@grafana/sharing-squad,2023-08-31,false,true,false -sseGroupByDatasource,experimental,@grafana/observability-metrics,2023-09-07,false,false,false -libraryPanelRBAC,experimental,@grafana/dashboards-squad,2023-10-11,false,true,false -lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,2023-09-19,false,false,false -wargamesTesting,experimental,@grafana/hosted-grafana-team,2023-09-13,false,false,false -alertingInsights,GA,@grafana/alerting-squad,2023-09-14,false,false,true -externalCorePlugins,experimental,@grafana/plugins-platform-backend,2023-09-22,false,false,false -pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,2023-09-21,false,false,true -idForwarding,experimental,@grafana/identity-access-team,2023-09-25,false,false,false -cloudWatchWildCardDimensionValues,GA,@grafana/aws-datasources,2023-09-27,false,false,false -externalServiceAccounts,preview,@grafana/identity-access-team,2023-09-28,false,false,false -panelMonitoring,experimental,@grafana/dataviz-squad,2023-10-08,false,false,true -enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,2023-10-03,false,false,false -formatString,preview,@grafana/grafana-bi-squad,2023-10-13,false,false,true -transformationsVariableSupport,preview,@grafana/grafana-bi-squad,2023-10-04,false,false,true -kubernetesPlaylists,experimental,@grafana/grafana-app-platform-squad,2023-11-08,false,true,false -kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,2023-12-04,false,true,false -kubernetesQueryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,2024-01-28,true,true,false -cloudWatchBatchQueries,preview,@grafana/aws-datasources,2023-10-20,false,false,false -recoveryThreshold,GA,@grafana/alerting-squad,2023-10-10,false,true,false -lokiStructuredMetadata,experimental,@grafana/observability-logs,2023-11-16,false,false,false -teamHttpHeaders,experimental,@grafana/identity-access-team,2023-10-17,false,false,false -awsDatasourcesNewFormStyling,preview,@grafana/aws-datasources,2023-10-12,false,false,true -cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,2023-10-12,false,false,false -panelTitleSearchInV1,experimental,@grafana/backend-platform,2023-10-13,true,false,false -pluginsInstrumentationStatusSource,experimental,@grafana/plugins-platform-backend,2023-10-17,false,false,false -managedPluginsInstall,preview,@grafana/plugins-platform-backend,2023-10-18,false,false,false -prometheusPromQAIL,experimental,@grafana/observability-metrics,2023-10-19,false,false,true -addFieldFromCalculationStatFunctions,preview,@grafana/grafana-bi-squad,2023-11-03,false,false,true -alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,2023-10-30,false,false,false -alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,2023-10-30,false,false,false -alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,2023-10-30,false,false,false -annotationPermissionUpdate,experimental,@grafana/identity-access-team,2023-10-31,false,false,false -extractFieldsNameDeduplication,experimental,@grafana/grafana-bi-squad,2023-11-02,false,false,true -dashboardSceneForViewers,experimental,@grafana/dashboards-squad,2023-11-02,false,false,true -dashboardScene,experimental,@grafana/dashboards-squad,2023-11-13,false,false,true -panelFilterVariable,experimental,@grafana/dashboards-squad,2023-11-03,false,false,true -pdfTables,preview,@grafana/sharing-squad,2023-11-06,false,false,false -ssoSettingsApi,experimental,@grafana/identity-access-team,2023-11-08,false,false,false -canvasPanelPanZoom,preview,@grafana/dataviz-squad,2023-12-27,false,false,true -logsInfiniteScrolling,experimental,@grafana/observability-logs,2023-11-09,false,false,true -flameGraphItemCollapsing,experimental,@grafana/observability-traces-and-profiling,2023-11-09,false,false,true -alertingDetailsViewV2,experimental,@grafana/alerting-squad,2023-11-09,false,false,true -datatrails,experimental,@grafana/dashboards-squad,2023-11-15,false,false,true -alertingSimplifiedRouting,experimental,@grafana/alerting-squad,2023-11-10,false,false,false -logRowsPopoverMenu,GA,@grafana/observability-logs,2023-11-16,false,false,true -pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,2023-11-15,false,false,false -tableSharedCrosshair,experimental,@grafana/grafana-bi-squad,2023-12-12,false,false,true -regressionTransformation,preview,@grafana/grafana-bi-squad,2023-11-24,false,false,true -displayAnonymousStats,GA,@grafana/identity-access-team,2023-11-29,false,false,true -lokiQueryHints,GA,@grafana/observability-logs,2023-12-18,false,false,true -kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,2023-12-22,false,false,true -alertingPreviewUpgrade,GA,@grafana/alerting-squad,2024-01-03,false,true,false -enablePluginsTracingByDefault,experimental,@grafana/plugins-platform-backend,2024-01-09,false,true,false -cloudRBACRoles,experimental,@grafana/identity-access-team,2024-01-10,false,true,false -alertingQueryOptimization,GA,@grafana/alerting-squad,2024-01-10,false,false,false -newFolderPicker,experimental,@grafana/grafana-frontend-platform,2024-01-12,false,false,true -jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,2024-01-17,false,true,false -onPremToCloudMigrations,experimental,@grafana/grafana-operator-experience-squad,2024-01-22,false,false,false -alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,2024-01-22,false,false,false -promQLScope,experimental,@grafana/observability-metrics,2024-01-29,false,false,false -nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,2024-01-02,false,false,true -groupToNestedTableTransformation,preview,@grafana/dataviz-squad,2024-02-05,false,false,true -newPDFRendering,experimental,@grafana/sharing-squad,2024-02-08,false,false,false +Name,Stage,Owner,requiresDevMode,RequiresRestart,FrontendOnly +disableEnvelopeEncryption,GA,@grafana/grafana-as-code,false,false,false +live-service-web-worker,experimental,@grafana/grafana-app-platform-squad,false,false,true +queryOverLive,experimental,@grafana/grafana-app-platform-squad,false,false,true +panelTitleSearch,preview,@grafana/grafana-app-platform-squad,false,false,false +publicDashboards,GA,@grafana/sharing-squad,false,false,false +publicDashboardsEmailSharing,preview,@grafana/sharing-squad,false,false,false +lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,false +featureHighlights,GA,@grafana/grafana-as-code,false,false,false +migrationLocking,preview,@grafana/backend-platform,false,false,false +storage,experimental,@grafana/grafana-app-platform-squad,false,false,false +correlations,GA,@grafana/explore-squad,false,false,false +exploreContentOutline,GA,@grafana/explore-squad,false,false,true +datasourceQueryMultiStatus,experimental,@grafana/plugins-platform-backend,false,false,false +traceToMetrics,experimental,@grafana/observability-traces-and-profiling,false,false,true +autoMigrateOldPanels,preview,@grafana/dataviz-squad,false,false,true +autoMigrateGraphPanel,preview,@grafana/dataviz-squad,false,false,true +disableAngular,preview,@grafana/dataviz-squad,false,false,true +canvasPanelNesting,experimental,@grafana/dataviz-squad,false,false,true +newVizTooltips,preview,@grafana/dataviz-squad,false,false,true +scenes,experimental,@grafana/dashboards-squad,false,false,true +disableSecretsCompatibility,experimental,@grafana/hosted-grafana-team,false,true,false +logRequestsInstrumentedAsUnknown,experimental,@grafana/hosted-grafana-team,false,false,false +dataConnectionsConsole,GA,@grafana/plugins-platform-backend,false,false,false +topnav,deprecated,@grafana/grafana-frontend-platform,false,false,false +returnToPrevious,experimental,@grafana/grafana-frontend-platform,false,false,true +grpcServer,preview,@grafana/grafana-app-platform-squad,false,false,false +unifiedStorage,experimental,@grafana/grafana-app-platform-squad,true,true,false +cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false +redshiftAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,false +athenaAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,true +showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false +mysqlAnsiQuotes,experimental,@grafana/backend-platform,false,false,false +accessControlOnCall,preview,@grafana/identity-access-team,false,false,false +nestedFolders,preview,@grafana/backend-platform,false,false,false +nestedFolderPicker,GA,@grafana/grafana-frontend-platform,false,false,true +alertingBacktesting,experimental,@grafana/alerting-squad,false,false,false +editPanelCSVDragAndDrop,experimental,@grafana/grafana-bi-squad,false,false,true +alertingNoNormalState,preview,@grafana/alerting-squad,false,false,false +logsContextDatasourceUi,GA,@grafana/observability-logs,false,false,true +lokiQuerySplitting,GA,@grafana/observability-logs,false,false,true +lokiQuerySplittingConfig,experimental,@grafana/observability-logs,false,false,true +individualCookiePreferences,experimental,@grafana/backend-platform,false,false,false +prometheusMetricEncyclopedia,GA,@grafana/observability-metrics,false,false,true +influxdbBackendMigration,GA,@grafana/observability-metrics,false,false,true +influxqlStreamingParser,experimental,@grafana/observability-metrics,false,false,false +influxdbRunQueriesInParallel,privatePreview,@grafana/observability-metrics,false,false,false +clientTokenRotation,GA,@grafana/identity-access-team,false,false,false +prometheusDataplane,GA,@grafana/observability-metrics,false,false,false +lokiMetricDataplane,GA,@grafana/observability-logs,false,false,false +lokiLogsDataplane,experimental,@grafana/observability-logs,false,false,false +dataplaneFrontendFallback,GA,@grafana/observability-metrics,false,false,true +disableSSEDataplane,experimental,@grafana/observability-metrics,false,false,false +alertStateHistoryLokiSecondary,experimental,@grafana/alerting-squad,false,false,false +alertStateHistoryLokiPrimary,experimental,@grafana/alerting-squad,false,false,false +alertStateHistoryLokiOnly,experimental,@grafana/alerting-squad,false,false,false +unifiedRequestLog,experimental,@grafana/backend-platform,false,false,false +renderAuthJWT,preview,@grafana/grafana-as-code,false,false,false +externalServiceAuth,experimental,@grafana/identity-access-team,true,false,false +refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false +enableElasticsearchBackendQuerying,GA,@grafana/observability-logs,false,false,false +faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true +enableDatagridEditing,preview,@grafana/grafana-bi-squad,false,false,true +extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true +lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,true +pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,false,true +dashboardEmbed,experimental,@grafana/grafana-as-code,false,false,true +frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,true +sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,true +lokiFormatQuery,experimental,@grafana/observability-logs,false,false,true +cloudWatchLogsMonacoEditor,GA,@grafana/aws-datasources,false,false,true +exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,true +recordedQueriesMulti,GA,@grafana/observability-metrics,false,false,false +pluginsDynamicAngularDetectionPatterns,experimental,@grafana/plugins-platform-backend,false,false,false +vizAndWidgetSplit,experimental,@grafana/dashboards-squad,false,false,true +prometheusIncrementalQueryInstrumentation,experimental,@grafana/observability-metrics,false,false,true +logsExploreTableVisualisation,experimental,@grafana/observability-logs,false,false,true +awsDatasourcesTempCredentials,experimental,@grafana/aws-datasources,false,false,false +transformationsRedesign,GA,@grafana/observability-metrics,false,false,true +mlExpressions,experimental,@grafana/alerting-squad,false,false,false +traceQLStreaming,experimental,@grafana/observability-traces-and-profiling,false,false,true +metricsSummary,experimental,@grafana/observability-traces-and-profiling,false,false,true +grafanaAPIServerWithExperimentalAPIs,experimental,@grafana/grafana-app-platform-squad,true,true,false +grafanaAPIServerEnsureKubectlAccess,experimental,@grafana/grafana-app-platform-squad,true,true,false +featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,false,true,false +awsAsyncQueryCaching,GA,@grafana/aws-datasources,false,false,false +splitScopes,deprecated,@grafana/identity-access-team,false,true,false +permissionsFilterRemoveSubquery,experimental,@grafana/backend-platform,false,false,false +prometheusConfigOverhaulAuth,GA,@grafana/observability-metrics,false,false,false +configurableSchedulerTick,experimental,@grafana/alerting-squad,false,true,false +influxdbSqlSupport,GA,@grafana/observability-metrics,false,true,false +alertingNoDataErrorExecution,GA,@grafana/alerting-squad,false,true,false +angularDeprecationUI,experimental,@grafana/plugins-platform-backend,false,false,true +dashgpt,preview,@grafana/dashboards-squad,false,false,true +reportingRetries,preview,@grafana/sharing-squad,false,true,false +sseGroupByDatasource,experimental,@grafana/observability-metrics,false,false,false +libraryPanelRBAC,experimental,@grafana/dashboards-squad,false,true,false +lokiRunQueriesInParallel,privatePreview,@grafana/observability-logs,false,false,false +wargamesTesting,experimental,@grafana/hosted-grafana-team,false,false,false +alertingInsights,GA,@grafana/alerting-squad,false,false,true +externalCorePlugins,experimental,@grafana/plugins-platform-backend,false,false,false +pluginsAPIMetrics,experimental,@grafana/plugins-platform-backend,false,false,true +idForwarding,experimental,@grafana/identity-access-team,false,false,false +cloudWatchWildCardDimensionValues,GA,@grafana/aws-datasources,false,false,false +externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false +panelMonitoring,experimental,@grafana/dataviz-squad,false,false,true +enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false +formatString,preview,@grafana/grafana-bi-squad,false,false,true +transformationsVariableSupport,preview,@grafana/grafana-bi-squad,false,false,true +kubernetesPlaylists,experimental,@grafana/grafana-app-platform-squad,false,true,false +kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false +kubernetesQueryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,true,true,false +cloudWatchBatchQueries,preview,@grafana/aws-datasources,false,false,false +recoveryThreshold,GA,@grafana/alerting-squad,false,true,false +lokiStructuredMetadata,experimental,@grafana/observability-logs,false,false,false +teamHttpHeaders,experimental,@grafana/identity-access-team,false,false,false +awsDatasourcesNewFormStyling,preview,@grafana/aws-datasources,false,false,true +cachingOptimizeSerializationMemoryUsage,experimental,@grafana/grafana-operator-experience-squad,false,false,false +panelTitleSearchInV1,experimental,@grafana/backend-platform,true,false,false +pluginsInstrumentationStatusSource,experimental,@grafana/plugins-platform-backend,false,false,false +managedPluginsInstall,preview,@grafana/plugins-platform-backend,false,false,false +prometheusPromQAIL,experimental,@grafana/observability-metrics,false,false,true +addFieldFromCalculationStatFunctions,preview,@grafana/grafana-bi-squad,false,false,true +alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false +alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false +alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false +annotationPermissionUpdate,experimental,@grafana/identity-access-team,false,false,false +extractFieldsNameDeduplication,experimental,@grafana/grafana-bi-squad,false,false,true +dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,true +dashboardScene,experimental,@grafana/dashboards-squad,false,false,true +panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true +pdfTables,preview,@grafana/sharing-squad,false,false,false +ssoSettingsApi,experimental,@grafana/identity-access-team,false,false,false +canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true +logsInfiniteScrolling,experimental,@grafana/observability-logs,false,false,true +flameGraphItemCollapsing,experimental,@grafana/observability-traces-and-profiling,false,false,true +alertingDetailsViewV2,experimental,@grafana/alerting-squad,false,false,true +datatrails,experimental,@grafana/dashboards-squad,false,false,true +alertingSimplifiedRouting,experimental,@grafana/alerting-squad,false,false,false +logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true +pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false +tableSharedCrosshair,experimental,@grafana/grafana-bi-squad,false,false,true +regressionTransformation,preview,@grafana/grafana-bi-squad,false,false,true +displayAnonymousStats,GA,@grafana/identity-access-team,false,false,true +lokiQueryHints,GA,@grafana/observability-logs,false,false,true +kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true +alertingPreviewUpgrade,GA,@grafana/alerting-squad,false,true,false +enablePluginsTracingByDefault,experimental,@grafana/plugins-platform-backend,false,true,false +cloudRBACRoles,experimental,@grafana/identity-access-team,false,true,false +alertingQueryOptimization,GA,@grafana/alerting-squad,false,false,false +newFolderPicker,experimental,@grafana/grafana-frontend-platform,false,false,true +jitterAlertRulesWithinGroups,preview,@grafana/alerting-squad,false,true,false +onPremToCloudMigrations,experimental,@grafana/grafana-operator-experience-squad,false,false,false +alertingSaveStatePeriodic,privatePreview,@grafana/alerting-squad,false,false,false +promQLScope,experimental,@grafana/observability-metrics,false,false,false +nodeGraphDotLayout,experimental,@grafana/observability-traces-and-profiling,false,false,true +groupToNestedTableTransformation,preview,@grafana/dataviz-squad,false,false,true +newPDFRendering,experimental,@grafana/sharing-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json new file mode 100644 index 00000000000..2d16608d938 --- /dev/null +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -0,0 +1,2028 @@ +{ + "kind": "FeatureList", + "apiVersion": "featuretoggle.grafana.app/v0alpha1", + "metadata": {}, + "items": [ + { + "metadata": { + "name": "disableEnvelopeEncryption", + "resourceVersion": "1653393600000", + "creationTimestamp": "2022-05-24T12:00:00Z" + }, + "spec": { + "description": "Disable envelope encryption (emergency only)", + "stage": "GA", + "codeowner": "@grafana/grafana-as-code", + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "live-service-web-worker", + "resourceVersion": "1636459200000", + "creationTimestamp": "2021-11-09T12:00:00Z" + }, + "spec": { + "description": "This will use a webworker thread to processes events rather than the main thread", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "queryOverLive", + "resourceVersion": "1641384000000", + "creationTimestamp": "2022-01-05T12:00:00Z" + }, + "spec": { + "description": "Use Grafana Live WebSocket to execute backend queries", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "panelTitleSearch", + "resourceVersion": "1644926400000", + "creationTimestamp": "2022-02-15T12:00:00Z" + }, + "spec": { + "description": "Search for dashboards using panel title", + "stage": "preview", + "codeowner": "@grafana/grafana-app-platform-squad", + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "publicDashboards", + "resourceVersion": "1649332800000", + "creationTimestamp": "2022-04-07T12:00:00Z" + }, + "spec": { + "description": "[Deprecated] Public dashboards are now enabled by default; to disable them, use the configuration setting. This feature toggle will be removed in the next major version.", + "stage": "GA", + "codeowner": "@grafana/sharing-squad", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "publicDashboardsEmailSharing", + "resourceVersion": "1671624000000", + "creationTimestamp": "2022-12-21T12:00:00Z" + }, + "spec": { + "description": "Enables public dashboard sharing to be restricted to only allowed emails", + "stage": "preview", + "codeowner": "@grafana/sharing-squad", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "lokiExperimentalStreaming", + "resourceVersion": "1687176000000", + "creationTimestamp": "2023-06-19T12:00:00Z" + }, + "spec": { + "description": "Support new streaming approach for loki (prototype, needs special loki build)", + "stage": "experimental", + "codeowner": "@grafana/observability-logs" + } + }, + { + "metadata": { + "name": "featureHighlights", + "resourceVersion": "1643889600000", + "creationTimestamp": "2022-02-03T12:00:00Z" + }, + "spec": { + "description": "Highlight Grafana Enterprise features", + "stage": "GA", + "codeowner": "@grafana/grafana-as-code", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "migrationLocking", + "resourceVersion": "1644926400000", + "creationTimestamp": "2022-02-15T12:00:00Z" + }, + "spec": { + "description": "Lock database during migrations", + "stage": "preview", + "codeowner": "@grafana/backend-platform" + } + }, + { + "metadata": { + "name": "storage", + "resourceVersion": "1647518400000", + "creationTimestamp": "2022-03-17T12:00:00Z" + }, + "spec": { + "description": "Configurable storage for dashboards, datasources, and resources", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad" + } + }, + { + "metadata": { + "name": "correlations", + "resourceVersion": "1663329600000", + "creationTimestamp": "2022-09-16T12:00:00Z" + }, + "spec": { + "description": "Correlations page", + "stage": "GA", + "codeowner": "@grafana/explore-squad", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "exploreContentOutline", + "resourceVersion": "1699012800000", + "creationTimestamp": "2023-11-03T12:00:00Z" + }, + "spec": { + "description": "Content outline sidebar", + "stage": "GA", + "codeowner": "@grafana/explore-squad", + "frontend": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "datasourceQueryMultiStatus", + "resourceVersion": "1651579200000", + "creationTimestamp": "2022-05-03T12:00:00Z" + }, + "spec": { + "description": "Introduce HTTP 207 Multi Status for api/ds/query", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" + } + }, + { + "metadata": { + "name": "traceToMetrics", + "resourceVersion": "1646654400000", + "creationTimestamp": "2022-03-07T12:00:00Z" + }, + "spec": { + "description": "Enable trace to metrics links", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, + { + "metadata": { + "name": "autoMigrateOldPanels", + "resourceVersion": "1654948800000", + "creationTimestamp": "2022-06-11T12:00:00Z" + }, + "spec": { + "description": "Migrate old angular panels to supported versions (graph, table-old, worldmap, etc)", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "disableAngular", + "resourceVersion": "1679572800000", + "creationTimestamp": "2023-03-23T12:00:00Z" + }, + "spec": { + "description": "Dynamic flag to disable angular at runtime. The preferred method is to set `angular_support_enabled` to `false` in the [security] settings, which allows you to change the state at runtime.", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true, + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "canvasPanelNesting", + "resourceVersion": "1653998400000", + "creationTimestamp": "2022-05-31T12:00:00Z" + }, + "spec": { + "description": "Allow elements nesting", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true, + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "newVizTooltips", + "resourceVersion": "1699012800000", + "creationTimestamp": "2023-11-03T12:00:00Z" + }, + "spec": { + "description": "New visualizations tooltips UX", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "scenes", + "resourceVersion": "1657195200000", + "creationTimestamp": "2022-07-07T12:00:00Z" + }, + "spec": { + "description": "Experimental framework to build interactive dashboards", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "disableSecretsCompatibility", + "resourceVersion": "1657713600000", + "creationTimestamp": "2022-07-13T12:00:00Z" + }, + "spec": { + "description": "Disable duplicated secret storage in legacy tables", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "logRequestsInstrumentedAsUnknown", + "resourceVersion": "1654862400000", + "creationTimestamp": "2022-06-10T12:00:00Z" + }, + "spec": { + "description": "Logs the path for requests that are instrumented as unknown", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team" + } + }, + { + "metadata": { + "name": "dataConnectionsConsole", + "resourceVersion": "1654084800000", + "creationTimestamp": "2022-06-01T12:00:00Z" + }, + "spec": { + "description": "Enables a new top-level page called Connections. This page is an experiment that provides a better experience when you install and configure data sources and other plugins.", + "stage": "GA", + "codeowner": "@grafana/plugins-platform-backend", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "topnav", + "resourceVersion": "1655726400000", + "creationTimestamp": "2022-06-20T12:00:00Z" + }, + "spec": { + "description": "Enables topnav support in external plugins. The new Grafana navigation cannot be disabled.", + "stage": "deprecated", + "codeowner": "@grafana/grafana-frontend-platform" + } + }, + { + "metadata": { + "name": "returnToPrevious", + "resourceVersion": "1704798000000", + "creationTimestamp": "2024-01-09T11:00:00Z" + }, + "spec": { + "description": "Enables the return to previous context functionality", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true + } + }, + { + "metadata": { + "name": "grpcServer", + "resourceVersion": "1664280000000", + "creationTimestamp": "2022-09-27T12:00:00Z" + }, + "spec": { + "description": "Run the GRPC server", + "stage": "preview", + "codeowner": "@grafana/grafana-app-platform-squad", + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "unifiedStorage", + "resourceVersion": "1669896000000", + "creationTimestamp": "2022-12-01T12:00:00Z" + }, + "spec": { + "description": "SQL-based k8s storage", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, + "requiresRestart": true + } + }, + { + "metadata": { + "name": "cloudWatchCrossAccountQuerying", + "resourceVersion": "1669636800000", + "creationTimestamp": "2022-11-28T12:00:00Z" + }, + "spec": { + "description": "Enables cross-account querying in CloudWatch datasources", + "stage": "GA", + "codeowner": "@grafana/aws-datasources", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "redshiftAsyncQueryDataSupport", + "resourceVersion": "1661601600000", + "creationTimestamp": "2022-08-27T12:00:00Z" + }, + "spec": { + "description": "Enable async query data support for Redshift", + "stage": "GA", + "codeowner": "@grafana/aws-datasources" + } + }, + { + "metadata": { + "name": "athenaAsyncQueryDataSupport", + "resourceVersion": "1661601600000", + "creationTimestamp": "2022-08-27T12:00:00Z" + }, + "spec": { + "description": "Enable async query data support for Athena", + "stage": "GA", + "codeowner": "@grafana/aws-datasources", + "frontend": true + } + }, + { + "metadata": { + "name": "showDashboardValidationWarnings", + "resourceVersion": "1665748800000", + "creationTimestamp": "2022-10-14T12:00:00Z" + }, + "spec": { + "description": "Show warnings when dashboards do not validate against the schema", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad" + } + }, + { + "metadata": { + "name": "mysqlAnsiQuotes", + "resourceVersion": "1665576000000", + "creationTimestamp": "2022-10-12T12:00:00Z" + }, + "spec": { + "description": "Use double quotes to escape keyword in a MySQL query", + "stage": "experimental", + "codeowner": "@grafana/backend-platform" + } + }, + { + "metadata": { + "name": "accessControlOnCall", + "resourceVersion": "1666180800000", + "creationTimestamp": "2022-10-19T12:00:00Z" + }, + "spec": { + "description": "Access control primitives for OnCall", + "stage": "preview", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "nestedFolders", + "resourceVersion": "1666440000000", + "creationTimestamp": "2022-10-22T12:00:00Z" + }, + "spec": { + "description": "Enable folder nesting", + "stage": "preview", + "codeowner": "@grafana/backend-platform" + } + }, + { + "metadata": { + "name": "nestedFolderPicker", + "resourceVersion": "1690200000000", + "creationTimestamp": "2023-07-24T12:00:00Z" + }, + "spec": { + "description": "Enables the new folder picker to work with nested folders. Requires the nestedFolders feature toggle", + "stage": "GA", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "alertingBacktesting", + "resourceVersion": "1666267200000", + "creationTimestamp": "2022-10-20T12:00:00Z" + }, + "spec": { + "description": "Rule backtesting API for alerting", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "editPanelCSVDragAndDrop", + "resourceVersion": "1671537600000", + "creationTimestamp": "2022-12-20T12:00:00Z" + }, + "spec": { + "description": "Enables drag and drop for CSV and Excel files", + "stage": "experimental", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "alertingNoNormalState", + "resourceVersion": "1673697600000", + "creationTimestamp": "2023-01-14T12:00:00Z" + }, + "spec": { + "description": "Stop maintaining state of alerts that are not firing", + "stage": "preview", + "codeowner": "@grafana/alerting-squad", + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "logsContextDatasourceUi", + "resourceVersion": "1674820800000", + "creationTimestamp": "2023-01-27T12:00:00Z" + }, + "spec": { + "description": "Allow datasource to provide custom UI for context view", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "lokiQuerySplitting", + "resourceVersion": "1675944000000", + "creationTimestamp": "2023-02-09T12:00:00Z" + }, + "spec": { + "description": "Split large interval queries into subqueries with smaller time intervals", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "lokiQuerySplittingConfig", + "resourceVersion": "1679313600000", + "creationTimestamp": "2023-03-20T12:00:00Z" + }, + "spec": { + "description": "Give users the option to configure split durations for Loki queries", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, + { + "metadata": { + "name": "individualCookiePreferences", + "resourceVersion": "1677153600000", + "creationTimestamp": "2023-02-23T12:00:00Z" + }, + "spec": { + "description": "Support overriding cookie preferences per user", + "stage": "experimental", + "codeowner": "@grafana/backend-platform" + } + }, + { + "metadata": { + "name": "prometheusMetricEncyclopedia", + "resourceVersion": "1678190400000", + "creationTimestamp": "2023-03-07T12:00:00Z" + }, + "spec": { + "description": "Adds the metrics explorer component to the Prometheus query builder as an option in metric select", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "influxdbBackendMigration", + "resourceVersion": "1678881600000", + "creationTimestamp": "2023-03-15T12:00:00Z" + }, + "spec": { + "description": "Query InfluxDB InfluxQL without the proxy", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true + } + }, + { + "metadata": { + "name": "influxqlStreamingParser", + "resourceVersion": "1701259200000", + "creationTimestamp": "2023-11-29T12:00:00Z" + }, + "spec": { + "description": "Enable streaming JSON parser for InfluxDB datasource InfluxQL query language", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics" + } + }, + { + "metadata": { + "name": "influxdbRunQueriesInParallel", + "resourceVersion": "1706529600000", + "creationTimestamp": "2024-01-29T12:00:00Z" + }, + "spec": { + "description": "Enables running InfluxDB Influxql queries in parallel", + "stage": "privatePreview", + "codeowner": "@grafana/observability-metrics" + } + }, + { + "metadata": { + "name": "clientTokenRotation", + "resourceVersion": "1679572800000", + "creationTimestamp": "2023-03-23T12:00:00Z" + }, + "spec": { + "description": "Replaces the current in-request token rotation so that the client initiates the rotation", + "stage": "GA", + "codeowner": "@grafana/identity-access-team" + } + }, + { + "metadata": { + "name": "prometheusDataplane", + "resourceVersion": "1680091200000", + "creationTimestamp": "2023-03-29T12:00:00Z" + }, + "spec": { + "description": "Changes responses to from Prometheus to be compliant with the dataplane specification. In particular, when this feature toggle is active, the numeric `Field.Name` is set from 'Value' to the value of the `__name__` label.", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "lokiMetricDataplane", + "resourceVersion": "1681387200000", + "creationTimestamp": "2023-04-13T12:00:00Z" + }, + "spec": { + "description": "Changes metric responses from Loki to be compliant with the dataplane specification.", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "lokiLogsDataplane", + "resourceVersion": "1689249600000", + "creationTimestamp": "2023-07-13T12:00:00Z" + }, + "spec": { + "description": "Changes logs responses from Loki to be compliant with the dataplane specification.", + "stage": "experimental", + "codeowner": "@grafana/observability-logs" + } + }, + { + "metadata": { + "name": "dataplaneFrontendFallback", + "resourceVersion": "1682337600000", + "creationTimestamp": "2023-04-24T12:00:00Z" + }, + "spec": { + "description": "Support dataplane contract field name change for transformations and field name matchers where the name is different", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "disableSSEDataplane", + "resourceVersion": "1682337600000", + "creationTimestamp": "2023-04-24T12:00:00Z" + }, + "spec": { + "description": "Disables dataplane specific processing in server side expressions.", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics" + } + }, + { + "metadata": { + "name": "alertStateHistoryLokiSecondary", + "resourceVersion": "1680177600000", + "creationTimestamp": "2023-03-30T12:00:00Z" + }, + "spec": { + "description": "Enable Grafana to write alert state history to an external Loki instance in addition to Grafana annotations.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "alertStateHistoryLokiPrimary", + "resourceVersion": "1680177600000", + "creationTimestamp": "2023-03-30T12:00:00Z" + }, + "spec": { + "description": "Enable a remote Loki instance as the primary source for state history reads.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "alertStateHistoryLokiOnly", + "resourceVersion": "1680177600000", + "creationTimestamp": "2023-03-30T12:00:00Z" + }, + "spec": { + "description": "Disable Grafana alerts from emitting annotations when a remote Loki instance is available.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "unifiedRequestLog", + "resourceVersion": "1680264000000", + "creationTimestamp": "2023-03-31T12:00:00Z" + }, + "spec": { + "description": "Writes error logs to the request logger", + "stage": "experimental", + "codeowner": "@grafana/backend-platform" + } + }, + { + "metadata": { + "name": "renderAuthJWT", + "resourceVersion": "1680523200000", + "creationTimestamp": "2023-04-03T12:00:00Z" + }, + "spec": { + "description": "Uses JWT-based auth for rendering instead of relying on remote cache", + "stage": "preview", + "codeowner": "@grafana/grafana-as-code", + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "externalServiceAuth", + "resourceVersion": "1681214400000", + "creationTimestamp": "2023-04-11T12:00:00Z" + }, + "spec": { + "description": "Starts an OAuth2 authentication provider for external services", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "requiresDevMode": true + } + }, + { + "metadata": { + "name": "refactorVariablesTimeRange", + "resourceVersion": "1686052800000", + "creationTimestamp": "2023-06-06T12:00:00Z" + }, + "spec": { + "description": "Refactor time range variables flow to reduce number of API calls made when query variables are chained", + "stage": "preview", + "codeowner": "@grafana/dashboards-squad", + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "enableElasticsearchBackendQuerying", + "resourceVersion": "1681473600000", + "creationTimestamp": "2023-04-14T12:00:00Z" + }, + "spec": { + "description": "Enable the processing of queries and responses in the Elasticsearch data source through backend", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "faroDatasourceSelector", + "resourceVersion": "1683201600000", + "creationTimestamp": "2023-05-04T12:00:00Z" + }, + "spec": { + "description": "Enable the data source selector within the Frontend Apps section of the Frontend Observability", + "stage": "preview", + "codeowner": "@grafana/app-o11y", + "frontend": true + } + }, + { + "metadata": { + "name": "enableDatagridEditing", + "resourceVersion": "1682337600000", + "creationTimestamp": "2023-04-24T12:00:00Z" + }, + "spec": { + "description": "Enables the edit functionality in the datagrid panel", + "stage": "preview", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "extraThemes", + "resourceVersion": "1683720000000", + "creationTimestamp": "2023-05-10T12:00:00Z" + }, + "spec": { + "description": "Enables extra themes", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true + } + }, + { + "metadata": { + "name": "lokiPredefinedOperations", + "resourceVersion": "1685707200000", + "creationTimestamp": "2023-06-02T12:00:00Z" + }, + "spec": { + "description": "Adds predefined query operations to Loki query editor", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, + { + "metadata": { + "name": "pluginsFrontendSandbox", + "resourceVersion": "1685966400000", + "creationTimestamp": "2023-06-05T12:00:00Z" + }, + "spec": { + "description": "Enables the plugins frontend sandbox", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true + } + }, + { + "metadata": { + "name": "dashboardEmbed", + "resourceVersion": "1688644800000", + "creationTimestamp": "2023-07-06T12:00:00Z" + }, + "spec": { + "description": "Allow embedding dashboard for external use in Code editors", + "stage": "experimental", + "codeowner": "@grafana/grafana-as-code", + "frontend": true + } + }, + { + "metadata": { + "name": "frontendSandboxMonitorOnly", + "resourceVersion": "1688558400000", + "creationTimestamp": "2023-07-05T12:00:00Z" + }, + "spec": { + "description": "Enables monitor only in the plugin frontend sandbox (if enabled)", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true + } + }, + { + "metadata": { + "name": "sqlDatasourceDatabaseSelection", + "resourceVersion": "1686052800000", + "creationTimestamp": "2023-06-06T12:00:00Z" + }, + "spec": { + "description": "Enables previous SQL data source dataset dropdown behavior", + "stage": "preview", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true, + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "lokiFormatQuery", + "resourceVersion": "1687348800000", + "creationTimestamp": "2023-06-21T12:00:00Z" + }, + "spec": { + "description": "Enables the ability to format Loki queries", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, + { + "metadata": { + "name": "cloudWatchLogsMonacoEditor", + "resourceVersion": "1686571200000", + "creationTimestamp": "2023-06-12T12:00:00Z" + }, + "spec": { + "description": "Enables the Monaco editor for CloudWatch Logs queries", + "stage": "GA", + "codeowner": "@grafana/aws-datasources", + "frontend": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "exploreScrollableLogsContainer", + "resourceVersion": "1686830400000", + "creationTimestamp": "2023-06-15T12:00:00Z" + }, + "spec": { + "description": "Improves the scrolling behavior of logs in Explore", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, + { + "metadata": { + "name": "recordedQueriesMulti", + "resourceVersion": "1686744000000", + "creationTimestamp": "2023-06-14T12:00:00Z" + }, + "spec": { + "description": "Enables writing multiple items from a single query within Recorded Queries", + "stage": "GA", + "codeowner": "@grafana/observability-metrics" + } + }, + { + "metadata": { + "name": "pluginsDynamicAngularDetectionPatterns", + "resourceVersion": "1687780800000", + "creationTimestamp": "2023-06-26T12:00:00Z" + }, + "spec": { + "description": "Enables fetching Angular detection patterns for plugins from GCOM and fallback to hardcoded ones", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" + } + }, + { + "metadata": { + "name": "vizAndWidgetSplit", + "resourceVersion": "1687867200000", + "creationTimestamp": "2023-06-27T12:00:00Z" + }, + "spec": { + "description": "Split panels between visualizations and widgets", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "prometheusIncrementalQueryInstrumentation", + "resourceVersion": "1688558400000", + "creationTimestamp": "2023-07-05T12:00:00Z" + }, + "spec": { + "description": "Adds RudderStack events to incremental queries", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics", + "frontend": true + } + }, + { + "metadata": { + "name": "logsExploreTableVisualisation", + "resourceVersion": "1689163200000", + "creationTimestamp": "2023-07-12T12:00:00Z" + }, + "spec": { + "description": "A table visualisation for logs in Explore", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, + { + "metadata": { + "name": "awsDatasourcesTempCredentials", + "resourceVersion": "1688644800000", + "creationTimestamp": "2023-07-06T12:00:00Z" + }, + "spec": { + "description": "Support temporary security credentials in AWS plugins for Grafana Cloud customers", + "stage": "experimental", + "codeowner": "@grafana/aws-datasources" + } + }, + { + "metadata": { + "name": "transformationsRedesign", + "resourceVersion": "1689163200000", + "creationTimestamp": "2023-07-12T12:00:00Z" + }, + "spec": { + "description": "Enables the transformations redesign", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "frontend": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "mlExpressions", + "resourceVersion": "1689249600000", + "creationTimestamp": "2023-07-13T12:00:00Z" + }, + "spec": { + "description": "Enable support for Machine Learning in server-side expressions", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "traceQLStreaming", + "resourceVersion": "1690372800000", + "creationTimestamp": "2023-07-26T12:00:00Z" + }, + "spec": { + "description": "Enables response streaming of TraceQL queries of the Tempo data source", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, + { + "metadata": { + "name": "metricsSummary", + "resourceVersion": "1693224000000", + "creationTimestamp": "2023-08-28T12:00:00Z" + }, + "spec": { + "description": "Enables metrics summary queries in the Tempo data source", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, + { + "metadata": { + "name": "grafanaAPIServerWithExperimentalAPIs", + "resourceVersion": "1696593600000", + "creationTimestamp": "2023-10-06T12:00:00Z" + }, + "spec": { + "description": "Register experimental APIs with the k8s API server", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, + "requiresRestart": true + } + }, + { + "metadata": { + "name": "grafanaAPIServerEnsureKubectlAccess", + "resourceVersion": "1701864000000", + "creationTimestamp": "2023-12-06T12:00:00Z" + }, + "spec": { + "description": "Start an additional https handler and write kubectl options", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, + "requiresRestart": true + } + }, + { + "metadata": { + "name": "featureToggleAdminPage", + "resourceVersion": "1689681600000", + "creationTimestamp": "2023-07-18T12:00:00Z" + }, + "spec": { + "description": "Enable admin page for managing feature toggles from the Grafana front-end", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "awsAsyncQueryCaching", + "resourceVersion": "1689940800000", + "creationTimestamp": "2023-07-21T12:00:00Z" + }, + "spec": { + "description": "Enable caching for async queries for Redshift and Athena. Requires that the datasource has caching and async query support enabled", + "stage": "GA", + "codeowner": "@grafana/aws-datasources" + } + }, + { + "metadata": { + "name": "splitScopes", + "resourceVersion": "1689940800000", + "creationTimestamp": "2023-07-21T12:00:00Z" + }, + "spec": { + "description": "Support faster dashboard and folder search by splitting permission scopes into parts", + "stage": "deprecated", + "codeowner": "@grafana/identity-access-team", + "requiresRestart": true, + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "permissionsFilterRemoveSubquery", + "resourceVersion": "1690977600000", + "creationTimestamp": "2023-08-02T12:00:00Z" + }, + "spec": { + "description": "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", + "stage": "experimental", + "codeowner": "@grafana/backend-platform" + } + }, + { + "metadata": { + "name": "prometheusConfigOverhaulAuth", + "resourceVersion": "1689940800000", + "creationTimestamp": "2023-07-21T12:00:00Z" + }, + "spec": { + "description": "Update the Prometheus configuration page with the new auth component", + "stage": "GA", + "codeowner": "@grafana/observability-metrics" + } + }, + { + "metadata": { + "name": "configurableSchedulerTick", + "resourceVersion": "1690372800000", + "creationTimestamp": "2023-07-26T12:00:00Z" + }, + "spec": { + "description": "Enable changing the scheduler base interval via configuration option unified_alerting.scheduler_tick_interval", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "influxdbSqlSupport", + "resourceVersion": "1690977600000", + "creationTimestamp": "2023-08-02T12:00:00Z" + }, + "spec": { + "description": "Enable InfluxDB SQL query language support with new querying UI", + "stage": "GA", + "codeowner": "@grafana/observability-metrics", + "requiresRestart": true, + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "alertingNoDataErrorExecution", + "resourceVersion": "1692100800000", + "creationTimestamp": "2023-08-15T12:00:00Z" + }, + "spec": { + "description": "Changes how Alerting state manager handles execution of NoData/Error", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "angularDeprecationUI", + "resourceVersion": "1693310400000", + "creationTimestamp": "2023-08-29T12:00:00Z" + }, + "spec": { + "description": "Display new Angular deprecation-related UI features", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true + } + }, + { + "metadata": { + "name": "dashgpt", + "resourceVersion": "1700222400000", + "creationTimestamp": "2023-11-17T12:00:00Z" + }, + "spec": { + "description": "Enable AI powered features in dashboards", + "stage": "preview", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "reportingRetries", + "resourceVersion": "1693483200000", + "creationTimestamp": "2023-08-31T12:00:00Z" + }, + "spec": { + "description": "Enables rendering retries for the reporting feature", + "stage": "preview", + "codeowner": "@grafana/sharing-squad", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "sseGroupByDatasource", + "resourceVersion": "1694088000000", + "creationTimestamp": "2023-09-07T12:00:00Z" + }, + "spec": { + "description": "Send query to the same datasource in a single request when using server side expressions. The `cloudWatchBatchQueries` feature toggle should be enabled if this used with CloudWatch.", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics" + } + }, + { + "metadata": { + "name": "libraryPanelRBAC", + "resourceVersion": "1697025600000", + "creationTimestamp": "2023-10-11T12:00:00Z" + }, + "spec": { + "description": "Enables RBAC support for library panels", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "lokiRunQueriesInParallel", + "resourceVersion": "1695124800000", + "creationTimestamp": "2023-09-19T12:00:00Z" + }, + "spec": { + "description": "Enables running Loki queries in parallel", + "stage": "privatePreview", + "codeowner": "@grafana/observability-logs" + } + }, + { + "metadata": { + "name": "wargamesTesting", + "resourceVersion": "1694606400000", + "creationTimestamp": "2023-09-13T12:00:00Z" + }, + "spec": { + "description": "Placeholder feature flag for internal testing", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team" + } + }, + { + "metadata": { + "name": "alertingInsights", + "resourceVersion": "1694692800000", + "creationTimestamp": "2023-09-14T12:00:00Z" + }, + "spec": { + "description": "Show the new alerting insights landing page", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "frontend": true, + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "externalCorePlugins", + "resourceVersion": "1695384000000", + "creationTimestamp": "2023-09-22T12:00:00Z" + }, + "spec": { + "description": "Allow core plugins to be loaded as external", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" + } + }, + { + "metadata": { + "name": "pluginsAPIMetrics", + "resourceVersion": "1695297600000", + "creationTimestamp": "2023-09-21T12:00:00Z" + }, + "spec": { + "description": "Sends metrics of public grafana packages usage by plugins", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "frontend": true + } + }, + { + "metadata": { + "name": "idForwarding", + "resourceVersion": "1695643200000", + "creationTimestamp": "2023-09-25T12:00:00Z" + }, + "spec": { + "description": "Generate signed id token for identity that can be forwarded to plugins and external services", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" + } + }, + { + "metadata": { + "name": "cloudWatchWildCardDimensionValues", + "resourceVersion": "1695816000000", + "creationTimestamp": "2023-09-27T12:00:00Z" + }, + "spec": { + "description": "Fetches dimension values from CloudWatch to correctly label wildcard dimensions", + "stage": "GA", + "codeowner": "@grafana/aws-datasources", + "allowSelfServe": true + } + }, + { + "metadata": { + "name": "externalServiceAccounts", + "resourceVersion": "1695902400000", + "creationTimestamp": "2023-09-28T12:00:00Z" + }, + "spec": { + "description": "Automatic service account and token setup for plugins", + "stage": "preview", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "panelMonitoring", + "resourceVersion": "1696766400000", + "creationTimestamp": "2023-10-08T12:00:00Z" + }, + "spec": { + "description": "Enables panel monitoring through logs and measurements", + "stage": "experimental", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "enableNativeHTTPHistogram", + "resourceVersion": "1696334400000", + "creationTimestamp": "2023-10-03T12:00:00Z" + }, + "spec": { + "description": "Enables native HTTP Histograms", + "stage": "experimental", + "codeowner": "@grafana/hosted-grafana-team" + } + }, + { + "metadata": { + "name": "formatString", + "resourceVersion": "1697198400000", + "creationTimestamp": "2023-10-13T12:00:00Z" + }, + "spec": { + "description": "Enable format string transformer", + "stage": "preview", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "transformationsVariableSupport", + "resourceVersion": "1696420800000", + "creationTimestamp": "2023-10-04T12:00:00Z" + }, + "spec": { + "description": "Allows using variables in transformations", + "stage": "preview", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "kubernetesPlaylists", + "resourceVersion": "1699444800000", + "creationTimestamp": "2023-11-08T12:00:00Z" + }, + "spec": { + "description": "Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "kubernetesSnapshots", + "resourceVersion": "1707374669879", + "creationTimestamp": "2023-12-04T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-08 06:44:29.879787 +0000 UTC" + } + }, + "spec": { + "description": "Routes snapshot requests from /api to the /apis endpoint", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "kubernetesQueryServiceRewrite", + "resourceVersion": "1706443200000", + "creationTimestamp": "2024-01-28T12:00:00Z" + }, + "spec": { + "description": "Rewrite requests targeting /ds/query to the query service", + "stage": "experimental", + "codeowner": "@grafana/grafana-app-platform-squad", + "requiresDevMode": true, + "requiresRestart": true + } + }, + { + "metadata": { + "name": "cloudWatchBatchQueries", + "resourceVersion": "1697803200000", + "creationTimestamp": "2023-10-20T12:00:00Z" + }, + "spec": { + "description": "Runs CloudWatch metrics queries as separate batches", + "stage": "preview", + "codeowner": "@grafana/aws-datasources" + } + }, + { + "metadata": { + "name": "recoveryThreshold", + "resourceVersion": "1696939200000", + "creationTimestamp": "2023-10-10T12:00:00Z" + }, + "spec": { + "description": "Enables feature recovery threshold (aka hysteresis) for threshold server-side expression", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "lokiStructuredMetadata", + "resourceVersion": "1700136000000", + "creationTimestamp": "2023-11-16T12:00:00Z" + }, + "spec": { + "description": "Enables the loki data source to request structured metadata from the Loki server", + "stage": "experimental", + "codeowner": "@grafana/observability-logs" + } + }, + { + "metadata": { + "name": "teamHttpHeaders", + "resourceVersion": "1697544000000", + "creationTimestamp": "2023-10-17T12:00:00Z" + }, + "spec": { + "description": "Enables datasources to apply team headers to the client requests", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" + } + }, + { + "metadata": { + "name": "awsDatasourcesNewFormStyling", + "resourceVersion": "1697112000000", + "creationTimestamp": "2023-10-12T12:00:00Z" + }, + "spec": { + "description": "Applies new form styling for configuration and query editors in AWS plugins", + "stage": "preview", + "codeowner": "@grafana/aws-datasources", + "frontend": true + } + }, + { + "metadata": { + "name": "cachingOptimizeSerializationMemoryUsage", + "resourceVersion": "1697112000000", + "creationTimestamp": "2023-10-12T12:00:00Z" + }, + "spec": { + "description": "If enabled, the caching backend gradually serializes query responses for the cache, comparing against the configured `[caching]max_value_mb` value as it goes. This can can help prevent Grafana from running out of memory while attempting to cache very large query responses.", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad" + } + }, + { + "metadata": { + "name": "panelTitleSearchInV1", + "resourceVersion": "1697198400000", + "creationTimestamp": "2023-10-13T12:00:00Z" + }, + "spec": { + "description": "Enable searching for dashboards using panel title in search v1", + "stage": "experimental", + "codeowner": "@grafana/backend-platform", + "requiresDevMode": true + } + }, + { + "metadata": { + "name": "pluginsInstrumentationStatusSource", + "resourceVersion": "1697544000000", + "creationTimestamp": "2023-10-17T12:00:00Z" + }, + "spec": { + "description": "Include a status source label for plugin request metrics and logs", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" + } + }, + { + "metadata": { + "name": "managedPluginsInstall", + "resourceVersion": "1697630400000", + "creationTimestamp": "2023-10-18T12:00:00Z" + }, + "spec": { + "description": "Install managed plugins directly from plugins catalog", + "stage": "preview", + "codeowner": "@grafana/plugins-platform-backend" + } + }, + { + "metadata": { + "name": "prometheusPromQAIL", + "resourceVersion": "1697716800000", + "creationTimestamp": "2023-10-19T12:00:00Z" + }, + "spec": { + "description": "Prometheus and AI/ML to assist users in creating a query", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics", + "frontend": true + } + }, + { + "metadata": { + "name": "addFieldFromCalculationStatFunctions", + "resourceVersion": "1699012800000", + "creationTimestamp": "2023-11-03T12:00:00Z" + }, + "spec": { + "description": "Add cumulative and window functions to the add field from calculation transformation", + "stage": "preview", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "alertmanagerRemoteSecondary", + "resourceVersion": "1698667200000", + "creationTimestamp": "2023-10-30T12:00:00Z" + }, + "spec": { + "description": "Enable Grafana to sync configuration and state with a remote Alertmanager.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "alertmanagerRemotePrimary", + "resourceVersion": "1698667200000", + "creationTimestamp": "2023-10-30T12:00:00Z" + }, + "spec": { + "description": "Enable Grafana to have a remote Alertmanager instance as the primary Alertmanager.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "alertmanagerRemoteOnly", + "resourceVersion": "1698667200000", + "creationTimestamp": "2023-10-30T12:00:00Z" + }, + "spec": { + "description": "Disable the internal Alertmanager and only use the external one defined.", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "annotationPermissionUpdate", + "resourceVersion": "1698753600000", + "creationTimestamp": "2023-10-31T12:00:00Z" + }, + "spec": { + "description": "Separate annotation permissions from dashboard permissions to allow for more granular control.", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" + } + }, + { + "metadata": { + "name": "extractFieldsNameDeduplication", + "resourceVersion": "1698926400000", + "creationTimestamp": "2023-11-02T12:00:00Z" + }, + "spec": { + "description": "Make sure extracted field names are unique in the dataframe", + "stage": "experimental", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "dashboardSceneForViewers", + "resourceVersion": "1698926400000", + "creationTimestamp": "2023-11-02T12:00:00Z" + }, + "spec": { + "description": "Enables dashboard rendering using Scenes for viewer roles", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "dashboardScene", + "resourceVersion": "1699876800000", + "creationTimestamp": "2023-11-13T12:00:00Z" + }, + "spec": { + "description": "Enables dashboard rendering using scenes for all roles", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "panelFilterVariable", + "resourceVersion": "1699012800000", + "creationTimestamp": "2023-11-03T12:00:00Z" + }, + "spec": { + "description": "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "pdfTables", + "resourceVersion": "1699272000000", + "creationTimestamp": "2023-11-06T12:00:00Z" + }, + "spec": { + "description": "Enables generating table data as PDF in reporting", + "stage": "preview", + "codeowner": "@grafana/sharing-squad" + } + }, + { + "metadata": { + "name": "ssoSettingsApi", + "resourceVersion": "1699444800000", + "creationTimestamp": "2023-11-08T12:00:00Z" + }, + "spec": { + "description": "Enables the SSO settings API", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team" + } + }, + { + "metadata": { + "name": "canvasPanelPanZoom", + "resourceVersion": "1703678400000", + "creationTimestamp": "2023-12-27T12:00:00Z" + }, + "spec": { + "description": "Allow pan and zoom in canvas panel", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "logsInfiniteScrolling", + "resourceVersion": "1699531200000", + "creationTimestamp": "2023-11-09T12:00:00Z" + }, + "spec": { + "description": "Enables infinite scrolling for the Logs panel in Explore and Dashboards", + "stage": "experimental", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, + { + "metadata": { + "name": "flameGraphItemCollapsing", + "resourceVersion": "1699531200000", + "creationTimestamp": "2023-11-09T12:00:00Z" + }, + "spec": { + "description": "Allow collapsing of flame graph items", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, + { + "metadata": { + "name": "alertingDetailsViewV2", + "resourceVersion": "1699531200000", + "creationTimestamp": "2023-11-09T12:00:00Z" + }, + "spec": { + "description": "Enables the preview of the new alert details view", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "frontend": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "datatrails", + "resourceVersion": "1700049600000", + "creationTimestamp": "2023-11-15T12:00:00Z" + }, + "spec": { + "description": "Enables the new core app datatrails", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "alertingSimplifiedRouting", + "resourceVersion": "1699617600000", + "creationTimestamp": "2023-11-10T12:00:00Z" + }, + "spec": { + "description": "Enables the simplified routing for alerting", + "stage": "experimental", + "codeowner": "@grafana/alerting-squad", + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "logRowsPopoverMenu", + "resourceVersion": "1700136000000", + "creationTimestamp": "2023-11-16T12:00:00Z" + }, + "spec": { + "description": "Enable filtering menu displayed when text of a log line is selected", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, + { + "metadata": { + "name": "pluginsSkipHostEnvVars", + "resourceVersion": "1700049600000", + "creationTimestamp": "2023-11-15T12:00:00Z" + }, + "spec": { + "description": "Disables passing host environment variable to plugin processes", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend" + } + }, + { + "metadata": { + "name": "tableSharedCrosshair", + "resourceVersion": "1702382400000", + "creationTimestamp": "2023-12-12T12:00:00Z" + }, + "spec": { + "description": "Enables shared crosshair in table panel", + "stage": "experimental", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "regressionTransformation", + "resourceVersion": "1700827200000", + "creationTimestamp": "2023-11-24T12:00:00Z" + }, + "spec": { + "description": "Enables regression analysis transformation", + "stage": "preview", + "codeowner": "@grafana/grafana-bi-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "displayAnonymousStats", + "resourceVersion": "1701259200000", + "creationTimestamp": "2023-11-29T12:00:00Z" + }, + "spec": { + "description": "Enables anonymous stats to be shown in the UI for Grafana", + "stage": "GA", + "codeowner": "@grafana/identity-access-team", + "frontend": true + } + }, + { + "metadata": { + "name": "lokiQueryHints", + "resourceVersion": "1702900800000", + "creationTimestamp": "2023-12-18T12:00:00Z" + }, + "spec": { + "description": "Enables query hints for Loki", + "stage": "GA", + "codeowner": "@grafana/observability-logs", + "frontend": true + } + }, + { + "metadata": { + "name": "kubernetesFeatureToggles", + "resourceVersion": "1703216580000", + "creationTimestamp": "2023-12-22T03:43:00Z" + }, + "spec": { + "description": "Use the kubernetes API for feature toggle management in the frontend", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad", + "frontend": true, + "hideFromAdminPage": true + } + }, + { + "metadata": { + "name": "alertingPreviewUpgrade", + "resourceVersion": "1707425412785", + "creationTimestamp": "2024-01-03T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-08 20:50:12.785364 +0000 UTC" + } + }, + "spec": { + "description": "Show Unified Alerting preview and upgrade page in legacy alerting", + "stage": "GA", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "enablePluginsTracingByDefault", + "resourceVersion": "1704801600000", + "creationTimestamp": "2024-01-09T12:00:00Z" + }, + "spec": { + "description": "Enable plugin tracing for all external plugins", + "stage": "experimental", + "codeowner": "@grafana/plugins-platform-backend", + "requiresRestart": true + } + }, + { + "metadata": { + "name": "cloudRBACRoles", + "resourceVersion": "1704888000000", + "creationTimestamp": "2024-01-10T12:00:00Z" + }, + "spec": { + "description": "Enabled grafana cloud specific RBAC roles", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "requiresRestart": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "alertingQueryOptimization", + "resourceVersion": "1704888000000", + "creationTimestamp": "2024-01-10T12:00:00Z" + }, + "spec": { + "description": "Optimizes eligible queries in order to reduce load on datasources", + "stage": "GA", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "newFolderPicker", + "resourceVersion": "1705060800000", + "creationTimestamp": "2024-01-12T12:00:00Z" + }, + "spec": { + "description": "Enables the nested folder picker without having nested folders enabled", + "stage": "experimental", + "codeowner": "@grafana/grafana-frontend-platform", + "frontend": true + } + }, + { + "metadata": { + "name": "jitterAlertRulesWithinGroups", + "resourceVersion": "1705492800000", + "creationTimestamp": "2024-01-17T12:00:00Z" + }, + "spec": { + "description": "Distributes alert rule evaluations more evenly over time, including spreading out rules within the same group", + "stage": "preview", + "codeowner": "@grafana/alerting-squad", + "requiresRestart": true, + "hideFromDocs": true + } + }, + { + "metadata": { + "name": "onPremToCloudMigrations", + "resourceVersion": "1705894200000", + "creationTimestamp": "2024-01-22T03:30:00Z" + }, + "spec": { + "description": "In-development feature that will allow users to easily migrate their on-prem Grafana instances to Grafana Cloud.", + "stage": "experimental", + "codeowner": "@grafana/grafana-operator-experience-squad" + } + }, + { + "metadata": { + "name": "alertingSaveStatePeriodic", + "resourceVersion": "1705924800000", + "creationTimestamp": "2024-01-22T12:00:00Z" + }, + "spec": { + "description": "Writes the state periodically to the database, asynchronous to rule evaluation", + "stage": "privatePreview", + "codeowner": "@grafana/alerting-squad" + } + }, + { + "metadata": { + "name": "promQLScope", + "resourceVersion": "1706486400000", + "creationTimestamp": "2024-01-29T00:00:00Z" + }, + "spec": { + "description": "In-development feature that will allow injection of labels into prometheus queries.", + "stage": "experimental", + "codeowner": "@grafana/observability-metrics" + } + }, + { + "metadata": { + "name": "nodeGraphDotLayout", + "resourceVersion": "1704196800000", + "creationTimestamp": "2024-01-02T12:00:00Z" + }, + "spec": { + "description": "Changed the layout algorithm for the node graph", + "stage": "experimental", + "codeowner": "@grafana/observability-traces-and-profiling", + "frontend": true + } + }, + { + "metadata": { + "name": "groupToNestedTableTransformation", + "resourceVersion": "1707134400000", + "creationTimestamp": "2024-02-05T12:00:00Z" + }, + "spec": { + "description": "Enables the group to nested table transformation", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + }, + { + "metadata": { + "name": "newPDFRendering", + "resourceVersion": "1707425412785", + "creationTimestamp": "2024-02-08T20:50:12Z" + }, + "spec": { + "description": "New implementation for the dashboard to PDF rendering", + "stage": "experimental", + "codeowner": "@grafana/sharing-squad" + } + }, + { + "metadata": { + "name": "autoMigrateGraphPanel", + "resourceVersion": "1707433170195", + "creationTimestamp": "2024-02-08T22:59:30Z" + }, + "spec": { + "description": "Migrate old graph panel to supported time series panel - broken out from autoMigrateOldPanels to enable granular tracking", + "stage": "preview", + "codeowner": "@grafana/dataviz-squad", + "frontend": true + } + } + ] +} \ No newline at end of file diff --git a/pkg/services/featuremgmt/toggles_gen_test.go b/pkg/services/featuremgmt/toggles_gen_test.go index 9dd117f976f..946a52e4079 100644 --- a/pkg/services/featuremgmt/toggles_gen_test.go +++ b/pkg/services/featuremgmt/toggles_gen_test.go @@ -3,6 +3,7 @@ package featuremgmt import ( "bytes" "encoding/csv" + "encoding/json" "fmt" "html/template" "log" @@ -16,7 +17,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/olekukonko/tablewriter" "github.com/stretchr/testify/require" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + featuretoggleapi "github.com/grafana/grafana/pkg/apis/featuretoggle/v0alpha1" + "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/services/featuremgmt/strcase" ) @@ -26,6 +30,9 @@ func TestFeatureToggleFiles(t *testing.T) { } t.Run("check registry constraints", func(t *testing.T) { + invalidNames := make([]string, 0) + + // Check that all flags set in code are valid for _, flag := range standardFeatureFlags { if flag.Expression == "true" && !(flag.Stage == FeatureStageGeneralAvailability || flag.Stage == FeatureStageDeprecated) { t.Errorf("only FeatureStageGeneralAvailability or FeatureStageDeprecated features can be enabled by default. See: %s", flag.Name) @@ -45,19 +52,105 @@ func TestFeatureToggleFiles(t *testing.T) { if flag.AllowSelfServe && flag.Stage != FeatureStageGeneralAvailability { t.Errorf("only allow self-serving GA toggles") } - if flag.Created.Year() < 2021 { - t.Errorf("flag requires a reasonable created date. See: %s (%s)", - flag.Name, flag.Created.Format(time.DateOnly)) - } - } - }) - - t.Run("all new features should have an owner", func(t *testing.T) { - for _, flag := range standardFeatureFlags { if flag.Owner == "" { t.Errorf("feature %s does not have an owner. please fill the FeatureFlag.Owner property", flag.Name) } + // Check camel case names + if flag.Name != strcase.ToLowerCamel(flag.Name) && !legacyNames[flag.Name] { + invalidNames = append(invalidNames, flag.Name) + } } + + // Make sure the names are valid + require.Empty(t, invalidNames, "%s feature names should be camel cased", invalidNames) + // acronyms can be configured as needed via `ConfigureAcronym` function from `./strcase/camel.go` + + // Now that we know they are valid, update the json database + t.Run("update k8s resource list", func(t *testing.T) { + created := v1.NewTime(time.Now().UTC()) + resourceVersion := fmt.Sprintf("%d", created.UnixMilli()) + + featuresFile := "toggles_gen.json" + current := featuretoggleapi.FeatureList{ + TypeMeta: v1.TypeMeta{ + Kind: "FeatureList", + APIVersion: featuretoggleapi.APIVERSION, + }, + } + existing := featuretoggleapi.FeatureList{} + body, err := os.ReadFile(featuresFile) + if err == nil { + _ = json.Unmarshal(body, &existing) + current.ListMeta = existing.ListMeta + } + + lookup := map[string]featuretoggleapi.FeatureSpec{} + for _, flag := range standardFeatureFlags { + lookup[flag.Name] = featuretoggleapi.FeatureSpec{ + Description: flag.Description, + Stage: flag.Stage.String(), + Owner: string(flag.Owner), + RequiresDevMode: flag.RequiresDevMode, + FrontendOnly: flag.FrontendOnly, + RequiresRestart: flag.RequiresRestart, + AllowSelfServe: flag.AllowSelfServe, + HideFromAdminPage: flag.HideFromAdminPage, + HideFromDocs: flag.HideFromDocs, + // EnabledVersion: ???, + } + + // Replace them all + // current.Items = append(current.Items, featuretoggleapi.Feature{ + // ObjectMeta: v1.ObjectMeta{ + // Name: flag.Name, + // CreationTimestamp: v1.NewTime(flag.Created), + // ResourceVersion: fmt.Sprintf("%d", flag.Created.UnixMilli()), + // }, + // Spec: lookup[flag.Name], + // }) + // current.ListMeta.ResourceVersion = resourceVersion + } + + // Check for changes in any existing values + for _, item := range existing.Items { + v, ok := lookup[item.Name] + if ok { + delete(lookup, item.Name) + a, e1 := json.Marshal(v) + b, e2 := json.Marshal(item.Spec) + if e1 != nil || e2 != nil || !bytes.Equal(a, b) { + item.ResourceVersion = resourceVersion + if item.Annotations == nil { + item.Annotations = make(map[string]string) + } + item.Annotations[utils.AnnoKeyUpdatedTimestamp] = created.String() + item.Spec = v // the current value + } + } else { + item.DeletionTimestamp = &created + fmt.Printf("mark feature as deleted") + } + current.Items = append(current.Items, item) + } + + // New flags not in the existing list + for k, v := range lookup { + current.Items = append(current.Items, featuretoggleapi.Feature{ + ObjectMeta: v1.ObjectMeta{ + Name: k, + CreationTimestamp: created, + ResourceVersion: fmt.Sprintf("%d", created.UnixMilli()), + }, + Spec: v, + }) + } + + out, err := json.MarshalIndent(current, "", " ") + require.NoError(t, err) + + err = os.WriteFile(featuresFile, out, 0644) + require.NoError(t, err, "error writing file") + }) }) t.Run("verify files", func(t *testing.T) { @@ -85,22 +178,6 @@ func TestFeatureToggleFiles(t *testing.T) { generateCSV(), ) }) - - t.Run("check feature naming convention", func(t *testing.T) { - invalidNames := make([]string, 0) - for _, f := range standardFeatureFlags { - if legacyNames[f.Name] { - continue - } - - if f.Name != strcase.ToLowerCamel(f.Name) { - invalidNames = append(invalidNames, f.Name) - } - } - - require.Empty(t, invalidNames, "%s feature names should be camel cased", invalidNames) - // acronyms can be configured as needed via `ConfigureAcronym` function from `./strcase/camel.go` - }) } func verifyAndGenerateFile(t *testing.T, fpath string, gen string) { @@ -214,9 +291,8 @@ func generateCSV() string { w := csv.NewWriter(&buf) if err := w.Write([]string{ "Name", - "Stage", //flag.Stage.String(), - "Owner", //string(flag.Owner), - "Created", + "Stage", //flag.Stage.String(), + "Owner", //string(flag.Owner), "requiresDevMode", //strconv.FormatBool(flag.RequiresDevMode), "RequiresRestart", //strconv.FormatBool(flag.RequiresRestart), "FrontendOnly", //strconv.FormatBool(flag.FrontendOnly), @@ -224,19 +300,11 @@ func generateCSV() string { log.Fatalln("error writing record to csv:", err) } - dateFormatter := func(t time.Time) string { - if t.Year() < 2020 { // fake year - return "" - } - return t.Format(time.DateOnly) - } - for _, flag := range standardFeatureFlags { if err := w.Write([]string{ flag.Name, flag.Stage.String(), string(flag.Owner), - dateFormatter(flag.Created), strconv.FormatBool(flag.RequiresDevMode), strconv.FormatBool(flag.RequiresRestart), strconv.FormatBool(flag.FrontendOnly), From f0bbfc8422d79a6dce2e8f4a0191ae51fa8100e9 Mon Sep 17 00:00:00 2001 From: Kyle Cunningham Date: Sat, 10 Feb 2024 07:24:08 +0700 Subject: [PATCH 33/50] Chore: Move BI feature flags to Dataviz (#82224) --- pkg/services/featuremgmt/codeowners.go | 1 - pkg/services/featuremgmt/registry.go | 18 ++--- pkg/services/featuremgmt/toggles_gen.csv | 18 ++--- pkg/services/featuremgmt/toggles_gen.json | 81 +++++++++++++++-------- 4 files changed, 72 insertions(+), 46 deletions(-) diff --git a/pkg/services/featuremgmt/codeowners.go b/pkg/services/featuremgmt/codeowners.go index 42e0c139623..a303f74f9b2 100644 --- a/pkg/services/featuremgmt/codeowners.go +++ b/pkg/services/featuremgmt/codeowners.go @@ -8,7 +8,6 @@ const ( grafanaAppPlatformSquad codeowner = "@grafana/grafana-app-platform-squad" grafanaDashboardsSquad codeowner = "@grafana/dashboards-squad" grafanaExploreSquad codeowner = "@grafana/explore-squad" - grafanaBiSquad codeowner = "@grafana/grafana-bi-squad" grafanaDatavizSquad codeowner = "@grafana/dataviz-squad" grafanaFrontendPlatformSquad codeowner = "@grafana/grafana-frontend-platform" grafanaBackendPlatformSquad codeowner = "@grafana/backend-platform" diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 6d13d0bf0a0..83b7c1d919f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -282,7 +282,7 @@ var ( Description: "Enables drag and drop for CSV and Excel files", FrontendOnly: true, Stage: FeatureStageExperimental, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, }, { Name: "alertingNoNormalState", @@ -464,7 +464,7 @@ var ( Description: "Enables the edit functionality in the datagrid panel", FrontendOnly: true, Stage: FeatureStagePublicPreview, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, }, { Name: "extraThemes", @@ -506,7 +506,7 @@ var ( Description: "Enables previous SQL data source dataset dropdown behavior", FrontendOnly: true, Stage: FeatureStagePublicPreview, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, HideFromAdminPage: true, }, { @@ -800,14 +800,14 @@ var ( Description: "Enable format string transformer", Stage: FeatureStagePublicPreview, FrontendOnly: true, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, }, { Name: "transformationsVariableSupport", Description: "Allows using variables in transformations", FrontendOnly: true, Stage: FeatureStagePublicPreview, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, }, { Name: "kubernetesPlaylists", @@ -907,7 +907,7 @@ var ( Description: "Add cumulative and window functions to the add field from calculation transformation", Stage: FeatureStagePublicPreview, FrontendOnly: true, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, }, { Name: "alertmanagerRemoteSecondary", @@ -939,7 +939,7 @@ var ( Description: "Make sure extracted field names are unique in the dataframe", Stage: FeatureStageExperimental, FrontendOnly: true, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, }, { Name: "dashboardSceneForViewers", @@ -1042,14 +1042,14 @@ var ( Description: "Enables shared crosshair in table panel", FrontendOnly: true, Stage: FeatureStageExperimental, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, }, { Name: "regressionTransformation", Description: "Enables regression analysis transformation", Stage: FeatureStagePublicPreview, FrontendOnly: true, - Owner: grafanaBiSquad, + Owner: grafanaDatavizSquad, }, { Name: "displayAnonymousStats", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index c88be17ca78..b6414f26aa7 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -35,7 +35,7 @@ accessControlOnCall,preview,@grafana/identity-access-team,false,false,false nestedFolders,preview,@grafana/backend-platform,false,false,false nestedFolderPicker,GA,@grafana/grafana-frontend-platform,false,false,true alertingBacktesting,experimental,@grafana/alerting-squad,false,false,false -editPanelCSVDragAndDrop,experimental,@grafana/grafana-bi-squad,false,false,true +editPanelCSVDragAndDrop,experimental,@grafana/dataviz-squad,false,false,true alertingNoNormalState,preview,@grafana/alerting-squad,false,false,false logsContextDatasourceUi,GA,@grafana/observability-logs,false,false,true lokiQuerySplitting,GA,@grafana/observability-logs,false,false,true @@ -60,13 +60,13 @@ externalServiceAuth,experimental,@grafana/identity-access-team,true,false,false refactorVariablesTimeRange,preview,@grafana/dashboards-squad,false,false,false enableElasticsearchBackendQuerying,GA,@grafana/observability-logs,false,false,false faroDatasourceSelector,preview,@grafana/app-o11y,false,false,true -enableDatagridEditing,preview,@grafana/grafana-bi-squad,false,false,true +enableDatagridEditing,preview,@grafana/dataviz-squad,false,false,true extraThemes,experimental,@grafana/grafana-frontend-platform,false,false,true lokiPredefinedOperations,experimental,@grafana/observability-logs,false,false,true pluginsFrontendSandbox,experimental,@grafana/plugins-platform-backend,false,false,true dashboardEmbed,experimental,@grafana/grafana-as-code,false,false,true frontendSandboxMonitorOnly,experimental,@grafana/plugins-platform-backend,false,false,true -sqlDatasourceDatabaseSelection,preview,@grafana/grafana-bi-squad,false,false,true +sqlDatasourceDatabaseSelection,preview,@grafana/dataviz-squad,false,false,true lokiFormatQuery,experimental,@grafana/observability-logs,false,false,true cloudWatchLogsMonacoEditor,GA,@grafana/aws-datasources,false,false,true exploreScrollableLogsContainer,experimental,@grafana/observability-logs,false,false,true @@ -105,8 +105,8 @@ cloudWatchWildCardDimensionValues,GA,@grafana/aws-datasources,false,false,false externalServiceAccounts,preview,@grafana/identity-access-team,false,false,false panelMonitoring,experimental,@grafana/dataviz-squad,false,false,true enableNativeHTTPHistogram,experimental,@grafana/hosted-grafana-team,false,false,false -formatString,preview,@grafana/grafana-bi-squad,false,false,true -transformationsVariableSupport,preview,@grafana/grafana-bi-squad,false,false,true +formatString,preview,@grafana/dataviz-squad,false,false,true +transformationsVariableSupport,preview,@grafana/dataviz-squad,false,false,true kubernetesPlaylists,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesQueryServiceRewrite,experimental,@grafana/grafana-app-platform-squad,true,true,false @@ -120,12 +120,12 @@ panelTitleSearchInV1,experimental,@grafana/backend-platform,true,false,false pluginsInstrumentationStatusSource,experimental,@grafana/plugins-platform-backend,false,false,false managedPluginsInstall,preview,@grafana/plugins-platform-backend,false,false,false prometheusPromQAIL,experimental,@grafana/observability-metrics,false,false,true -addFieldFromCalculationStatFunctions,preview,@grafana/grafana-bi-squad,false,false,true +addFieldFromCalculationStatFunctions,preview,@grafana/dataviz-squad,false,false,true alertmanagerRemoteSecondary,experimental,@grafana/alerting-squad,false,false,false alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false annotationPermissionUpdate,experimental,@grafana/identity-access-team,false,false,false -extractFieldsNameDeduplication,experimental,@grafana/grafana-bi-squad,false,false,true +extractFieldsNameDeduplication,experimental,@grafana/dataviz-squad,false,false,true dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,true dashboardScene,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true @@ -139,8 +139,8 @@ datatrails,experimental,@grafana/dashboards-squad,false,false,true alertingSimplifiedRouting,experimental,@grafana/alerting-squad,false,false,false logRowsPopoverMenu,GA,@grafana/observability-logs,false,false,true pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,false,false,false -tableSharedCrosshair,experimental,@grafana/grafana-bi-squad,false,false,true -regressionTransformation,preview,@grafana/grafana-bi-squad,false,false,true +tableSharedCrosshair,experimental,@grafana/dataviz-squad,false,false,true +regressionTransformation,preview,@grafana/dataviz-squad,false,false,true displayAnonymousStats,GA,@grafana/identity-access-team,false,false,true lokiQueryHints,GA,@grafana/observability-logs,false,false,true kubernetesFeatureToggles,experimental,@grafana/grafana-operator-experience-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 2d16608d938..08f37910cda 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -456,13 +456,16 @@ { "metadata": { "name": "editPanelCSVDragAndDrop", - "resourceVersion": "1671537600000", - "creationTimestamp": "2022-12-20T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2022-12-20T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Enables drag and drop for CSV and Excel files", "stage": "experimental", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, @@ -775,13 +778,16 @@ { "metadata": { "name": "enableDatagridEditing", - "resourceVersion": "1682337600000", - "creationTimestamp": "2023-04-24T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2023-04-24T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Enables the edit functionality in the datagrid panel", "stage": "preview", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, @@ -853,13 +859,16 @@ { "metadata": { "name": "sqlDatasourceDatabaseSelection", - "resourceVersion": "1686052800000", - "creationTimestamp": "2023-06-06T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2023-06-06T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Enables previous SQL data source dataset dropdown behavior", "stage": "preview", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true, "hideFromAdminPage": true } @@ -1356,26 +1365,32 @@ { "metadata": { "name": "formatString", - "resourceVersion": "1697198400000", - "creationTimestamp": "2023-10-13T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2023-10-13T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Enable format string transformer", "stage": "preview", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { "name": "transformationsVariableSupport", - "resourceVersion": "1696420800000", - "creationTimestamp": "2023-10-04T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2023-10-04T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Allows using variables in transformations", "stage": "preview", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, @@ -1549,13 +1564,16 @@ { "metadata": { "name": "addFieldFromCalculationStatFunctions", - "resourceVersion": "1699012800000", - "creationTimestamp": "2023-11-03T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2023-11-03T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Add cumulative and window functions to the add field from calculation transformation", "stage": "preview", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, @@ -1610,13 +1628,16 @@ { "metadata": { "name": "extractFieldsNameDeduplication", - "resourceVersion": "1698926400000", - "creationTimestamp": "2023-11-02T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2023-11-02T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Make sure extracted field names are unique in the dataframe", "stage": "experimental", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, @@ -1792,26 +1813,32 @@ { "metadata": { "name": "tableSharedCrosshair", - "resourceVersion": "1702382400000", - "creationTimestamp": "2023-12-12T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2023-12-12T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Enables shared crosshair in table panel", "stage": "experimental", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, { "metadata": { "name": "regressionTransformation", - "resourceVersion": "1700827200000", - "creationTimestamp": "2023-11-24T12:00:00Z" + "resourceVersion": "1707523537136", + "creationTimestamp": "2023-11-24T12:00:00Z", + "annotations": { + "grafana.app/updatedTimestamp": "2024-02-10 00:05:37.1362 +0000 UTC" + } }, "spec": { "description": "Enables regression analysis transformation", "stage": "preview", - "codeowner": "@grafana/grafana-bi-squad", + "codeowner": "@grafana/dataviz-squad", "frontend": true } }, From 02c0f5929ca6c7fe22c3420ee0314688546c3f77 Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Sat, 10 Feb 2024 09:57:11 -0500 Subject: [PATCH 34/50] prometheus: fix: use shallow clone of scopedVars (#82280) fix: use shallow clone of scopedVars --- packages/grafana-prometheus/src/datasource.ts | 4 ++-- public/app/plugins/datasource/prometheus/datasource.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 5b3e9ec7f27..5a0264a4571 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -1,4 +1,4 @@ -import { cloneDeep, defaults } from 'lodash'; +import { defaults } from 'lodash'; import { lastValueFrom, Observable, throwError } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import semver from 'semver/preload'; @@ -877,7 +877,7 @@ export class PrometheusDatasource // Used when running queries through backend applyTemplateVariables(target: PromQuery, scopedVars: ScopedVars, filters?: AdHocVariableFilter[]) { - const variables = cloneDeep(scopedVars); + const variables = { ...scopedVars }; // We want to interpolate these variables on backend. // The pre-calculated values are replaced withe the variable strings. diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 5b3e9ec7f27..5a0264a4571 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -1,4 +1,4 @@ -import { cloneDeep, defaults } from 'lodash'; +import { defaults } from 'lodash'; import { lastValueFrom, Observable, throwError } from 'rxjs'; import { map, tap } from 'rxjs/operators'; import semver from 'semver/preload'; @@ -877,7 +877,7 @@ export class PrometheusDatasource // Used when running queries through backend applyTemplateVariables(target: PromQuery, scopedVars: ScopedVars, filters?: AdHocVariableFilter[]) { - const variables = cloneDeep(scopedVars); + const variables = { ...scopedVars }; // We want to interpolate these variables on backend. // The pre-calculated values are replaced withe the variable strings. From fe6d1460b09b403fc74fd20e95f61513eede2555 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 11 Feb 2024 09:08:47 +0100 Subject: [PATCH 35/50] DashboardScene: Adds solo page that uses dasboarde scene to render single panel (#77940) * DashboardScene: Adds solo page that uses dasboarde scene to render single panel * Update * Panel and row repeats working * Update * added e2e tests * Refactor * Fixes * Fix e2e * fix * fix * fix --- .../feature-toggles/index.md | 1 + e2e/various-suite/solo-route.spec.ts | 30 +++++++ .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 7 ++ pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 13 +++ .../pages/DashboardScenePageStateManager.ts | 8 +- .../dashboard-scene/solo/SoloPanelPage.tsx | 63 ++++++++++++++ .../dashboard-scene/solo/useSoloPanel.ts | 84 +++++++++++++++++++ public/app/routes/routes.tsx | 15 +--- 11 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 public/app/features/dashboard-scene/solo/SoloPanelPage.tsx create mode 100644 public/app/features/dashboard-scene/solo/useSoloPanel.ts diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 6cda1d14fe9..afc76be7de0 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -159,6 +159,7 @@ Experimental features might be changed or removed without prior notice. | `annotationPermissionUpdate` | Separate annotation permissions from dashboard permissions to allow for more granular control. | | `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe | | `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles | +| `dashboardSceneSolo` | Enables rendering dashboards using scenes for solo panels | | `dashboardScene` | Enables dashboard rendering using scenes for all roles | | `ssoSettingsApi` | Enables the SSO settings API | | `logsInfiniteScrolling` | Enables infinite scrolling for the Logs panel in Explore and Dashboards | diff --git a/e2e/various-suite/solo-route.spec.ts b/e2e/various-suite/solo-route.spec.ts index 1cfba1b70b7..c508ace3aef 100644 --- a/e2e/various-suite/solo-route.spec.ts +++ b/e2e/various-suite/solo-route.spec.ts @@ -11,4 +11,34 @@ describe('Solo Route', () => { cy.get('canvas').should('have.length', 6); }); + + it('Can view solo panel in scenes', () => { + // open Panel Tests - Graph NG + e2e.pages.SoloPanel.visit( + 'TkZXxlNG3/panel-tests-graph-ng?orgId=1&from=1699954597665&to=1699956397665&panelId=54&__feature.dashboardSceneSolo=true' + ); + + e2e.components.Panels.Panel.title('Interpolation: Step before').should('exist'); + cy.contains('uplot-main-div').should('not.exist'); + }); + + it('Can view solo repeated panel in scenes', () => { + // open Panel Tests - Graph NG + e2e.pages.SoloPanel.visit( + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true' + ); + + e2e.components.Panels.Panel.title('server=B').should('exist'); + cy.contains('uplot-main-div').should('not.exist'); + }); + + it('Can view solo in repeaterd row and panel in scenes', () => { + // open Panel Tests - Graph NG + e2e.pages.SoloPanel.visit( + 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-2-row-2-clone-2&__feature.dashboardSceneSolo=true' + ); + + e2e.components.Panels.Panel.title('server = D, pod = Sod').should('exist'); + cy.contains('uplot-main-div').should('not.exist'); + }); }); diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b87e766734a..59acae823fa 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -146,6 +146,7 @@ export interface FeatureToggles { annotationPermissionUpdate?: boolean; extractFieldsNameDeduplication?: boolean; dashboardSceneForViewers?: boolean; + dashboardSceneSolo?: boolean; dashboardScene?: boolean; panelFilterVariable?: boolean; pdfTables?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 83b7c1d919f..cb84847fa25 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -948,6 +948,13 @@ var ( FrontendOnly: true, Owner: grafanaDashboardsSquad, }, + { + Name: "dashboardSceneSolo", + Description: "Enables rendering dashboards using scenes for solo panels", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaDashboardsSquad, + }, { Name: "dashboardScene", Description: "Enables dashboard rendering using scenes for all roles", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b6414f26aa7..5d693934c4e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -127,6 +127,7 @@ alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false annotationPermissionUpdate,experimental,@grafana/identity-access-team,false,false,false extractFieldsNameDeduplication,experimental,@grafana/dataviz-squad,false,false,true dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,true +dashboardSceneSolo,experimental,@grafana/dashboards-squad,false,false,true dashboardScene,experimental,@grafana/dashboards-squad,false,false,true panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true pdfTables,preview,@grafana/sharing-squad,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 4707f9d8afb..7456a13c927 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -519,6 +519,10 @@ const ( // Enables dashboard rendering using Scenes for viewer roles FlagDashboardSceneForViewers = "dashboardSceneForViewers" + // FlagDashboardSceneSolo + // Enables rendering dashboards using scenes for solo panels + FlagDashboardSceneSolo = "dashboardSceneSolo" + // FlagDashboardScene // Enables dashboard rendering using scenes for all roles FlagDashboardScene = "dashboardScene" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 08f37910cda..e80566d3192 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2050,6 +2050,19 @@ "codeowner": "@grafana/dataviz-squad", "frontend": true } + }, + { + "metadata": { + "name": "dashboardSceneSolo", + "resourceVersion": "1707577534071", + "creationTimestamp": "2024-02-10T15:05:34Z" + }, + "spec": { + "description": "Enables rendering dashboards using scenes for solo panels", + "stage": "experimental", + "codeowner": "@grafana/dashboards-squad", + "frontend": true + } } ] } \ No newline at end of file diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index eea3b3816bb..6fc3934e65e 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -202,7 +202,13 @@ export class DashboardScenePageStateManager extends StateManagerBase {} + +/** + * Used for iframe embedding and image rendering of single panels + */ +export function SoloPanelPage({ match, queryParams }: Props) { + const stateManager = getDashboardScenePageStateManager(); + const { dashboard } = stateManager.useState(); + + useEffect(() => { + stateManager.loadDashboard({ uid: match.params.uid!, route: DashboardRoutes.Embedded }); + return () => stateManager.clearState(); + }, [stateManager, match, queryParams]); + + if (!queryParams.panelId) { + return ; + } + + if (!dashboard) { + return ; + } + + return ; +} + +export default SoloPanelPage; + +export function SoloPanelRenderer({ dashboard, panelId }: { dashboard: DashboardScene; panelId: string }) { + const [panel, error] = useSoloPanel(dashboard, panelId); + + if (error) { + return ; + } + + if (!panel) { + return ( + + Loading + + ); + } + + return ( +
+ +
+ ); +} diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.ts b/public/app/features/dashboard-scene/solo/useSoloPanel.ts new file mode 100644 index 00000000000..9ca7d2139bd --- /dev/null +++ b/public/app/features/dashboard-scene/solo/useSoloPanel.ts @@ -0,0 +1,84 @@ +import { useState, useEffect } from 'react'; + +import { VizPanel, SceneObject, SceneGridRow, getUrlSyncManager } from '@grafana/scenes'; + +import { DashboardScene } from '../scene/DashboardScene'; +import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; +import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; +import { DashboardRepeatsProcessedEvent } from '../scene/types'; +import { findVizPanelByKey, isPanelClone } from '../utils/utils'; + +export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPanel | undefined, string | undefined] { + const [panel, setPanel] = useState(); + const [error, setError] = useState(); + + useEffect(() => { + getUrlSyncManager().initSync(dashboard); + + const cleanUp = dashboard.activate(); + + const panel = findVizPanelByKey(dashboard, panelId); + if (panel) { + activateParents(panel); + setPanel(panel); + } else if (isPanelClone(panelId)) { + findRepeatClone(dashboard, panelId).then((panel) => { + if (panel) { + setPanel(panel); + } else { + setError('Panel not found'); + } + }); + } + + return cleanUp; + }, [dashboard, panelId]); + + return [panel, error]; +} + +function activateParents(panel: VizPanel) { + let parent = panel.parent; + + while (parent && !parent.isActive) { + parent.activate(); + parent = parent.parent; + } +} + +function findRepeatClone(dashboard: DashboardScene, panelId: string): Promise { + return new Promise((resolve) => { + dashboard.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { + const panel = findVizPanelByKey(dashboard, panelId); + if (panel) { + resolve(panel); + } else { + // If rows are repeated they could add new panel repeaters that needs to be activated + activateAllRepeaters(dashboard.state.body); + } + }); + + activateAllRepeaters(dashboard.state.body); + }); +} + +function activateAllRepeaters(layout: SceneObject) { + layout.forEachChild((child) => { + if (child instanceof PanelRepeaterGridItem && !child.isActive) { + child.activate(); + return; + } + + if (child instanceof SceneGridRow && child.state.$behaviors) { + for (const behavior of child.state.$behaviors) { + if (behavior instanceof RowRepeaterBehavior && !child.isActive) { + child.activate(); + break; + } + } + + // Activate any panel PanelRepeaterGridItem inside the row + activateAllRepeaters(child); + } + }); +} diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 20c5d5e8fc8..846e354fb43 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -85,8 +85,10 @@ export function getAppRoutes(): RouteDescriptor[] { pageClass: 'dashboard-solo', routeName: DashboardRoutes.Normal, chromeless: true, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') + component: SafeDynamicImport(() => + config.featureToggles.dashboardSceneSolo + ? import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard-scene/solo/SoloPanelPage') + : import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') ), }, // This route handles embedding of snapshot/scripted dashboard panels @@ -99,15 +101,6 @@ export function getAppRoutes(): RouteDescriptor[] { () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') ), }, - { - path: '/d-solo/:uid', - pageClass: 'dashboard-solo', - routeName: DashboardRoutes.Normal, - chromeless: true, - component: SafeDynamicImport( - () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') - ), - }, { path: '/dashboard/import', component: SafeDynamicImport( From fcf2543fe38263393f506e42e87b3c9a7be0d142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A1bor=20Farkas?= Date: Mon, 12 Feb 2024 09:14:03 +0100 Subject: [PATCH 36/50] updated grafana-plugin-sdk-go dependency (#82136) update grafana-plugin-sdk-go dependency --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c5a3d492415..03d2dee4c87 100644 --- a/go.mod +++ b/go.mod @@ -63,7 +63,7 @@ require ( github.com/grafana/cuetsy v0.1.11 // @grafana/grafana-as-code github.com/grafana/grafana-aws-sdk v0.23.1 // @grafana/aws-datasources github.com/grafana/grafana-azure-sdk-go v1.12.0 // @grafana/partner-datasources - github.com/grafana/grafana-plugin-sdk-go v0.208.0 // @grafana/plugins-platform-backend + github.com/grafana/grafana-plugin-sdk-go v0.209.0 // @grafana/plugins-platform-backend github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // @grafana/backend-platform github.com/hashicorp/go-hclog v1.6.2 // @grafana/plugins-platform-backend github.com/hashicorp/go-plugin v1.6.0 // @grafana/plugins-platform-backend diff --git a/go.sum b/go.sum index 6c38639b981..81402e4e444 100644 --- a/go.sum +++ b/go.sum @@ -1946,8 +1946,8 @@ github.com/grafana/grafana-google-sdk-go v0.1.0/go.mod h1:Vo2TKWfDVmNTELBUM+3lkr github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79 h1:r+mU5bGMzcXCRVAuOrTn54S80qbfVkvTdUJZfSfTNbs= github.com/grafana/grafana-openapi-client-go v0.0.0-20231213163343-bd475d63fb79/go.mod h1:wc6Hbh3K2TgCUSfBC/BOzabItujtHMESZeFk5ZhdxhQ= github.com/grafana/grafana-plugin-sdk-go v0.114.0/go.mod h1:D7x3ah+1d4phNXpbnOaxa/osSaZlwh9/ZUnGGzegRbk= -github.com/grafana/grafana-plugin-sdk-go v0.208.0 h1:+cHmkoayG+nkqwbyQ9uVoRZJFcyLuZafwkTaeRO/r8U= -github.com/grafana/grafana-plugin-sdk-go v0.208.0/go.mod h1:RpVdugdeeNmo/DUQdFRbsBrIwDg+igNHSNsaUqiXdEc= +github.com/grafana/grafana-plugin-sdk-go v0.209.0 h1:izPAnJePvzqrpJ3/X3eCbESnxR2bPqx32Q92glYHmkU= +github.com/grafana/grafana-plugin-sdk-go v0.209.0/go.mod h1:RpVdugdeeNmo/DUQdFRbsBrIwDg+igNHSNsaUqiXdEc= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482 h1:1YNoeIhii4UIIQpCPU+EXidnqf449d0C3ZntAEt4KSo= github.com/grafana/kindsys v0.0.0-20230508162304-452481b63482/go.mod h1:GNcfpy5+SY6RVbNGQW264gC0r336Dm+0zgQ5vt6+M8Y= github.com/grafana/prometheus-alertmanager v0.25.1-0.20240208102907-e82436ce63e6 h1:CBm0rwLCPDyarg9/bHJ50rBLYmyMDoyCWpgRMITZhdA= From 815e61258c2cd755440a0d64a33feb005153d4d7 Mon Sep 17 00:00:00 2001 From: Kim Nylander <104772500+knylander-grafana@users.noreply.github.com> Date: Mon, 12 Feb 2024 03:34:36 -0500 Subject: [PATCH 37/50] [DOC] Update Pyroscope data source (#82130) Co-authored-by: Jack Baldry --- docs/sources/datasources/pyroscope/_index.md | 80 +++++++++++++++ .../configure-pyroscope-data-source.md} | 99 ++----------------- .../pyroscope/query-profile-data.md | 82 +++++++++++++++ .../datasources/tempo-traces-to-profiles.md | 8 +- 4 files changed, 174 insertions(+), 95 deletions(-) create mode 100644 docs/sources/datasources/pyroscope/_index.md rename docs/sources/datasources/{grafana-pyroscope.md => pyroscope/configure-pyroscope-data-source.md} (50%) create mode 100644 docs/sources/datasources/pyroscope/query-profile-data.md diff --git a/docs/sources/datasources/pyroscope/_index.md b/docs/sources/datasources/pyroscope/_index.md new file mode 100644 index 00000000000..88d5a1c88be --- /dev/null +++ b/docs/sources/datasources/pyroscope/_index.md @@ -0,0 +1,80 @@ +--- +aliases: + - ../features/datasources/phlare/ # /docs/grafana//features/datasources/phlare/ + - ../features/datasources/grafana-pyroscope/ # /docs/grafana//features/datasources/grafana-pyroscope/ + - ../datasources/grafana-pyroscope/ # /docs/grafana//datasources/grafana-pyroscope/ +description: Horizontally-scalable, highly-available, multi-tenant continuous profiling + aggregation system. OSS profiling solution from Grafana Labs. +keywords: + - grafana + - phlare + - guide + - profiling + - pyroscope +labels: + products: + - cloud + - enterprise + - oss +title: Grafana Pyroscope +weight: 1150 +--- + +# Grafana Pyroscope data source + +Grafana Pyroscope is a horizontally scalable, highly available, multi-tenant, OSS, continuous profiling aggregation system. Add it as a data source, and you are ready to query your profiles in [Explore][explore]. + +To learn more about profiling and Pyroscope, refer to the [Introduction to Pyroscope](/docs/pyroscope/introduction/). + +For information on configuring the Pyroscope data source, refer to [Configure the Grafana Pyroscope data source](./configure-pyroscope-data-source). + +## Integrate profiles into dashboards + +Using the Pyroscope data source, you can integrate profiles into your dashboards. +In this case, the screenshot shows memory profiles alongside panels for logs and metrics to be able to debug out of memory (OOM) errors alongside the associated logs and metrics. + +![dashboard](https://grafana.com/static/img/pyroscope/grafana-pyroscope-dashboard-2023-11-30.png) + +## Visualize traces and profiles data using Traces to profiles + +You can link profile and tracing data using your Pyroscope data source with the Tempo data source. + +Combined traces and profiles let you see granular line-level detail when available for a trace span. This allows you pinpoint the exact function that's causing a bottleneck in your application as well as a specific request. + +![trace-profiler-view](https://grafana.com/static/img/pyroscope/pyroscope-trace-profiler-view-2023-11-30.png) + +For more information, refer to the [Traces to profile section][configure-tempo-data-source] of the Tempo data source documentation. + +{{< youtube id="AG8VzfFMLxo" >}} + +## Provision the Grafana Pyroscope data source + +You can modify the Grafana configuration files to provision the Grafana Pyroscope data source. +To learn more, and to view the available provisioning settings, refer to [provisioning documentation][provisioning-data-sources]. + +Here is an example configuration: + +```yaml +apiVersion: 1 + +datasources: + - name: Grafana Pyroscope + type: grafana-pyroscope-datasource + url: http://localhost:4040 + jsonData: + minStep: '15s' +``` + +{{% docs/reference %}} +[explore]: "/docs/grafana/ -> /docs/grafana//explore" +[explore]: "/docs/grafana-cloud/ -> /docs/grafana//explore" + +[flame-graph]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/flame-graph" +[flame-graph]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/flame-graph" + +[provisioning-data-sources]: "/docs/grafana/ -> /docs/grafana//administration/provisioning#datasources" +[provisioning-data-sources]: "/docs/grafana-cloud/ -> /docs/grafana//administration/provisioning#datasources" + +[configure-tempo-data-source]: "/docs/grafana/ -> /docs/grafana//datasources/tempo/configure-tempo-data-source" +[configure-tempo-data-source]: "/docs/grafana-cloud/ -> docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/configure-tempo-data-source" +{{% /docs/reference %}} diff --git a/docs/sources/datasources/grafana-pyroscope.md b/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md similarity index 50% rename from docs/sources/datasources/grafana-pyroscope.md rename to docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md index 014b36f170e..8503f2dfa45 100644 --- a/docs/sources/datasources/grafana-pyroscope.md +++ b/docs/sources/datasources/pyroscope/configure-pyroscope-data-source.md @@ -1,13 +1,7 @@ --- -aliases: - - ../features/datasources/phlare/ - - ../features/datasources/grafana-pyroscope/ -description: Horizontally-scalable, highly-available, multi-tenant continuous profiling - aggregation system. OSS profiling solution from Grafana Labs. +description: Configure your Pyroscope data source for Grafana. keywords: - - grafana - - phlare - - guide + - configure - profiling - pyroscope labels: @@ -15,15 +9,12 @@ labels: - cloud - enterprise - oss -title: Grafana Pyroscope -weight: 1150 +title: Configure the Grafana Pyroscope data source +menuTitle: Configure Pyroscope +weight: 200 --- -# Grafana Pyroscope data source - -Grafana Pyroscope is a horizontally scalable, highly available, multi-tenant, OSS, continuous profiling aggregation system. Add it as a data source, and you are ready to query your profiles in [Explore][explore]. - -## Configure the Grafana Pyroscope data source +# Configure the Grafana Pyroscope data source To configure basic settings for the data source, complete the following steps: @@ -44,84 +35,6 @@ To configure basic settings for the data source, complete the following steps: | `Password` | Password for basic authentication. | | `Minimal step` | Used for queries returning timeseries data. The Pyroscope backend, similar to Prometheus, scrapes profiles at certain intervals. To prevent querying at smaller interval, use Minimal step same or higher than your Pyroscope scrape interval. This prevents returning too many data points to the frontend. | -### Traces to profiles - -You can link profile and tracing data using your Pyroscope data source with the Tempo data source. -For more information, refer to the [Traces to profile section][configure-tempo-data-source] of the Tempo data source documentation. - -{{< youtube id="AG8VzfFMLxo" >}} - -## Querying - -You can query your profiling data using the query editor. - -### Query editor - -The query editor gives you access to a profile type selector, a label selector, and collapsible options. - -![Query editor](/media/docs/pyroscope/query-editor/query-editor.png 'Query editor') - -To access the query editor: - -1. Sign into Grafana or Grafana Cloud. -1. Select your Pyroscope data source. -1. From the menu, choose **Explore**. - -1. Select a profile type from the drop-down menu. - - {{< figure src="/media/docs/pyroscope/query-editor/select-profile.png" class="docs-image--no-shadow" max-width="450px" caption="Profile selector" >}} - -1. Use the labels selector input to filter by labels. Pyroscope uses similar syntax to Prometheus to filter labels. - Refer to [Pyroscope documentation](https://grafana.com/docs/pyroscope/latest/) for available operators and syntax. - - While the label selector can be left empty to query all profiles without filtering by labels, the profile type or app must be selected for the query to be valid. - - Grafana doesn't show any data if the profile type or app isn’t selected when a query runs. - - ![Labels selector](/media/docs/pyroscope/query-editor/labels-selector.png 'Labels selector') - -1. Expand the **Options** section to view **Query Type** and **Group by**. - ![Options section](/media/docs/pyroscope/query-editor/options-section.png 'Options section') - -1. Select a query type to return the profile data which can be shown in the [Flame Graph][flame-graph], metric data visualized in a graph, or both. You can only select both options in a dashboard, because panels allow only one visualization. - -**Group by** allows you to group metric data by a specified label. Without any **Group by** label, metric data is aggregated over all the labels into single time series. You can use multiple labels to group by. Group by has only an effect on the metric data and doesn't change the profile data results. - -### Profiles query results - -Profiles can be visualized in a flame graph. See the [Flame Graph documentation][flame-graph] to learn about the visualization and its features. - -![Flame graph](/media/docs/pyroscope/query-editor/flame-graph.png 'Flame graph') - -Pyroscope returns profiles aggregated over a selected time range. -The absolute values in the flame graph grow as the time range gets bigger while keeping the relative values meaningful. -You can zoom in on the time range to get a higher granularity profile up to the point of a single scrape interval. - -### Metrics query results - -Metrics results represent the aggregated sum value over time of the selected profile type. - -![Metrics graph](/media/docs/pyroscope/query-editor/metric-graph.png 'Metrics graph') - -This allows you to quickly see any spikes in the value of the scraped profiles and zoom in to a particular time range. - -## Provision the Grafana Pyroscope data source - -You can modify the Grafana configuration files to provision the Grafana Pyroscope data source. To learn more, and to view the available provisioning settings, see [provisioning documentation][provisioning-data-sources]. - -Here is an example configuration: - -```yaml -apiVersion: 1 - -datasources: - - name: Grafana Pyroscope - type: grafana-pyroscope-datasource - url: http://localhost:4040 - jsonData: - minStep: '15s' -``` - {{% docs/reference %}} [explore]: "/docs/grafana/ -> /docs/grafana//explore" [explore]: "/docs/grafana-cloud/ -> /docs/grafana//explore" diff --git a/docs/sources/datasources/pyroscope/query-profile-data.md b/docs/sources/datasources/pyroscope/query-profile-data.md new file mode 100644 index 00000000000..8c68fb4275b --- /dev/null +++ b/docs/sources/datasources/pyroscope/query-profile-data.md @@ -0,0 +1,82 @@ +--- +description: Use the query editor to explore your Pyroscope data. +keywords: + - query + - profiling + - pyroscope +labels: + products: + - cloud + - enterprise + - oss +title: Query profile data +menuTitle: Query profile data +weight: 300 +--- + +# Query profile data + +The Pyroscope data source query editor gives you access to a profile type selector, a label selector, and collapsible options. + +![Query editor](/media/docs/pyroscope/query-editor/query-editor.png 'Query editor') + +To access the query editor: + +1. Sign into Grafana or Grafana Cloud. +1. Select your Pyroscope data source. +1. From the menu, choose **Explore**. + +1. Select a profile type from the drop-down menu. + + {{< figure src="/media/docs/pyroscope/query-editor/select-profile.png" class="docs-image--no-shadow" max-width="450px" caption="Profile selector" >}} + +1. Use the labels selector input to filter by labels. Pyroscope uses similar syntax to Prometheus to filter labels. + Refer to [Pyroscope documentation](https://grafana.com/docs/pyroscope/latest/) for available operators and syntax. + + While the label selector can be left empty to query all profiles without filtering by labels, the profile type or app must be selected for the query to be valid. + + Grafana doesn't show any data if the profile type or app isn’t selected when a query runs. + + ![Labels selector](/media/docs/pyroscope/query-editor/labels-selector.png 'Labels selector') + +1. Expand the **Options** section to view **Query Type** and **Group by**. + ![Options section](/media/docs/pyroscope/query-editor/options-section.png 'Options section') + +1. Select a query type to return the profile data. Data is shown in the [Flame Graph][flame-graph], metric data visualized in a graph, or both. You can only select both options in Explore. The panels used on dashboards allow only one visualization. + +Using **Group by**, you can group metric data by a specified label. +Without any **Group by** label, metric data aggregates over all the labels into single time series. +You can use multiple labels to group by. Group by only effects the metric data and doesn't change the profile data results. + +## Profiles query results + +Profiles can be visualized in a flame graph. +Refer to the [Flame Graph documentation][flame-graph] to learn about the visualization and its features. + +![Flame graph](/media/docs/pyroscope/query-editor/flame-graph.png 'Flame graph') + +Pyroscope returns profiles aggregated over a selected time range. +The absolute values in the flame graph grow as the time range gets bigger while keeping the relative values meaningful. +You can zoom in on the time range to get a higher granularity profile up to the point of a single scrape interval. + +## Metrics query results + +Metrics results represent the aggregated sum value over time of the selected profile type. + +![Metrics graph](/media/docs/pyroscope/query-editor/metric-graph.png 'Metrics graph') + +This allows you to quickly see any spikes in the value of the scraped profiles and zoom in to a particular time range. + +{{% docs/reference %}} +[explore]: "/docs/grafana/ -> /docs/grafana//explore" +[explore]: "/docs/grafana-cloud/ -> /docs/grafana//explore" + +[flame-graph]: "/docs/grafana/ -> /docs/grafana//panels-visualizations/visualizations/flame-graph" +[flame-graph]: "/docs/grafana-cloud/ -> /docs/grafana//panels-visualizations/visualizations/flame-graph" + +[provisioning-data-sources]: "/docs/grafana/ -> /docs/grafana//administration/provisioning#datasources" +[provisioning-data-sources]: "/docs/grafana-cloud/ -> /docs/grafana//administration/provisioning#datasources" + +[configure-tempo-data-source]: "/docs/grafana/ -> /docs/grafana//datasources/tempo/configure-tempo-data-source" +[configure-tempo-data-source]: "/docs/grafana-cloud/ -> docs/grafana-cloud/connect-externally-hosted/data-sources/tempo/configure-tempo-data-source" +{{% /docs/reference %}} diff --git a/docs/sources/shared/datasources/tempo-traces-to-profiles.md b/docs/sources/shared/datasources/tempo-traces-to-profiles.md index 5724051fceb..fde1964f20d 100644 --- a/docs/sources/shared/datasources/tempo-traces-to-profiles.md +++ b/docs/sources/shared/datasources/tempo-traces-to-profiles.md @@ -25,9 +25,13 @@ When configured, this connection lets you run queries from a trace span into the There are two ways to configure the trace to profiles feature: -- Use a simplified configuration with default query, or +- Use a basic configuration with default query, or - Configure a custom query where you can use a template language to interpolate variables from the trace or span. +{{< admonition type="note">}} +Traces to profile requires a Tempo data source with Traces to profiles configured and a Pyroscope data source. This integration supports profile data generated using Go, Ruby, and Java instrumentation SDKs. +{{< /admonition >}} + To use trace to profiles, navigate to **Explore** and query a trace. Each span now links to your queries. Clicking a link runs the query in a split panel. If tags are configured, Grafana dynamically inserts the span attribute values into the query. The query runs over the time range of the (span start time - 60) to (span end time + 60 seconds). ![Selecting a link in the span queries the profile data source](/media/docs/tempo/profiles/tempo-trace-to-profile.png) @@ -40,7 +44,7 @@ Hover over a particular block in the flame graph to see more details about the r ## Use a basic configuration -To use a simple configuration, follow these steps: +To use a basic configuration, follow these steps: 1. Select a Pyroscope data source from the **Data source** drop-down. 1. Optional: Choose any tags to use in the query. If left blank, the default values of `service.name` and `service.namespace` are used. From 9c92329bee8c6f8901eb10fc0f145e5109c3faca Mon Sep 17 00:00:00 2001 From: Pepe Cano <825430+ppcano@users.noreply.github.com> Date: Mon, 12 Feb 2024 10:27:18 +0100 Subject: [PATCH 38/50] Alerting docs: `Provision alerting resources` updates (#82221) * Alert provisioning: initial restructuring * Fix products labels * Restructure `Import and export Grafana Alerting resources` * Change URL to `export-alerting-resources` * Complete `Export alerting resources` * Export alerting resources * Update `configuration files` provisioning * Terraform Provisioning * Change to `Provision/Import/Export` terms and some notes * Replace `config` to `configuration` * Set (menu)Title `Export alerting resources` * Minor change on note about `Export Alerting endpoints` * Fix `doc-validator` issues * Fix grammar * Update docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Update docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> * Fix numbered lists and `Note:` without admonition * Convert text-based notes (`Note:`) to `admonition` blocks * Replace text-based `Note:` with adminitions * Remove `file-provisioning` grafana-cloud links * Update `Export alerting resources` intro * nitpicky format order --------- Co-authored-by: brendamuir <100768211+brendamuir@users.noreply.github.com> --- docs/sources/alerting/set-up/_index.md | 5 +- .../provision-alerting-resources/_index.md | 143 +- .../export-alerting-resources/index.md | 106 ++ .../file-provisioning/index.md | 279 ++- .../http-api-provisioning/_index.md | 20 + .../terraform-provisioning/index.md | 430 ++--- .../view-provisioned-resources/index.md | 108 -- .../http_api/alerting_provisioning.md | 1524 +--------------- .../shared/alerts/alerting_provisioning.md | 1622 +++++++++++++++++ 9 files changed, 2198 insertions(+), 2039 deletions(-) create mode 100644 docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md create mode 100644 docs/sources/alerting/set-up/provision-alerting-resources/http-api-provisioning/_index.md delete mode 100644 docs/sources/alerting/set-up/provision-alerting-resources/view-provisioned-resources/index.md create mode 100644 docs/sources/shared/alerts/alerting_provisioning.md diff --git a/docs/sources/alerting/set-up/_index.md b/docs/sources/alerting/set-up/_index.md index 761a2be3222..4123fda2935 100644 --- a/docs/sources/alerting/set-up/_index.md +++ b/docs/sources/alerting/set-up/_index.md @@ -54,7 +54,7 @@ Grafana Alerting supports many additional configuration options, from configurin The following topics provide you with advanced configuration options for Grafana Alerting. -- [Provision alert rules using file provisioning][file-provisioning] +- [Provision alert rules using file provisioning](/docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning) - [Provision alert rules using Terraform][terraform-provisioning] - [Add an external Alertmanager][configure-alertmanager] - [Configure high availability][configure-high-availability] @@ -72,9 +72,6 @@ The following topics provide you with advanced configuration options for Grafana [data-source-management]: "/docs/grafana/ -> /docs/grafana//administration/data-source-management" [data-source-management]: "/docs/grafana-cloud/ -> /docs/grafana//administration/data-source-management" -[file-provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning" -[file-provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/file-provisioning" - [terraform-provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" [terraform-provisioning]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/set-up/provision-alerting-resources/terraform-provisioning" {{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/_index.md b/docs/sources/alerting/set-up/provision-alerting-resources/_index.md index 8e6c588da66..823ae1be82a 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/_index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/_index.md @@ -2,7 +2,7 @@ aliases: - ../provision-alerting-resources/ canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/ -description: Import and export alerting resources +description: Provision alerting resources keywords: - grafana - alerting @@ -14,11 +14,11 @@ labels: - cloud - enterprise - oss -title: Import and export Grafana Alerting resources +title: Provision Alerting resources weight: 300 --- -# Import and export Grafana Alerting resources +# Provision Alerting resources Alerting infrastructure is often complex, with many pieces of the pipeline that often live in different places. Scaling this across multiple teams and organizations is an especially challenging task. Importing and exporting (or provisioning) your alerting resources in Grafana Alerting makes this process easier by enabling you to create, manage, and maintain your alerting data in a way that best suits your organization. @@ -26,116 +26,55 @@ You can import alert rules, contact points, notification policies, mute timings, You cannot edit imported alerting resources in the Grafana UI in the same way as alerting resources that were not imported. You can only edit imported contact points, notification policies, templates, and mute timings in the source where they were created. For example, if you manage your alerting resources using files from disk, you cannot edit the data in Terraform or from within Grafana. +## Import alerting resources + +Choose from the options below to import (or provision) your Grafana Alerting resources. + +1. [Use configuration files to provision your alerting resources](/docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning), such as alert rules and contact points, through files on disk. + + {{< admonition type="note" >}} + File provisioning is not available in Grafana Cloud instances. + {{< /admonition >}} + +1. Use [Terraform to provision alerting resources][alerting_tf_provisioning]. + +1. Use the [Alerting provisioning HTTP API][alerting_http_provisioning] to manage alerting resources. + + {{< admonition type="note" >}} + The JSON output from the majority of Alerting HTTP endpoints isn't compatible for provisioning via configuration files. + Instead, use the [Export Alerting endpoints](/docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints) to return or download the alerting resources in provisioning format. + {{< /admonition >}} + +## Export alerting resources + +You can export both manually created and provisioned alerting resources. For more information, refer to [Export alerting resources][alerting_export]. + To modify imported alert rules, you can use the **Modify export** feature to edit and then export. -Choose from the options below to import your Grafana Alerting resources. +## View provisioned alerting resources -1. Use file provisioning to manage your Grafana Alerting resources, such as alert rules and contact points, through files on disk. +To view your provisioned resources in Grafana, complete the following steps. - {{% admonition type="note" %}} - File provisioning is not available in Grafana Cloud instances. - {{% /admonition %}} +1. Open your Grafana instance. +1. Navigate to Alerting. +1. Click an alerting resource folder, for example, Alert rules. -2. Use the Alerting Provisioning HTTP API. - - For more information on the Alerting Provisioning HTTP API, refer to [Alerting provisioning HTTP API][alerting_provisioning]. - - Here is a ready-to-use template for alert rules: - - #### Alert rules template - -``` -{ - "title": "TEST-API_1", - "ruleGroup": "API", - "folderUID": "FOLDER", - "noDataState": "OK", - "execErrState": "OK", - "for": "5m", - "orgId": 1, - "uid": "", - "condition": "B", - "annotations": { - "summary": "test_api_1" - }, - "labels": { - "API": "test1" - }, - "data": [ - { - "refId": "A", - "queryType": "", - "relativeTimeRange": { - "from": 600, - "to": 0 - }, - "datasourceUid": " XXXXXXXXX-XXXXXXXXX-XXXXXXXXXX", - "model": { - "expr": "up", - "hide": false, - "intervalMs": 1000, - "maxDataPoints": 43200, - "refId": "A" - } - }, - { - "refId": "B", - "queryType": "", - "relativeTimeRange": { - "from": 0, - "to": 0 - }, - "datasourceUid": "-100", - "model": { - "conditions": [ - { - "evaluator": { - "params": [ - 6 - ], - "type": "gt" - }, - "operator": { - "type": "and" - }, - "query": { - "params": [ - "A" - ] - }, - "reducer": { - "params": [], - "type": "last" - }, - "type": "query" - } - ], - "datasource": { - "type": "__expr__", - "uid": "-100" - }, - "hide": false, - "intervalMs": 1000, - "maxDataPoints": 43200, - "refId": "B", - "type": "classic_conditions" - } - } - ] -} -``` - -3. Use [Terraform](https://www.terraform.io/). +Provisioned resources are labeled **Provisioned**, so that it is clear that they were not created manually. **Useful Links:** [Grafana provisioning][provisioning] -[Grafana Alerting provisioning API][alerting_provisioning] - {{% docs/reference %}} -[alerting_provisioning]: "/docs/grafana/ -> /docs/grafana//developers/http_api/alerting_provisioning" -[alerting_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//developers/http_api/alerting_provisioning" +[alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" +[alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" +[alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" +[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" +[alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" +[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" + +[alerting_export_http]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints" +[alerting_export_http]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources#export-api-endpoints" [provisioning]: "/docs/grafana/ -> /docs/grafana//administration/provisioning" [provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//administration/provisioning" diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md new file mode 100644 index 00000000000..2bfc1500c1b --- /dev/null +++ b/docs/sources/alerting/set-up/provision-alerting-resources/export-alerting-resources/index.md @@ -0,0 +1,106 @@ +--- +aliases: + - ../../provision-alerting-resources/view-provisioned-resources/ + - ./view-provisioned-resources/ +canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/export-alerting-resources/ +description: Export alerting resources in Grafana +keywords: + - grafana + - alerting + - alerting resources + - provisioning +labels: + products: + - cloud + - enterprise + - oss +title: Export alerting resources +weight: 300 +--- + +# Export alerting resources + +Export your alerting resources, such as alert rules, contact points, and notification policies for provisioning, automatically importing single folders and single groups. + +The export options listed below enable you to download resources in YAML, JSON, or Terraform format, facilitating their provisioning through [configuration files](/docs/grafana//alerting/set-up/provision-alerting-resources/file-provisioning) or [Terraform][alerting_tf_provisioning]. + +## Export alert rules + +To export alert rules from the Grafana UI, complete the following steps. + +1. Click **Alerts & IRM** -> **Alert rules**. +1. To export all Grafana-managed rules, click **Export rules**. +1. To export a folder, change the **View as** to **List**. +1. Select the folder you want to export and click the **Export rules folder** icon. +1. To export a group, change the **View as** to **Grouped**. +1. Find the group you want to export and click the **Export rule group** icon. +1. Choose the format to export in. + + The exported rule data appears in different formats - YAML, JSON, Terraform. + +1. Click **Copy Code** or **Download**. + + a. Choose **Copy Code** to go to an existing file and paste in the code. + + b. Choose **Download** to download a file with the exported data. + +## Modify and export alert rules without saving changes + +Use the **Modify export** mode to edit and export an alert rule without updating it. + +{{% admonition type="note" %}} This feature is for Grafana-managed alert rules only. It is available to Admin, Viewer, and Editor roles. {{% /admonition %}} + +To export a modified alert rule without saving the modifications, complete the following steps from the Grafana UI. + +1. Click **Alerts & IRM** -> **Alert rules**. +1. Locate the alert rule you want to edit and click **More** -> **Modify Export** to open the Alert Rule form. +1. From the Alert Rule form, edit the fields you want to change. Changes made are not applied to the alert rule. +1. Click **Export**. +1. Choose the format to export in. + + The exported rule data appears in different formats - YAML, JSON, Terraform. + +1. Click **Copy Code** or **Download**. + + a. Choose **Copy Code** to go to an existing file and paste in the code. + + b. Choose **Download** to download a file with the exported data. + +## Export API endpoints + +You can also use the **Alerting provisioning HTTP API** to export alerting resources in YAML or JSON formats for provisioning. + +Note that most Alerting endpoints return a JSON format that is not compatible for provisioning via configuration files, except the ones listed below. + +| Method | URI | Summary | +| ------ | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| GET | /api/v1/provisioning/alert-rules/:uid/export | [Export an alert rule in provisioning file format.][export_rule] | +| GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export | [Export an alert rule group in provisioning file format.][export_rule_group] | +| GET | /api/v1/provisioning/alert-rules/export | [Export all alert rules in provisioning file format.][export_rules] | +| GET | /api/v1/provisioning/contact-points/export | [Export all contact points in provisioning file format.][export_contacts] | +| GET | /api/v1/provisioning/policies/export | [Export the notification policy tree in provisioning file format.][export_notifications] | + +These endpoints accept a `download` parameter to download a file containing the exported resources. + +{{% docs/reference %}} +[alerting_tf_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" +[alerting_tf_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/terraform-provisioning" + +[alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" +[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" + +[export_rule]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-_routegetalertruleexport_" +[export_rule]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-exportspan-export-an-alert-rule-in-provisioning-file-format-_routegetalertruleexport_" + +[export_rule_group]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-group-exportspan-export-an-alert-rule-group-in-provisioning-file-format-_routegetalertrulegroupexport_" +[export_rule_group]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rule-group-exportspan-export-an-alert-rule-group-in-provisioning-file-format-_routegetalertrulegroupexport_" + +[export_rules]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rules-exportspan-export-all-alert-rules-in-provisioning-file-format-_routegetalertrulesexport_" +[export_rules]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-alert-rules-exportspan-export-all-alert-rules-in-provisioning-file-format-_routegetalertrulesexport_" + +[export_contacts]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-_routegetcontactpointsexport_" +[export_contacts]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-contactpoints-exportspan-export-all-contact-points-in-provisioning-file-format-_routegetcontactpointsexport_" + +[export_notifications]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-_routegetpolicytreeexport_" +[export_notifications]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning/#span-idroute-get-policy-tree-exportspan-export-the-notification-policy-tree-in-provisioning-file-format-_routegetpolicytreeexport_" +{{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md index b99e6a70f22..63018a906ad 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/file-provisioning/index.md @@ -11,14 +11,14 @@ keywords: - provisioning labels: products: - - cloud - enterprise - oss -title: Use file provisioning to manage alerting resources +menuTitle: Use configuration files to provision +title: Use configuration files to provision alerting resources weight: 100 --- -## Use file provisioning to manage alerting resources +# Use configuration files to provision alerting resources Manage your alerting resources using files from disk. When you start Grafana, the data from these files is created in your Grafana system. Grafana adds any new resources you created, updates any that you changed, and deletes old ones. @@ -26,26 +26,28 @@ Arrange your files in a directory in a way that best suits your use case. For ex Details on how to set up the files and which fields are required for each object are listed below depending on which resource you are provisioning. -**Note:** +For a complete guide about how Grafana provisions resources, refer to the [Provision Grafana][provisioning] documentation. -Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API][reload-provisioning-configurations]. +{{< admonition type="note" >}} -### Import alert rules +- You cannot edit provisioned resources from files in Grafana. You can only change the resource properties by changing the provisioning file and restarting Grafana or carrying out a hot reload. This prevents changes being made to the resource that would be overwritten if a file is provisioned again or a hot reload is carried out. + +- Importing takes place during the initial set up of your Grafana system, but you can re-run it at any time using the [Grafana Admin API](/docs/grafana//developers/http_api/admin#reload-provisioning-configurations). + +- Importing an existing alerting resource results in a conflict. First, when present, remove the resources you plan to import. + {{< /admonition >}} + +## Import alert rules Create or delete alert rules in your Grafana instance(s). 1. Create alert rules in Grafana. -1. Use the [Alerting provisioning API][alerting_provisioning] export endpoints to download a provisioning file for your alert rules. -1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory. +1. [Export][alerting_export] and download a provisioning file for your alert rules. +1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory. Example configuration files can be found below. -1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s). -1. Delete the alert rules in Grafana that are going to be imported. - - **Note:** - - If you do not delete the alert rule, it will clash with the imported alert rule once uploaded. +1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). Here is an example of a configuration file for creating alert rules. @@ -134,17 +136,17 @@ deleteRules: uid: my_id_1 ``` -### Import contact points +## Import contact points Create or delete contact points in your Grafana instance(s). 1. Create a contact point in Grafana. -1. Use the [Alerting provisioning API][alerting_provisioning] export endpoints to download a provisioning file for your contact point. -1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory. +1. [Export][alerting_export] and download a provisioning file for your contact point. +1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory. Example configuration files can be found below. -1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s). +1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). Here is an example of a configuration file for creating contact points. @@ -184,12 +186,14 @@ deleteContactPoints: uid: first_uid ``` -#### Settings +### Settings Here are some examples of settings you can use for the different contact point integrations. -##### Alertmanager +{{< collapse title="Alertmanager" >}} + +#### Alertmanager ```yaml type: prometheus-alertmanager @@ -202,7 +206,11 @@ settings: basicAuthPassword: abc123 ``` -##### DingDing +{{< /collapse >}} + +{{< collapse title="DingDing" >}} + +#### DingDing ```yaml type: dingding @@ -216,7 +224,11 @@ settings: {{ template "default.message" . }} ``` -##### Discord +{{< /collapse >}} + +{{< collapse title="Discord" >}} + +#### Discord ```yaml type: discord @@ -232,7 +244,11 @@ settings: {{ template "default.message" . }} ``` -##### E-Mail +{{< /collapse >}} + +{{< collapse title="E-Mail" >}} + +#### E-Mail ```yaml type: email @@ -248,7 +264,11 @@ settings: {{ template "default.title" . }} ``` -##### Google Chat +{{< /collapse >}} + +{{< collapse title="Google Chat" >}} + +#### Google Chat ```yaml type: googlechat @@ -260,7 +280,11 @@ settings: {{ template "default.message" . }} ``` -##### Kafka +{{< /collapse >}} + +{{< collapse title="Kafka" >}} + +#### Kafka ```yaml type: kafka @@ -271,7 +295,11 @@ settings: kafkaTopic: topic1 ``` -##### LINE +{{< /collapse >}} + +{{< collapse title="LINE" >}} + +#### LINE ```yaml type: line @@ -280,7 +308,11 @@ settings: token: xxx ``` -##### Microsoft Teams +{{< /collapse >}} + +{{< collapse title="Microsoft Teams" >}} + +#### Microsoft Teams ```yaml type: teams @@ -297,7 +329,11 @@ settings: {{ template "default.message" . }} ``` -##### OpsGenie +{{< /collapse >}} + +{{< collapse title="OpsGenie" >}} + +#### OpsGenie ```yaml type: opsgenie @@ -319,7 +355,11 @@ settings: sendTagsAs: both ``` -##### PagerDuty +{{< /collapse >}} + +{{< collapse title="PagerDuty" >}} + +#### PagerDuty ```yaml type: pagerduty @@ -339,7 +379,11 @@ settings: {{ template "default.message" . }} ``` -##### Pushover +{{< /collapse >}} + +{{< collapse title="Pushover" >}} + +#### Pushover ```yaml type: pushover @@ -367,7 +411,11 @@ settings: {{ template "default.message" . }} ``` -##### Slack +{{< /collapse >}} + +{{< collapse title="Slack" >}} + +#### Slack ```yaml type: slack @@ -399,7 +447,11 @@ settings: {{ template "slack.default.text" . }} ``` -##### Sensu Go +{{< /collapse >}} + +{{< collapse title="Sensu Go" >}} + +#### Sensu Go ```yaml type: sensugo @@ -421,7 +473,11 @@ settings: {{ template "default.message" . }} ``` -##### Telegram +{{< /collapse >}} + +{{< collapse title="Telegram" >}} + +#### Telegram ```yaml type: telegram @@ -435,7 +491,11 @@ settings: {{ template "default.message" . }} ``` -##### Threema Gateway +{{< /collapse >}} + +{{< collapse title="Threema Gateway" >}} + +#### Threema Gateway ```yaml type: threema @@ -448,7 +508,11 @@ settings: recipient_id: A9R4KL4S ``` -##### VictorOps +{{< /collapse >}} + +{{< collapse title="VictorOps" >}} + +#### VictorOps ```yaml type: victorops @@ -459,7 +523,11 @@ settings: messageType: CRITICAL ``` -##### Webhook +{{< /collapse >}} + +{{< collapse title="Webhook" >}} + +#### Webhook ```yaml type: webhook @@ -480,7 +548,11 @@ settings: maxAlerts: '10' ``` -##### WeCom +{{< /collapse >}} + +{{< collapse title="WeCom" >}} + +#### WeCom ```yaml type: wecom @@ -495,17 +567,27 @@ settings: {{ template "default.title" . }} ``` -### Import notification policies +{{< /collapse >}} + +## Import notification policies Create or reset the notification policy tree in your Grafana instance(s). +In Grafana, the entire notification policy tree is considered a single, large resource. Add new specific policies as sub-policies under the root policy. Since specific policies may depend on each other, you cannot provision subsets of the policy tree; the entire tree must be defined in a single place. + +{{% admonition type="warning" %}} + +Since the policy tree is a single resource, provisioning it will overwrite a policy tree created through any other means. + +{{< /admonition >}} + 1. Create a notification policy in Grafana. -1. Use the [Alerting provisioning API][alerting_provisioning] export endpoints to download a provisioning file for your notification policy. -1. Copy the contents into a YAML or JSON configuration file in the default provisioning directory or in your configured directory. +1. [Export][alerting_export] and download a provisioning file for your notification policy. +1. Copy the contents into a YAML or JSON configuration file in the `provisioning/alerting` directory. Example configuration files can be found below. -1. Ensure that your files are in the right directory on the node running the Grafana server, so that they deploy alongside your Grafana instance(s). +1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). Here is an example of a configuration file for creating notification policies. @@ -581,13 +663,7 @@ resetPolicies: - 1 ``` -**Note:** - -In Grafana, the entire notification policy tree is considered a single, large resource. Add new specific policies as sub-policies under the root policy. Since specific policies may depend on each other, you cannot provision subsets of the policy tree; the entire tree must be defined in a single place. - -Since the policy tree is a single resource, applying it will overwrite a policy tree created through any other means. - -### Import templates +## Import templates Create or delete templates in your Grafana instance(s). @@ -595,7 +671,7 @@ Create or delete templates in your Grafana instance(s). Example configuration files can be found below. -2. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). +1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). Here is an example of a configuration file for creating templates. @@ -627,7 +703,7 @@ deleteTemplates: name: my_first_template ``` -### Import mute timings +## Import mute timings Create or delete mute timings in your Grafana instance(s). @@ -676,65 +752,68 @@ deleteMuteTimes: name: mti_1 ``` -### File provisioning using Kubernetes +## File provisioning using Kubernetes If you are a Kubernetes user, you can leverage file provisioning using Kubernetes configuration maps. 1. Create one or more configuration maps as follows. -```yaml -apiVersion: v1 -kind: ConfigMap -metadata: - name: grafana-alerting -data: - provisioning.yaml: | - templates: - - name: my_first_template - template: the content for my template -``` + ```yaml + apiVersion: v1 + kind: ConfigMap + metadata: + name: grafana-alerting + data: + provisioning.yaml: | + templates: + - name: my_first_template + template: the content for my template + ``` -2. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). +1. Add the file(s) to your GitOps workflow, so that they deploy alongside your Grafana instance(s). -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: grafana -spec: - replicas: 1 - selector: - matchLabels: - app: grafana - template: - metadata: - name: grafana - labels: - app: grafana - spec: - containers: - - name: grafana - image: grafana/grafana:latest - ports: - - name: grafana - containerPort: 3000 - volumeMounts: - - mountPath: /etc/grafana/provisioning/alerting - name: grafana-alerting - readOnly: false - volumes: - - name: grafana-alerting - configMap: - defaultMode: 420 - name: grafana-alerting -``` + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: grafana + spec: + replicas: 1 + selector: + matchLabels: + app: grafana + template: + metadata: + name: grafana + labels: + app: grafana + spec: + containers: + - name: grafana + image: grafana/grafana:latest + ports: + - name: grafana + containerPort: 3000 + volumeMounts: + - mountPath: /etc/grafana/provisioning/alerting + name: grafana-alerting + readOnly: false + volumes: + - name: grafana-alerting + configMap: + defaultMode: 420 + name: grafana-alerting + ``` This eliminates the need for a persistent database to use Grafana Alerting in Kubernetes; all your provisioned resources appear after each restart or re-deployment. Grafana still requires a database for normal operation, you do not need to persist the contents of the database between restarts if all objects are provisioned using files. -{{% docs/reference %}} -[alerting_provisioning]: "/docs/grafana/ -> /docs/grafana//developers/http_api/alerting_provisioning" -[alerting_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//developers/http_api/alerting_provisioning" +**Useful Links:** -[reload-provisioning-configurations]: "/docs/grafana/ -> /docs/grafana//developers/http_api/admin#reload-provisioning-configurations" -[reload-provisioning-configurations]: "/docs/grafana-cloud/ -> /docs/grafana//developers/http_api/admin#reload-provisioning-configurations" +[Grafana provisioning][provisioning] + +{{% docs/reference %}} +[alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" +[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" +[provisioning]: "/docs/grafana/ -> /docs/grafana//administration/provisioning" +[provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//administration/provisioning" {{% /docs/reference %}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/http-api-provisioning/_index.md b/docs/sources/alerting/set-up/provision-alerting-resources/http-api-provisioning/_index.md new file mode 100644 index 00000000000..4c1f4606571 --- /dev/null +++ b/docs/sources/alerting/set-up/provision-alerting-resources/http-api-provisioning/_index.md @@ -0,0 +1,20 @@ +--- +canonical: https://grafana.com/docs/grafana/latest/developers/http_api/alerting_provisioning/ +description: Create and manage alerting resources using the HTTP API +keywords: + - grafana + - alerting + - alerting resources + - provisioning +labels: + products: + - cloud + - enterprise + - oss +title: Use the HTTP API to manage alerting resources +weight: 400 +--- + +# Use the HTTP API to manage alerting resources + +{{< docs/shared lookup="alerts/alerting_provisioning.md" source="grafana" version="latest" >}} diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md index 0ad04166a99..5050d2868f9 100644 --- a/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md +++ b/docs/sources/alerting/set-up/provision-alerting-resources/terraform-provisioning/index.md @@ -11,41 +11,46 @@ keywords: - Terraform labels: products: + - cloud - enterprise - oss -title: Use Terraform to manage alerting resources +menuTitle: Use Terraform to provision +title: Use Terraform to provision alerting resources weight: 200 --- -# Use Terraform to manage alerting resources +# Use Terraform to provision alerting resources Use Terraform’s Grafana Provider to manage your alerting resources and provision them into your Grafana system. Terraform provider support for Grafana Alerting makes it easy to create, manage, and maintain your entire Grafana Alerting stack as code. -For more information on managing your alerting resources using Terraform, refer to the [Grafana Provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) documentation. +Refer to [Grafana Provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) documentation for more examples and information on Terraform Alerting schemas. Complete the following tasks to create and manage your alerting resources using Terraform. 1. Create an API key for provisioning. 1. Configure the Terraform provider. -1. Define your alerting resources in Terraform. +1. Define your alerting resources in Terraform. [Export alerting resources][alerting_export] in Terraform format, or implement the [Terraform Alerting schemas](https://registry.terraform.io/providers/grafana/grafana/latest/docs). + 1. Run `terraform apply` to provision your alerting resources. -## Before you begin +{{< admonition type="note" >}} -- Ensure you have the grafana/grafana [Terraform provider](https://registry.terraform.io/providers/grafana/grafana/1.28.0) 1.27.0 or higher. +- By default, you cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code. To change the default behaviour, refer to [Edit provisioned resources in the Grafana UI](#edit-provisioned-resources-in-the-grafana-ui). -- Ensure you are using Grafana 9.1 or higher. +- Before you begin, ensure you have the [Grafana Terraform Provider](https://registry.terraform.io/providers/grafana/grafana/) 1.27.0 or higher, and are using Grafana 9.1 or higher. + +{{< /admonition >}} ## Create an API key for provisioning -You can [create a normal Grafana API key][api-keys] to authenticate Terraform with Grafana. Most existing tooling using API keys should automatically work with the new Grafana Alerting support. +You can create a [service account token][service-accounts] to authenticate Terraform with Grafana. Most existing tooling using API keys should automatically work with the new Grafana Alerting support. -There are also dedicated RBAC roles for alerting provisioning. This lets you easily authenticate as a [service account][service-accounts] with the minimum permissions needed to provision your Alerting infrastructure. +There are also dedicated RBAC roles for alerting provisioning. This lets you easily authenticate as a service account with the minimum permissions needed to provision your Alerting infrastructure. To create an API key for provisioning, complete the following steps. -1. Create a new service account for your CI pipeline. -1. Assign the role “Access the alert rules Provisioning API.” +1. Create a new service account. +1. Assign the role or permission to access the [Alerting provisioning API][alerting_http_provisioning]. 1. Create a new service account token. 1. Name and save the token for use in Terraform. @@ -73,70 +78,68 @@ provider "grafana" { } ``` -## Provision contact points and templates +## Import contact points and templates -Contact points connect an alerting stack to the outside world. They tell Grafana how to connect to your external systems and where to deliver notifications. There are over fifteen different [integrations](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/contact_point#optional) to choose from. +Contact points connect an alerting stack to the outside world. They tell Grafana how to connect to your external systems and where to deliver notifications. -To provision contact points and templates, complete the following steps. +To provision contact points and templates, refer to the [grafana_contact_point schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/contact_point) and [grafana_message_template schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/message_template), and complete the following steps. -1. Copy this code block into a .tf file on your local machine. +1. Copy this code block into a `.tf` file on your local machine. -This example creates a contact point that sends alert notifications to Slack. + This example creates a contact point that sends alert notifications to Slack. -```HCL -resource "grafana_contact_point" "my_slack_contact_point" { - name = "Send to My Slack Channel" + ```HCL + resource "grafana_contact_point" "my_slack_contact_point" { + name = "Send to My Slack Channel" - slack { - url = - text = < + text = <}} -```HCL -resource "grafana_notification_policy" "my_policy" { - group_by = ["alertname"] - contact_point = grafana_contact_point.my_slack_contact_point.name +1. Copy this code block into a `.tf` file on your local machine. - group_wait = "45s" - group_interval = "6m" - repeat_interval = "3h" + In this example, the alerts are grouped by `alertname`, which means that any notifications coming from alerts which share the same name, are grouped into the same Slack message. You can provide any set of label keys here, or you can use the special label `"..."` to route by all label keys, sending each alert in a separate notification. - policy { - matcher { - label = "a" - match = "=" - value = "b" - } - group_by = ["..."] - contact_point = grafana_contact_point.a_different_contact_point.name - mute_timings = [grafana_mute_timing.my_mute_timing.name] + If you want to route specific notifications differently, you can add sub-policies. Sub-policies allow you to apply routing to different alerts based on label matching. In this example, we apply a mute timing to all alerts with the label a=b. - policy { - matcher { - label = "sublabel" - match = "=" - value = "subvalue" - } - contact_point = grafana_contact_point.a_third_contact_point.name - group_by = ["..."] - } - } -} -``` + ```HCL + resource "grafana_notification_policy" "my_policy" { + group_by = ["alertname"] + contact_point = grafana_contact_point.my_slack_contact_point.name -2. In the mute_timings field, link a mute timing to your notification policy. + group_wait = "45s" + group_interval = "6m" + repeat_interval = "3h" -3. Run the command ‘terraform apply’. + policy { + matcher { + label = "a" + match = "=" + value = "b" + } + group_by = ["..."] + contact_point = grafana_contact_point.a_different_contact_point.name + mute_timings = [grafana_mute_timing.my_mute_timing.name] -4. Go to the Grafana UI and check the details of your notification policy. + policy { + matcher { + label = "sublabel" + match = "=" + value = "subvalue" + } + contact_point = grafana_contact_point.a_third_contact_point.name + group_by = ["..."] + } + } + } + ``` -**Note:** +1. In the mute_timings field, link a mute timing to your notification policy. -Since the policy tree is a single resource, applying it will overwrite a policy tree created through any other means. +1. Run the command `terraform apply`. -By default, you cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code. +1. Go to the Grafana UI and check the details of your notification policy. -5. Click **Test** to verify that the notification point is working correctly. +1. Click **Test** to verify that the notification point is working correctly. -## Provision mute timings +## Import mute timings Mute timings provide the ability to mute alert notifications for defined time periods. -To provision mute timings, complete the following steps. +To provision mute timings, refer to the [grafana_mute_timing schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/mute_timing), and complete the following steps. -1. Copy this code block into a .tf file on your local machine. +1. Copy this code block into a `.tf` file on your local machine. -In this example, alert notifications are muted on weekends. + In this example, alert notifications are muted on weekends. -```HCL -resource "grafana_mute_timing" "my_mute_timing" { - name = "My Mute Timing" + ```HCL + resource "grafana_mute_timing" "my_mute_timing" { + name = "My Mute Timing" - intervals { - times { - start = "04:56" - end = "14:17" - } - weekdays = ["saturday", "sunday", "tuesday:thursday"] - months = ["january:march", "12"] - years = ["2025:2027"] - } -} -``` + intervals { + times { + start = "04:56" + end = "14:17" + } + weekdays = ["saturday", "sunday", "tuesday:thursday"] + months = ["january:march", "12"] + years = ["2025:2027"] + } + } + ``` -2. Run the command ‘terraform apply’. -3. Go to the Grafana UI and check the details of your mute timing. -4. Reference your newly created mute timing in a notification policy using the `mute_timings` field. +1. Run the command `terraform apply`. +1. Go to the Grafana UI and check the details of your mute timing. +1. Reference your newly created mute timing in a notification policy using the `mute_timings` field. This will apply your mute timing to some or all of your notifications. -**Note:** +1. Click **Test** to verify that the mute timing is working correctly. -By default, you cannot edit resources provisioned from Terraform from the UI. This ensures that your alerting stack always stays in sync with your code. - -5. Click **Test** to verify that the mute timing is working correctly. - -## Provision alert rules +## Import alert rules [Alert rules][alerting-rules] enable you to alert against any Grafana data source. This can be a data source that you already have configured, or you can [define your data sources in Terraform](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/data_source) alongside your alert rules. -To provision alert rules, complete the following steps. +To provision alert rules, refer to the [grafana_rule_group schema](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/rule_group), and complete the following steps. 1. Create a data source to query and a folder to store your rules in. -In this example, the [TestData][testdata] data source is used. + In this example, the [TestData][testdata] data source is used. -Alerts can be defined against any backend datasource in Grafana. + Alerts can be defined against any backend datasource in Grafana. -```HCL -resource "grafana_data_source" "testdata_datasource" { - name = "TestData" - type = "testdata" -} + ```HCL + resource "grafana_data_source" "testdata_datasource" { + name = "TestData" + type = "testdata" + } -resource "grafana_folder" "rule_folder" { - title = "My Rule Folder" -} -``` + resource "grafana_folder" "rule_folder" { + title = "My Rule Folder" + } + ``` -2. Define an alert rule. +1. Define an alert rule. -For more information on alert rules, refer to [how to create Grafana-managed alerts](/blog/2022/08/01/grafana-alerting-video-how-to-create-alerts-in-grafana-9/). + For more information on alert rules, refer to [how to create Grafana-managed alerts](/blog/2022/08/01/grafana-alerting-video-how-to-create-alerts-in-grafana-9/). -3. Create a rule group containing one or more rules. +1. Create a rule group containing one or more rules. -In this example, the `grafana_rule_group` resource group is used. + In this example, the `grafana_rule_group` resource group is used. -```HCL -resource "grafana_rule_group" "my_rule_group" { - name = "My Alert Rules" - folder_uid = grafana_folder.rule_folder.uid - interval_seconds = 60 - org_id = 1 + ```HCL + resource "grafana_rule_group" "my_rule_group" { + name = "My Alert Rules" + folder_uid = grafana_folder.rule_folder.uid + interval_seconds = 60 + org_id = 1 - rule { - name = "My Random Walk Alert" - condition = "C" - for = "0s" + rule { + name = "My Random Walk Alert" + condition = "C" + for = "0s" - // Query the datasource. - data { - ref_id = "A" - relative_time_range { - from = 600 - to = 0 - } - datasource_uid = grafana_data_source.testdata_datasource.uid - // `model` is a JSON blob that sends datasource-specific data. - // It's different for every datasource. The alert's query is defined here. - model = jsonencode({ - intervalMs = 1000 - maxDataPoints = 43200 - refId = "A" - }) - } + // Query the datasource. + data { + ref_id = "A" + relative_time_range { + from = 600 + to = 0 + } + datasource_uid = grafana_data_source.testdata_datasource.uid + // `model` is a JSON blob that sends datasource-specific data. + // It's different for every datasource. The alert's query is defined here. + model = jsonencode({ + intervalMs = 1000 + maxDataPoints = 43200 + refId = "A" + }) + } - // The query was configured to obtain data from the last 60 seconds. Let's alert on the average value of that series using a Reduce stage. - data { - datasource_uid = "__expr__" - // You can also create a rule in the UI, then GET that rule to obtain the JSON. - // This can be helpful when using more complex reduce expressions. - model = < /docs/grafana//alerting/alerting-rules" [alerting-rules]: "/docs/grafana-cloud/ -> /docs/grafana-cloud/alerting-and-irm/alerting/alerting-rules" -[api-keys]: "/docs/grafana/ -> /docs/grafana//administration/api-keys" -[api-keys]: "/docs/grafana-cloud/ -> /docs/grafana//administration/api-keys" +[alerting_export]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" +[alerting_export]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/export-alerting-resources" + +[alerting_http_provisioning]: "/docs/grafana/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" +[alerting_http_provisioning]: "/docs/grafana-cloud/ -> /docs/grafana//alerting/set-up/provision-alerting-resources/http-api-provisioning" [service-accounts]: "/docs/grafana/ -> /docs/grafana//administration/service-accounts" [service-accounts]: "/docs/grafana-cloud/ -> /docs/grafana//administration/service-accounts" diff --git a/docs/sources/alerting/set-up/provision-alerting-resources/view-provisioned-resources/index.md b/docs/sources/alerting/set-up/provision-alerting-resources/view-provisioned-resources/index.md deleted file mode 100644 index 5b473a7776d..00000000000 --- a/docs/sources/alerting/set-up/provision-alerting-resources/view-provisioned-resources/index.md +++ /dev/null @@ -1,108 +0,0 @@ ---- -aliases: - - ../../provision-alerting-resources/view-provisioned-resources/ -canonical: https://grafana.com/docs/grafana/latest/alerting/set-up/provision-alerting-resources/view-provisioned-resources/ -description: Manage provisioned alerting resources in Grafana -keywords: - - grafana - - alerting - - alerting resources - - provisioning -labels: - products: - - cloud - - enterprise - - oss -menuTitle: Manage provisioned alerting resources -title: Manage provisioned alerting resources -weight: 300 ---- - -# Manage provisioned alerting resources - -Verify that your alerting resources were created in Grafana, as well as edit or export your provisioned alerting resources. - -## View provisioned alerting resoureces - -To view your provisioned resources in Grafana, complete the following steps. - -1. Open your Grafana instance. -1. Navigate to Alerting. -1. Click an alerting resource folder, for example, Alert rules. - -Provisioned resources are labeled **Provisioned**, so that it is clear that they were not created manually. - -## Export provisioned alerting resources - -Export your alerting resources, such as alert rules, contact points, and notification policies in JSON, YAML, or Terraform format. You can export all Grafana-managed alert rules, single folders, and single groups. - -To export provisioned alerting resources from the Grafana UI, complete the following steps. - -1. Click **Alerts & IRM** -> **Alert rules**. -1. To export all Grafana-managed rules, click **Export rules**. -1. To export a folder, change the **View as** to **List**. -1. Select the folder you want to export and click the **Export rules folder** icon. -1. To export a group, change the **View as** to **Grouped**. -1. Find the group you want to export and click the **Export rule group** icon. -1. Choose the format to export in. - - Note that formats JSON and YAML are suitable only for file provisioning. To get rule definition in provisioning API format, use the provisioning GET API. - -1. Click **Copy Code** or **Download**. -1. Choose **Copy Code** to go to an existing file and paste in the code. -1. Choose **Download** to download a file with the exported data. - -## Edit provisioned alert rules - -Use the **Modify export** mode for alert rules to edit provisioned alert rules and export a modified version. - -{{% admonition type="note" %}} This feature is for Grafana-managed alert rules only. It is available to Admin, Viewer, and Editor roles. {{% /admonition %}} - -To edit provisioned alerting alert rules from the Grafana UI, complete the following steps. - -1. Click **Alerts & IRM** -> **Alert rules**. -1. Locate the alert rule you want to edit and click **More** -> **Modify Export** to open the Alert Rule form. -1. From the Alert Rule form, edit the fields you want to change. -1. Click **Export** to export all alert rules within the group. - - You can only export groups of rules; not single rules. - The exported rule data appears in different formats - HTML, JSON, Terraform. - -1. Choose the format to export in. -1. Click **Copy Code** or **Download**. - - a. Choose **Copy Code** to go to an existing file and paste in the code. - - b. Choose **Download** to download a file with the exported data. - -## Edit API-provisioned alerting resources - -To enable editing of API-provisioned resources in the Grafana UI, add the `X-Disable-Provenance` header to the following requests in the API: - -- `POST /api/v1/provisioning/alert-rules` -- `PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}` (calling this endpoint will change provenance for all alert rules within the alert group) -- `POST /api/v1/provisioning/contact-points` -- `POST /api/v1/provisioning/mute-timings` -- `PUT /api/v1/provisioning/policies` -- `PUT /api/v1/provisioning/templates/{name}` - -To reset the notification policy tree to the default and unlock it for editing in the Grafana UI, use the `DELETE /api/v1/provisioning/policies` endpoint. - -In Terraform, you can use the `disable_provenance` attribute on alerting resources: - -``` -provider "grafana" { - url = "http://grafana.example.com/" - auth = var.grafana_auth -} - -resource "grafana_mute_timing" "mute_all" { - name = "mute all" - disable_provenance = true - intervals {} -} -``` - -**Note:** - -You cannot edit provisioned resources from files in Grafana. You can only change the resource properties by changing the provisioning file and restarting Grafana or carrying out a hot reload. This prevents changes being made to the resource that would be overwritten if a file is provisioned again or a hot reload is carried out. diff --git a/docs/sources/developers/http_api/alerting_provisioning.md b/docs/sources/developers/http_api/alerting_provisioning.md index 7643fabd65a..0f9cfe3225a 100644 --- a/docs/sources/developers/http_api/alerting_provisioning.md +++ b/docs/sources/developers/http_api/alerting_provisioning.md @@ -12,6 +12,7 @@ keywords: - alerts labels: products: + - cloud - enterprise - oss title: 'Alerting Provisioning HTTP API ' @@ -19,1525 +20,4 @@ title: 'Alerting Provisioning HTTP API ' # Alerting provisioning HTTP API -The Alerting provisioning API can be used to create, modify, and delete resources relevant to [Grafana Managed alerts]({{< relref "/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule" >}}). And is the one used by our [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs). - -For managing resources related to [data source-managed alerts]({{< relref "/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule" >}}) including Recording Rules, you can use [Mimir tool](https://grafana.com/docs/mimir/latest/manage/tools/mimirtool/) and [Cortex tool](https://github.com/grafana/cortex-tools#cortextool) respectively. - -## Information - -### Version - -1.1.0 - -## Content negotiation - -### Consumes - -- application/json - -### Produces - -- application/json -- text/yaml -- application/yaml - -## All endpoints - -### Alert rules - -| Method | URI | Name | Summary | -| ------ | ---------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | -| DELETE | /api/v1/provisioning/alert-rules/:uid | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | -| GET | /api/v1/provisioning/alert-rules/:uid | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | -| GET | /api/v1/provisioning/alert-rules/:uid/export | [route get alert rule export](#route-get-alert-rule-export) | Export an alert rule in provisioning file format. | -| GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group | [route get alert rule group](#route-get-alert-rule-group) | Get a rule group. | -| GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export | [route get alert rule group export](#route-get-alert-rule-group-export) | Export an alert rule group in provisioning file format. | -| GET | /api/v1/provisioning/alert-rules | [route get alert rules](#route-get-alert-rules) | Get all the alert rules. | -| GET | /api/v1/provisioning/alert-rules/export | [route get alert rules export](#route-get-alert-rules-export) | Export all alert rules in provisioning file format. | -| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | -| PUT | /api/v1/provisioning/alert-rules/:uid | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | -| PUT | /api/v1/provisioning/folder/:folderUid/rule-groups/:group | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group or modify the rules of the group. | - -### Contact points - -| Method | URI | Name | Summary | -| ------ | ------------------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ | -| DELETE | /api/v1/provisioning/contact-points/:uid | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | -| GET | /api/v1/provisioning/contact-points | [route get contactpoints](#route-get-contactpoints) | Get all the contact points. | -| GET | /api/v1/provisioning/contact-points/export | [route get contactpoints export](#route-get-contactpoints-export) | Export all contact points in provisioning file format. | -| POST | /api/v1/provisioning/contact-points | [route post contactpoints](#route-post-contactpoints) | Create a contact point. | -| PUT | /api/v1/provisioning/contact-points/:uid | [route put contactpoint](#route-put-contactpoint) | Update an existing contact point. | - -### Notification policies - -| Method | URI | Name | Summary | -| ------ | ------------------------------------ | ------------------------------------------------------------- | ---------------------------------------------------------------- | -| DELETE | /api/v1/provisioning/policies | [route reset policy tree](#route-reset-policy-tree) | Clears the notification policy tree. | -| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | -| GET | /api/v1/provisioning/policies/export | [route get policy tree export](#route-get-policy-tree-export) | Export the notification policy tree in provisioning file format. | -| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | - -### Mute timings - -| Method | URI | Name | Summary | -| ------ | --------------------------------------- | ----------------------------------------------------- | -------------------------------- | -| DELETE | /api/v1/provisioning/mute-timings/:name | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | -| GET | /api/v1/provisioning/mute-timings/:name | [route get mute timing](#route-get-mute-timing) | Get a mute timing. | -| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | -| POST | /api/v1/provisioning/mute-timings | [route post mute timing](#route-post-mute-timing) | Create a new mute timing. | -| PUT | /api/v1/provisioning/mute-timings/:name | [route put mute timing](#route-put-mute-timing) | Replace an existing mute timing. | - -### Templates - -| Method | URI | Name | Summary | -| ------ | ------------------------------------ | ----------------------------------------------- | ------------------------------------------ | -| DELETE | /api/v1/provisioning/templates/:name | [route delete template](#route-delete-template) | Delete a template. | -| GET | /api/v1/provisioning/templates/:name | [route get template](#route-get-template) | Get a notification template. | -| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. | -| PUT | /api/v1/provisioning/templates/:name | [route put template](#route-put-template) | Updates an existing notification template. | - -## Paths - -### Delete a specific alert rule by UID. (_RouteDeleteAlertRule_) - -``` -DELETE /api/v1/provisioning/alert-rules/:uid -``` - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | ------ | -------- | --------- | :------: | ------- | --------------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | Alert rule UID | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | ---------- | ---------------------------------------- | :---------: | --------------------------------------------- | -| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) | - -#### Responses - -##### 204 - The alert rule was deleted successfully. - -Status: No Content - -###### Schema - -### Delete a contact point. (_RouteDeleteContactpoints_) - -``` -DELETE /api/v1/provisioning/contact-points/:uid -``` - -#### Consumes - -- application/json - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------ | -| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| -------------------------------------- | ---------- | ------------------------------------------- | :---------: | ------------------------------------------------ | -| [204](#route-delete-contactpoints-204) | No Content | The contact point was deleted successfully. | | [schema](#route-delete-contactpoints-204-schema) | - -#### Responses - -##### 204 - The contact point was deleted successfully. - -Status: No Content - -###### Schema - -### Delete a mute timing. (_RouteDeleteMuteTiming_) - -``` -DELETE /api/v1/provisioning/mute-timings/:name -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | -| name | `path` | string | `string` | | ✓ | | Mute timing name | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ------------------------------------ | ---------- | ----------------------------------------- | :---------: | ---------------------------------------------- | -| [204](#route-delete-mute-timing-204) | No Content | The mute timing was deleted successfully. | | [schema](#route-delete-mute-timing-204-schema) | - -#### Responses - -##### 204 - The mute timing was deleted successfully. - -Status: No Content - -###### Schema - -### Delete a template. (_RouteDeleteTemplate_) - -``` -DELETE /api/v1/provisioning/templates/:name -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ---------- | -------------------------------------- | :---------: | ------------------------------------------- | -| [204](#route-delete-template-204) | No Content | The template was deleted successfully. | | [schema](#route-delete-template-204-schema) | - -#### Responses - -##### 204 - The template was deleted successfully. - -Status: No Content - -###### Schema - -### Get a specific alert rule by UID. (_RouteGetAlertRule_) - -``` -GET /api/v1/provisioning/alert-rules/:uid -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- | -| UID | `path` | string | `string` | | ✓ | | Alert rule UID | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| -------------------------------- | --------- | -------------------- | :---------: | ------------------------------------------ | -| [200](#route-get-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-get-alert-rule-200-schema) | -| [404](#route-get-alert-rule-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-404-schema) | - -#### Responses - -##### 200 - ProvisionedAlertRule - -Status: OK - -###### Schema - -[ProvisionedAlertRule](#provisioned-alert-rule) - -##### 404 - Not found. - -Status: Not Found - -###### Schema - -### Export an alert rule in provisioning file format. (_RouteGetAlertRuleExport_) - -``` -GET /api/v1/provisioning/alert-rules/:uid/export -``` - -#### Produces - -- application/json -- application/yaml -- text/yaml - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------- | ------- | ------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | Alert rule UID | -| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | -| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------- | -| [200](#route-get-alert-rule-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-export-200-schema) | -| [404](#route-get-alert-rule-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-export-404-schema) | - -#### Responses - -##### 200 - AlertingFileExport - -Status: OK - -###### Schema - -[AlertingFileExport](#alerting-file-export) - -##### 404 - Not found. - -Status: Not Found - -###### Schema - -### Get a rule group. (_RouteGetAlertRuleGroup_) - -``` -GET /api/v1/provisioning/folder/:folderUid/rule-groups/:group -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| --------- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | -| FolderUID | `path` | string | `string` | | ✓ | | | -| Group | `path` | string | `string` | | ✓ | | | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| -------------------------------------- | --------- | -------------- | :---------: | ------------------------------------------------ | -| [200](#route-get-alert-rule-group-200) | OK | AlertRuleGroup | | [schema](#route-get-alert-rule-group-200-schema) | -| [404](#route-get-alert-rule-group-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-404-schema) | - -#### Responses - -##### 200 - AlertRuleGroup - -Status: OK - -###### Schema - -[AlertRuleGroup](#alert-rule-group) - -##### 404 - Not found. - -Status: Not Found - -###### Schema - -### Export an alert rule group in provisioning file format. (_RouteGetAlertRuleGroupExport_) - -``` -GET /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export -``` - -#### Produces - -- application/json -- application/yaml -- text/yaml - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| --------- | ------- | ------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- | -| FolderUID | `path` | string | `string` | | ✓ | | | -| Group | `path` | string | `string` | | ✓ | | | -| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | -| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------------- | -| [200](#route-get-alert-rule-group-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-group-export-200-schema) | -| [404](#route-get-alert-rule-group-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-export-404-schema) | - -#### Responses - -##### 200 - AlertingFileExport - -Status: OK - -###### Schema - -[AlertingFileExport](#alerting-file-export) - -##### 404 - Not found. - -Status: Not Found - -###### Schema - -### Get all the alert rules. (_RouteGetAlertRules_) - -``` -GET /api/v1/provisioning/alert-rules -``` - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ------ | --------------------- | :---------: | ------------------------------------------- | -| [200](#route-get-alert-rules-200) | OK | ProvisionedAlertRules | | [schema](#route-get-alert-rules-200-schema) | - -#### Responses - -##### 200 - ProvisionedAlertRules - -Status: OK - -###### Schema - -[ProvisionedAlertRules](#provisioned-alert-rules) - -### Export all alert rules in provisioning file format. (_RouteGetAlertRulesExport_) - -``` -GET /api/v1/provisioning/alert-rules/export -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------- | ------- | ------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- | -| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | -| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ---------------------------------------- | --------- | ------------------ | :---------: | -------------------------------------------------- | -| [200](#route-get-alert-rules-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rules-export-200-schema) | -| [404](#route-get-alert-rules-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rules-export-404-schema) | - -#### Responses - -##### 200 - AlertingFileExport - -Status: OK - -###### Schema - -[AlertingFileExport](#alerting-file-export) - -##### 404 - Not found. - -Status: Not Found - -###### Schema - -### Get all the contact points. (_RouteGetContactpoints_) - -``` -GET /api/v1/provisioning/contact-points -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------- | ------ | -------- | --------- | :------: | ------- | -------------- | -| name | `query` | string | `string` | | | | Filter by name | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | ------ | ------------- | :---------: | --------------------------------------------- | -| [200](#route-get-contactpoints-200) | OK | ContactPoints | | [schema](#route-get-contactpoints-200-schema) | - -#### Responses - -##### 200 - ContactPoints - -Status: OK - -###### Schema - -[ContactPoints](#contact-points) - -### Export all contact points in provisioning file format. (_RouteGetContactpointsExport_) - -``` -GET /api/v1/provisioning/contact-points/export -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------- | ------- | ------- | -------- | --------- | :------: | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| decrypt | `query` | boolean | `bool` | | | | Whether any contained secure settings should be decrypted or left redacted. Redacted settings will contain RedactedValue instead. Currently, only org admin can view decrypted secure settings. | -| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | -| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | -| name | `query` | string | `string` | | | | Filter by name | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ------------------------------------------ | --------- | ------------------ | :---------: | ---------------------------------------------------- | -| [200](#route-get-contactpoints-export-200) | OK | AlertingFileExport | | [schema](#route-get-contactpoints-export-200-schema) | -| [403](#route-get-contactpoints-export-403) | Forbidden | PermissionDenied | | [schema](#route-get-contactpoints-export-403-schema) | - -#### Responses - -##### 200 - AlertingFileExport - -Status: OK - -###### Schema - -[AlertingFileExport](#alerting-file-export) - -##### 403 - PermissionDenied - -Status: Forbidden - -###### Schema - -[PermissionDenied](#permission-denied) - -### Get a mute timing. (_RouteGetMuteTiming_) - -``` -GET /api/v1/provisioning/mute-timings/:name -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | -| name | `path` | string | `string` | | ✓ | | Mute timing name | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | --------- | ---------------- | :---------: | ------------------------------------------- | -| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) | -| [404](#route-get-mute-timing-404) | Not Found | Not found. | | [schema](#route-get-mute-timing-404-schema) | - -#### Responses - -##### 200 - MuteTimeInterval - -Status: OK - -###### Schema - -[MuteTimeInterval](#mute-time-interval) - -##### 404 - Not found. - -Status: Not Found - -###### Schema - -### Get all the mute timings. (_RouteGetMuteTimings_) - -``` -GET /api/v1/provisioning/mute-timings -``` - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ---------------------------------- | ------ | ----------- | :---------: | -------------------------------------------- | -| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) | - -#### Responses - -##### 200 - MuteTimings - -Status: OK - -###### Schema - -[MuteTimings](#mute-timings) - -### Get the notification policy tree. (_RouteGetPolicyTree_) - -``` -GET /api/v1/provisioning/policies -``` - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ------ | ----------- | :---------: | ------------------------------------------- | -| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) | - -#### Responses - -##### 200 - Route - -Status: OK - -###### Schema - -[Route](#route) - -### Export the notification policy tree in provisioning file format. (_RouteGetPolicyTreeExport_) - -``` -GET /api/v1/provisioning/policies/export -``` - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ---------------------------------------- | --------- | ------------------ | :---------: | -------------------------------------------------- | -| [200](#route-get-policy-tree-export-200) | OK | AlertingFileExport | | [schema](#route-get-policy-tree-export-200-schema) | -| [404](#route-get-policy-tree-export-404) | Not Found | NotFound | | [schema](#route-get-policy-tree-export-404-schema) | - -#### Responses - -##### 200 - AlertingFileExport - -Status: OK - -###### Schema - -[AlertingFileExport](#alerting-file-export) - -##### 404 - NotFound - -Status: Not Found - -###### Schema - -[NotFound](#not-found) - -### Get a notification template. (_RouteGetTemplate_) - -``` -GET /api/v1/provisioning/templates/:name -``` - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ------------------------------ | --------- | -------------------- | :---------: | ---------------------------------------- | -| [200](#route-get-template-200) | OK | NotificationTemplate | | [schema](#route-get-template-200-schema) | -| [404](#route-get-template-404) | Not Found | Not found. | | [schema](#route-get-template-404-schema) | - -#### Responses - -##### 200 - NotificationTemplate - -Status: OK - -###### Schema - -[NotificationTemplate](#notification-template) - -##### 404 - Not found. - -Status: Not Found - -###### Schema - -### Get all notification templates. (_RouteGetTemplates_) - -``` -GET /api/v1/provisioning/templates -``` - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ------------------------------- | --------- | --------------------- | :---------: | ----------------------------------------- | -| [200](#route-get-templates-200) | OK | NotificationTemplates | | [schema](#route-get-templates-200-schema) | -| [404](#route-get-templates-404) | Not Found | Not found. | | [schema](#route-get-templates-404-schema) | - -#### Responses - -##### 200 - NotificationTemplates - -Status: OK - -###### Schema - -[NotificationTemplates](#notification-templates) - -##### 404 - Not found. - -Status: Not Found - -###### Schema - -### Create a new alert rule. (_RoutePostAlertRule_) - -``` -POST /api/v1/provisioning/alert-rules -``` - -#### Consumes - -- application/json - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------- | -| [201](#route-post-alert-rule-201) | Created | ProvisionedAlertRule | | [schema](#route-post-alert-rule-201-schema) | -| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) | - -#### Responses - -##### 201 - ProvisionedAlertRule - -Status: Created - -###### Schema - -[ProvisionedAlertRule](#provisioned-alert-rule) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Create a contact point. (_RoutePostContactpoints_) - -``` -POST /api/v1/provisioning/contact-points -``` - -#### Consumes - -- application/json - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ------------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------------- | -| [202](#route-post-contactpoints-202) | Accepted | EmbeddedContactPoint | | [schema](#route-post-contactpoints-202-schema) | -| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) | - -#### Responses - -##### 202 - EmbeddedContactPoint - -Status: Accepted - -###### Schema - -[EmbeddedContactPoint](#embedded-contact-point) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Create a new mute timing. (_RoutePostMuteTiming_) - -``` -POST /api/v1/provisioning/mute-timings -``` - -#### Consumes - -- application/json - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ---------------------------------- | ----------- | ---------------- | :---------: | -------------------------------------------- | -| [201](#route-post-mute-timing-201) | Created | MuteTimeInterval | | [schema](#route-post-mute-timing-201-schema) | -| [400](#route-post-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-post-mute-timing-400-schema) | - -#### Responses - -##### 201 - MuteTimeInterval - -Status: Created - -###### Schema - -[MuteTimeInterval](#mute-time-interval) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Update an existing alert rule. (_RoutePutAlertRule_) - -``` -PUT /api/v1/provisioning/alert-rules/:uid -``` - -#### Consumes - -- application/json - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | Alert rule UID | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| -------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------ | -| [200](#route-put-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-put-alert-rule-200-schema) | -| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) | - -#### Responses - -##### 200 - ProvisionedAlertRule - -Status: OK - -###### Schema - -[ProvisionedAlertRule](#provisioned-alert-rule) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Update the interval or alert rules of a rule group. (_RoutePutAlertRuleGroup_) - -``` -PUT /api/v1/provisioning/folder/:folderUid/rule-groups/:group -``` - -#### Consumes - -- application/json - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | ----------------------------------- | ----------------------- | --------- | :------: | ------- | ------------------------------------------------------------------------------------------------------- | -| FolderUID | `path` | string | `string` | | ✓ | | | -| Group | `path` | string | `string` | | ✓ | | | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [AlertRuleGroup](#alert-rule-group) | `models.AlertRuleGroup` | | | | This action is idempotent and rules included in this body will overwrite configured rules for the group | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| -------------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------------ | -| [200](#route-put-alert-rule-group-200) | OK | AlertRuleGroup | | [schema](#route-put-alert-rule-group-200-schema) | -| [400](#route-put-alert-rule-group-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-group-400-schema) | - -#### Responses - -##### 200 - AlertRuleGroup - -Status: OK - -###### Schema - -[AlertRuleGroup](#alert-rule-group) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Update an existing contact point. (_RoutePutContactpoint_) - -``` -PUT /api/v1/provisioning/contact-points/:uid -``` - -#### Consumes - -- application/json - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | -| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ---------------------------------- | ----------- | --------------- | :---------: | -------------------------------------------- | -| [202](#route-put-contactpoint-202) | Accepted | Ack | | [schema](#route-put-contactpoint-202-schema) | -| [400](#route-put-contactpoint-400) | Bad Request | ValidationError | | [schema](#route-put-contactpoint-400-schema) | - -#### Responses - -##### 202 - Ack - -Status: Accepted - -###### Schema - -[Ack](#ack) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Replace an existing mute timing. (_RoutePutMuteTiming_) - -``` -PUT /api/v1/provisioning/mute-timings/:name -``` - -#### Consumes - -- application/json - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | -| name | `path` | string | `string` | | ✓ | | Mute timing name | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | ---------------- | :---------: | ------------------------------------------- | -| [200](#route-put-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-put-mute-timing-200-schema) | -| [400](#route-put-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-put-mute-timing-400-schema) | - -#### Responses - -##### 200 - MuteTimeInterval - -Status: OK - -###### Schema - -[MuteTimeInterval](#mute-time-interval) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Sets the notification policy tree. (_RoutePutPolicyTree_) - -``` -PUT /api/v1/provisioning/policies -``` - -#### Consumes - -- application/json - -#### Parameters - -{{% responsive-table %}} - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | --------------- | -------------- | --------- | :------: | ------- | --------------------------------------------------------- | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [Route](#route) | `models.Route` | | | | The new notification routing tree to use | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- | -| [202](#route-put-policy-tree-202) | Accepted | Ack | | [schema](#route-put-policy-tree-202-schema) | -| [400](#route-put-policy-tree-400) | Bad Request | ValidationError | | [schema](#route-put-policy-tree-400-schema) | - -#### Responses - -##### 202 - Ack - -Status: Accepted - -###### Schema - -[Ack](#ack) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Updates an existing notification template. (_RoutePutTemplate_) - -``` -PUT /api/v1/provisioning/templates/:name -``` - -#### Consumes - -- application/json - -{{% responsive-table %}} - -#### Parameters - -| Name | Source | Type | Go type | Separator | Required | Default | Description | -| -------------------------- | -------- | ------------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | --------------------------------------------------------- | -| name | `path` | string | `string` | | ✓ | | Template Name | -| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | -| Body | `body` | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | | - -{{% /responsive-table %}} - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------- | -| [202](#route-put-template-202) | Accepted | NotificationTemplate | | [schema](#route-put-template-202-schema) | -| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | - -#### Responses - -##### 202 - NotificationTemplate - -Status: Accepted - -###### Schema - -[NotificationTemplate](#notification-template) - -##### 400 - ValidationError - -Status: Bad Request - -###### Schema - -[ValidationError](#validation-error) - -### Clears the notification policy tree. (_RouteResetPolicyTree_) - -``` -DELETE /api/v1/provisioning/policies -``` - -#### Consumes - -- application/json - -#### All responses - -| Code | Status | Description | Has headers | Schema | -| ----------------------------------- | -------- | ----------- | :---------: | --------------------------------------------- | -| [202](#route-reset-policy-tree-202) | Accepted | Ack | | [schema](#route-reset-policy-tree-202-schema) | - -#### Responses - -##### 202 - Ack - -Status: Accepted - -###### Schema - -[Ack](#ack) - -## Models - -### Ack - -[interface{}](#interface) - -### AlertQuery - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | ------------------------------------------------------------------------------------------------------ | ------- | -| datasourceUid | string | `string` | | | Grafana data source unique identifier; it should be '**expr**' for a Server Side Expression operation. | | -| model | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | | -| queryType | string | `string` | | | QueryType is an optional identifier for the type of query. | -| It can be used to distinguish different types of queries. | | -| refId | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | | -| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | - -{{% /responsive-table %}} - -### AlertQueryExport - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ----------------- | ----------------------------------------- | ------------------- | :------: | ------- | ----------- | ------- | -| datasourceUid | string | `string` | | | | | -| model | [interface{}](#interface) | `interface{}` | | | | | -| queryType | string | `string` | | | | | -| refId | string | `string` | | | | | -| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | - -{{% /responsive-table %}} - -### AlertRuleExport - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ------------ | ----------------------------------------- | --------------------- | :------: | ------- | ----------- | ------- | -| annotations | map of string | `map[string]string` | | | | | -| condition | string | `string` | | | | | -| dashboardUid | string | `string` | | | | | -| data | [][AlertQueryExport](#alert-query-export) | `[]*AlertQueryExport` | | | | | -| execErrState | string | `string` | | | | | -| for | [Duration](#duration) | `Duration` | | | | | -| isPaused | boolean | `bool` | | | | | -| labels | map of string | `map[string]string` | | | | | -| noDataState | string | `string` | | | | | -| panelId | int64 (formatted integer) | `int64` | | | | | -| title | string | `string` | | | | | -| uid | string | `string` | | | | | - -{{% /responsive-table %}} - -### AlertRuleGroup - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| --------- | ------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- | -| folderUid | string | `string` | | | | | -| interval | int64 (formatted integer) | `int64` | | | | | -| rules | [][ProvisionedAlertRule](#provisioned-alert-rule) | `[]*ProvisionedAlertRule` | | | | | -| title | string | `string` | | | | | - -{{% /responsive-table %}} - -### AlertRuleGroupExport - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| -------- | --------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- | -| folder | string | `string` | | | | | -| interval | [Duration](#duration) | `Duration` | | | | | -| name | string | `string` | | | | | -| orgId | int64 (formatted integer) | `int64` | | | | | -| rules | [][AlertRuleExport](#alert-rule-export) | `[]*AlertRuleExport` | | | | | - -{{% /responsive-table %}} - -### AlertingFileExport - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ------------- | --------------------------------------------------------- | ----------------------------- | :------: | ------- | ----------- | ------- | -| apiVersion | int64 (formatted integer) | `int64` | | | | | -| contactPoints | [][ContactPointExport](#contact-point-export) | `[]*ContactPointExport` | | | | | -| groups | [][AlertRuleGroupExport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | | -| policies | [][NotificationPolicyExport](#notification-policy-export) | `[]*NotificationPolicyExport` | | | | | - -{{% /responsive-table %}} - -### ContactPointExport - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| --------- | ------------------------------------ | ------------------- | :------: | ------- | ----------- | ------- | -| name | string | `string` | | | | | -| orgId | int64 (formatted integer) | `int64` | | | | | -| receivers | [][ReceiverExport](#receiver-export) | `[]*ReceiverExport` | | | | | - -### ContactPoints - -[][EmbeddedContactPoint](#embedded-contact-point) - -### Duration - -| Name | Type | Go type | Default | Description | Example | -| -------- | ------------------------- | ------- | ------- | ----------- | ------- | -| Duration | int64 (formatted integer) | int64 | | | | - -### EmbeddedContactPoint - -> EmbeddedContactPoint is the contact point type that is used -> by grafanas embedded alertmanager implementation. - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ------------------------------------ | ----------------------- | -------- | :------: | ------- | ----------------------------------------------------------------- | --------- | -| disableResolveMessage | boolean | `bool` | | | | `false` | -| name | string | `string` | | | Name is used as grouping key in the UI. Contact points with the | -| same name will be grouped in the UI. | `webhook_1` | -| provenance | string | `string` | | | | | -| settings | [JSON](#json) | `JSON` | ✓ | | | | -| type | string | `string` | ✓ | | | `webhook` | -| uid | string | `string` | | | UID is the unique identifier of the contact point. The UID can be | -| set by the user. | `my_external_reference` | - -{{% /responsive-table %}} - -### Json - -[interface{}](#interface) - -### MatchRegexps - -[MatchRegexps](#match-regexps) - -### MatchType - -| Name | Type | Go type | Default | Description | Example | -| --------- | ------------------------- | ------- | ------- | ----------- | ------- | -| MatchType | int64 (formatted integer) | int64 | | | | - -### Matcher - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ----- | ------------------------ | ----------- | :------: | ------- | ----------- | ------- | -| Name | string | `string` | | | | | -| Type | [MatchType](#match-type) | `MatchType` | | | | | -| Value | string | `string` | | | | | - -{{% /responsive-table %}} - -### Matchers - -> Matchers is a slice of Matchers that is sortable, implements Stringer, and -> provides a Matches method to match a LabelSet against all Matchers in the -> slice. Note that some users of Matchers might require it to be sorted. - -[][Matcher](#matcher) - -### MuteTimeInterval - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| -------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- | -| name | string | `string` | | | | | -| time_intervals | [][TimeInterval](#time-interval) | `[]*TimeInterval` | | | | | - -{{% /responsive-table %}} - -### MuteTimings - -[][MuteTimeInterval](#mute-time-interval) - -### NotFound - -[interface{}](#interface) - -### NotificationPolicyExport - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ------ | ---------------------------- | ------------- | :------: | ------- | ----------- | ------- | -| Policy | [RouteExport](#route-export) | `RouteExport` | | | inline | | -| orgId | int64 (formatted integer) | `int64` | | | | | - -### NotificationTemplate - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ---------- | ------------------------- | ------------ | :------: | ------- | ----------- | ------- | -| name | string | `string` | | | | | -| provenance | [Provenance](#provenance) | `Provenance` | | | | | -| template | string | `string` | | | | | - -{{% /responsive-table %}} - -### NotificationTemplateContent - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| -------- | ------ | -------- | :------: | ------- | ----------- | ------- | -| template | string | `string` | | | | | - -{{% /responsive-table %}} - -### NotificationTemplates - -[][NotificationTemplate](#notification-template) - -### ObjectMatchers - -[Matchers](#matchers) - -#### Inlined models - -### PermissionDenied - -[interface{}](#interface) - -### Provenance - -| Name | Type | Go type | Default | Description | Example | -| ---------- | ------ | ------- | ------- | ----------- | ------- | -| Provenance | string | string | | | | - -### ProvisionedAlertRule - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` | -| condition | string | `string` | ✓ | | | `A` | -| data | [][AlertQuery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"__expr__","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` | -| execErrState | string | `string` | ✓ | | | | -| folderUID | string | `string` | ✓ | | | `project_x` | -| for | [Duration](#duration) | `Duration` | ✓ | | | | -| id | int64 (formatted integer) | `int64` | | | | | -| isPaused | boolean | `bool` | | | | `false` | -| labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` | -| noDataState | string | `string` | ✓ | | | | -| orgID | int64 (formatted integer) | `int64` | ✓ | | | | -| provenance | [Provenance](#provenance) | `Provenance` | | | | | -| ruleGroup | string | `string` | ✓ | | | `eval_group_1` | -| title | string | `string` | ✓ | | | `Always firing` | -| uid | string | `string` | | | | | -| updated | date-time (formatted string) | `strfmt.DateTime` | | | | | - -{{% /responsive-table %}} - -### ProvisionedAlertRules - -[][ProvisionedAlertRule](#provisioned-alert-rule) - -### RawMessage - -[interface{}](#interface) - -### ReceiverExport - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| --------------------- | -------------------------- | ------------ | :------: | ------- | ----------- | ------- | -| disableResolveMessage | boolean | `bool` | | | | | -| settings | [RawMessage](#raw-message) | `RawMessage` | | | | | -| type | string | `string` | | | | | -| uid | string | `string` | | | | | - -### Regexp - -> A Regexp is safe for concurrent use by multiple goroutines, -> except for configuration methods, such as Longest. - -[interface{}](#interface) - -### RelativeTimeRange - -> RelativeTimeRange is the per query start and end time -> for requests. - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ---- | --------------------- | ---------- | :------: | ------- | ----------- | ------- | -| from | [Duration](#duration) | `Duration` | | | | | -| to | [Duration](#duration) | `Duration` | | | | | - -{{% /responsive-table %}} - -### Route - -> A Route is a node that contains definitions of how to handle alerts. This is modified -> from the upstream alertmanager in that it adds the ObjectMatchers property. - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- | -| continue | boolean | `bool` | | | | | -| group_by | []string | `[]string` | | | | | -| group_interval | string | `string` | | | | | -| group_wait | string | `string` | | | | | -| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | | -| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | | -| matchers | [Matchers](#matchers) | `Matchers` | | | | | -| mute_time_intervals | []string | `[]string` | | | | | -| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | -| provenance | [Provenance](#provenance) | `Provenance` | | | | | -| receiver | string | `string` | | | | | -| repeat_interval | string | `string` | | | | | -| routes | [][Route](#route) | `[]*Route` | | | | | - -{{% /responsive-table %}} - -### RouteExport - -> RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't usable in -> provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them. - -**Properties** - -| Name | Type | Go type | Required | Default | Description | Example | -| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- | -| continue | boolean | `bool` | | | | | -| group_by | []string | `[]string` | | | | | -| group_interval | string | `string` | | | | | -| group_wait | string | `string` | | | | | -| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | | -| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | | -| matchers | [Matchers](#matchers) | `Matchers` | | | | | -| mute_time_intervals | []string | `[]string` | | | | | -| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | -| receiver | string | `string` | | | | | -| repeat_interval | string | `string` | | | | | -| routes | [][RouteExport](#route-export) | `[]*RouteExport` | | | | | - -### TimeInterval - -> TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained -> within the interval. - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ------------- | -------------------------- | -------------- | :------: | ------- | ----------- | ------- | -| days_of_month | []string | `[]string` | | | | | -| location | string | `string` | | | | | -| months | []string | `[]string` | | | | | -| times | [][TimeRange](#time-range) | `[]*TimeRange` | | | | | -| weekdays | []string | `[]string` | | | | | -| years | []string | `[]string` | | | | | - -{{% /responsive-table %}} - -### TimeRange - -> For example, 4:00PM to End of the day would Begin at 1020 and End at 1440. - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ----------- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | -| EndMinute | int64 (formatted integer) | `int64` | | | | | -| StartMinute | int64 (formatted integer) | `int64` | | | | | - -{{% /responsive-table %}} - -### ValidationError - -**Properties** - -{{% responsive-table %}} - -| Name | Type | Go type | Required | Default | Description | Example | -| ---- | ------ | -------- | :------: | ------- | ----------- | --------------- | -| msg | string | `string` | | | | `error message` | - -{{% /responsive-table %}} +{{< docs/shared lookup="alerts/alerting_provisioning.md" source="grafana" version="latest" >}} diff --git a/docs/sources/shared/alerts/alerting_provisioning.md b/docs/sources/shared/alerts/alerting_provisioning.md new file mode 100644 index 00000000000..7be0b2d010c --- /dev/null +++ b/docs/sources/shared/alerts/alerting_provisioning.md @@ -0,0 +1,1622 @@ +--- +labels: + products: + - enterprise + - oss +title: 'Alerting Provisioning HTTP API ' +--- + +The Alerting provisioning API can be used to create, modify, and delete resources relevant to [Grafana Managed alerts]({{< relref "/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule" >}}). And is the one used by our [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs). + +For managing resources related to [data source-managed alerts]({{< relref "/docs/grafana/latest/alerting/alerting-rules/create-grafana-managed-rule" >}}) including Recording Rules, you can use [Mimir tool](https://grafana.com/docs/mimir/latest/manage/tools/mimirtool/) and [Cortex tool](https://github.com/grafana/cortex-tools#cortextool) respectively. + +## Information + +### Version + +1.1.0 + +## Content negotiation + +### Consumes + +- application/json + +### Produces + +- application/json +- text/yaml +- application/yaml + +## All endpoints + +### Alert rules + +| Method | URI | Name | Summary | +| ------ | ---------------------------------------------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------- | +| DELETE | /api/v1/provisioning/alert-rules/:uid | [route delete alert rule](#route-delete-alert-rule) | Delete a specific alert rule by UID. | +| GET | /api/v1/provisioning/alert-rules/:uid | [route get alert rule](#route-get-alert-rule) | Get a specific alert rule by UID. | +| GET | /api/v1/provisioning/alert-rules/:uid/export | [route get alert rule export](#route-get-alert-rule-export) | Export an alert rule in provisioning file format. | +| GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group | [route get alert rule group](#route-get-alert-rule-group) | Get a rule group. | +| GET | /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export | [route get alert rule group export](#route-get-alert-rule-group-export) | Export an alert rule group in provisioning file format. | +| GET | /api/v1/provisioning/alert-rules | [route get alert rules](#route-get-alert-rules) | Get all the alert rules. | +| GET | /api/v1/provisioning/alert-rules/export | [route get alert rules export](#route-get-alert-rules-export) | Export all alert rules in provisioning file format. | +| POST | /api/v1/provisioning/alert-rules | [route post alert rule](#route-post-alert-rule) | Create a new alert rule. | +| PUT | /api/v1/provisioning/alert-rules/:uid | [route put alert rule](#route-put-alert-rule) | Update an existing alert rule. | +| PUT | /api/v1/provisioning/folder/:folderUid/rule-groups/:group | [route put alert rule group](#route-put-alert-rule-group) | Update the interval of a rule group or modify the rules of the group. | + +#### Example alert rules template + +```json +{ + "title": "TEST-API_1", + "ruleGroup": "API", + "folderUID": "FOLDER", + "noDataState": "OK", + "execErrState": "OK", + "for": "5m", + "orgId": 1, + "uid": "", + "condition": "B", + "annotations": { + "summary": "test_api_1" + }, + "labels": { + "API": "test1" + }, + "data": [ + { + "refId": "A", + "queryType": "", + "relativeTimeRange": { + "from": 600, + "to": 0 + }, + "datasourceUid": " XXXXXXXXX-XXXXXXXXX-XXXXXXXXXX", + "model": { + "expr": "up", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "A" + } + }, + { + "refId": "B", + "queryType": "", + "relativeTimeRange": { + "from": 0, + "to": 0 + }, + "datasourceUid": "-100", + "model": { + "conditions": [ + { + "evaluator": { + "params": [6], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": ["A"] + }, + "reducer": { + "params": [], + "type": "last" + }, + "type": "query" + } + ], + "datasource": { + "type": "__expr__", + "uid": "-100" + }, + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 43200, + "refId": "B", + "type": "classic_conditions" + } + } + ] +} +``` + +### Contact points + +| Method | URI | Name | Summary | +| ------ | ------------------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------ | +| DELETE | /api/v1/provisioning/contact-points/:uid | [route delete contactpoints](#route-delete-contactpoints) | Delete a contact point. | +| GET | /api/v1/provisioning/contact-points | [route get contactpoints](#route-get-contactpoints) | Get all the contact points. | +| GET | /api/v1/provisioning/contact-points/export | [route get contactpoints export](#route-get-contactpoints-export) | Export all contact points in provisioning file format. | +| POST | /api/v1/provisioning/contact-points | [route post contactpoints](#route-post-contactpoints) | Create a contact point. | +| PUT | /api/v1/provisioning/contact-points/:uid | [route put contactpoint](#route-put-contactpoint) | Update an existing contact point. | + +### Notification policies + +| Method | URI | Name | Summary | +| ------ | ------------------------------------ | ------------------------------------------------------------- | ---------------------------------------------------------------- | +| DELETE | /api/v1/provisioning/policies | [route reset policy tree](#route-reset-policy-tree) | Clears the notification policy tree. | +| GET | /api/v1/provisioning/policies | [route get policy tree](#route-get-policy-tree) | Get the notification policy tree. | +| GET | /api/v1/provisioning/policies/export | [route get policy tree export](#route-get-policy-tree-export) | Export the notification policy tree in provisioning file format. | +| PUT | /api/v1/provisioning/policies | [route put policy tree](#route-put-policy-tree) | Sets the notification policy tree. | + +### Mute timings + +| Method | URI | Name | Summary | +| ------ | --------------------------------------- | ----------------------------------------------------- | -------------------------------- | +| DELETE | /api/v1/provisioning/mute-timings/:name | [route delete mute timing](#route-delete-mute-timing) | Delete a mute timing. | +| GET | /api/v1/provisioning/mute-timings/:name | [route get mute timing](#route-get-mute-timing) | Get a mute timing. | +| GET | /api/v1/provisioning/mute-timings | [route get mute timings](#route-get-mute-timings) | Get all the mute timings. | +| POST | /api/v1/provisioning/mute-timings | [route post mute timing](#route-post-mute-timing) | Create a new mute timing. | +| PUT | /api/v1/provisioning/mute-timings/:name | [route put mute timing](#route-put-mute-timing) | Replace an existing mute timing. | + +### Templates + +| Method | URI | Name | Summary | +| ------ | ------------------------------------ | ----------------------------------------------- | ------------------------------------------ | +| DELETE | /api/v1/provisioning/templates/:name | [route delete template](#route-delete-template) | Delete a template. | +| GET | /api/v1/provisioning/templates/:name | [route get template](#route-get-template) | Get a notification template. | +| GET | /api/v1/provisioning/templates | [route get templates](#route-get-templates) | Get all notification templates. | +| PUT | /api/v1/provisioning/templates/:name | [route put template](#route-put-template) | Updates an existing notification template. | + +## Edit resources in the Grafana UI + +By default, you cannot edit API-provisioned alerting resources in Grafana. To enable editing these resources in the Grafana UI, add the `X-Disable-Provenance` header to the following requests in the API: + +- `POST /api/v1/provisioning/alert-rules` +- `PUT /api/v1/provisioning/folder/{FolderUID}/rule-groups/{Group}` (calling this endpoint will change provenance for all alert rules within the alert group) +- `POST /api/v1/provisioning/contact-points` +- `POST /api/v1/provisioning/mute-timings` +- `PUT /api/v1/provisioning/policies` +- `PUT /api/v1/provisioning/templates/{name}` + +To reset the notification policy tree to the default and unlock it for editing in the Grafana UI, use the `DELETE /api/v1/provisioning/policies` endpoint. + +## Paths + +### Delete a specific alert rule by UID. (_RouteDeleteAlertRule_) + +``` +DELETE /api/v1/provisioning/alert-rules/:uid +``` + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | ------ | -------- | --------- | :------: | ------- | --------------------------------------------------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | ---------- | ---------------------------------------- | :---------: | --------------------------------------------- | +| [204](#route-delete-alert-rule-204) | No Content | The alert rule was deleted successfully. | | [schema](#route-delete-alert-rule-204-schema) | + +#### Responses + +##### 204 - The alert rule was deleted successfully. + +Status: No Content + +###### Schema + +### Delete a contact point. (_RouteDeleteContactpoints_) + +``` +DELETE /api/v1/provisioning/contact-points/:uid +``` + +#### Consumes + +- application/json + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------------------------------------ | +| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | ---------- | ------------------------------------------- | :---------: | ------------------------------------------------ | +| [204](#route-delete-contactpoints-204) | No Content | The contact point was deleted successfully. | | [schema](#route-delete-contactpoints-204-schema) | + +#### Responses + +##### 204 - The contact point was deleted successfully. + +Status: No Content + +###### Schema + +### Delete a mute timing. (_RouteDeleteMuteTiming_) + +``` +DELETE /api/v1/provisioning/mute-timings/:name +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ------------------------------------ | ---------- | ----------------------------------------- | :---------: | ---------------------------------------------- | +| [204](#route-delete-mute-timing-204) | No Content | The mute timing was deleted successfully. | | [schema](#route-delete-mute-timing-204-schema) | + +#### Responses + +##### 204 - The mute timing was deleted successfully. + +Status: No Content + +###### Schema + +### Delete a template. (_RouteDeleteTemplate_) + +``` +DELETE /api/v1/provisioning/templates/:name +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | +| name | `path` | string | `string` | | ✓ | | Template Name | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ---------- | -------------------------------------- | :---------: | ------------------------------------------- | +| [204](#route-delete-template-204) | No Content | The template was deleted successfully. | | [schema](#route-delete-template-204-schema) | + +#### Responses + +##### 204 - The template was deleted successfully. + +Status: No Content + +###### Schema + +### Get a specific alert rule by UID. (_RouteGetAlertRule_) + +``` +GET /api/v1/provisioning/alert-rules/:uid +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | -------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| -------------------------------- | --------- | -------------------- | :---------: | ------------------------------------------ | +| [200](#route-get-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-get-alert-rule-200-schema) | +| [404](#route-get-alert-rule-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-404-schema) | + +#### Responses + +##### 200 - ProvisionedAlertRule + +Status: OK + +###### Schema + +[ProvisionedAlertRule](#provisioned-alert-rule) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Export an alert rule in provisioning file format. (_RouteGetAlertRuleExport_) + +``` +GET /api/v1/provisioning/alert-rules/:uid/export +``` + +#### Produces + +- application/json +- application/yaml +- text/yaml + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | +| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------- | +| [200](#route-get-alert-rule-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-export-200-schema) | +| [404](#route-get-alert-rule-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get a rule group. (_RouteGetAlertRuleGroup_) + +``` +GET /api/v1/provisioning/folder/:folderUid/rule-groups/:group +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| --------- | ------ | ------ | -------- | --------- | :------: | ------- | ----------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | --------- | -------------- | :---------: | ------------------------------------------------ | +| [200](#route-get-alert-rule-group-200) | OK | AlertRuleGroup | | [schema](#route-get-alert-rule-group-200-schema) | +| [404](#route-get-alert-rule-group-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-404-schema) | + +#### Responses + +##### 200 - AlertRuleGroup + +Status: OK + +###### Schema + +[AlertRuleGroup](#alert-rule-group) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Export an alert rule group in provisioning file format. (_RouteGetAlertRuleGroupExport_) + +``` +GET /api/v1/provisioning/folder/:folderUid/rule-groups/:group/export +``` + +#### Produces + +- application/json +- application/yaml +- text/yaml + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| --------- | ------- | ------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | +| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------------------- | --------- | ------------------ | :---------: | ------------------------------------------------------- | +| [200](#route-get-alert-rule-group-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rule-group-export-200-schema) | +| [404](#route-get-alert-rule-group-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rule-group-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get all the alert rules. (_RouteGetAlertRules_) + +``` +GET /api/v1/provisioning/alert-rules +``` + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ------ | --------------------- | :---------: | ------------------------------------------- | +| [200](#route-get-alert-rules-200) | OK | ProvisionedAlertRules | | [schema](#route-get-alert-rules-200-schema) | + +#### Responses + +##### 200 - ProvisionedAlertRules + +Status: OK + +###### Schema + +[ProvisionedAlertRules](#provisioned-alert-rules) + +### Export all alert rules in provisioning file format. (_RouteGetAlertRulesExport_) + +``` +GET /api/v1/provisioning/alert-rules/export +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | -------- | --------- | :------: | -------- | --------------------------------------------------------------------------------------------------------------------------------- | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | +| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------------- | --------- | ------------------ | :---------: | -------------------------------------------------- | +| [200](#route-get-alert-rules-export-200) | OK | AlertingFileExport | | [schema](#route-get-alert-rules-export-200-schema) | +| [404](#route-get-alert-rules-export-404) | Not Found | Not found. | | [schema](#route-get-alert-rules-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get all the contact points. (_RouteGetContactpoints_) + +``` +GET /api/v1/provisioning/contact-points +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------- | ------ | -------- | --------- | :------: | ------- | -------------- | +| name | `query` | string | `string` | | | | Filter by name | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | ------ | ------------- | :---------: | --------------------------------------------- | +| [200](#route-get-contactpoints-200) | OK | ContactPoints | | [schema](#route-get-contactpoints-200-schema) | + +#### Responses + +##### 200 - ContactPoints + +Status: OK + +###### Schema + +[ContactPoints](#contact-points) + +### Export all contact points in provisioning file format. (_RouteGetContactpointsExport_) + +``` +GET /api/v1/provisioning/contact-points/export +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------- | ------- | ------- | -------- | --------- | :------: | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| decrypt | `query` | boolean | `bool` | | | | Whether any contained secure settings should be decrypted or left redacted. Redacted settings will contain RedactedValue instead. Currently, only org admin can view decrypted secure settings. | +| download | `query` | boolean | `bool` | | | | Whether to initiate a download of the file or not. | +| format | `query` | string | `string` | | | `"yaml"` | Format of the downloaded file, either yaml or json. Accept header can also be used, but the query parameter will take precedence. | +| name | `query` | string | `string` | | | | Filter by name | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ------------------------------------------ | --------- | ------------------ | :---------: | ---------------------------------------------------- | +| [200](#route-get-contactpoints-export-200) | OK | AlertingFileExport | | [schema](#route-get-contactpoints-export-200-schema) | +| [403](#route-get-contactpoints-export-403) | Forbidden | PermissionDenied | | [schema](#route-get-contactpoints-export-403-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 403 - PermissionDenied + +Status: Forbidden + +###### Schema + +[PermissionDenied](#permission-denied) + +### Get a mute timing. (_RouteGetMuteTiming_) + +``` +GET /api/v1/provisioning/mute-timings/:name +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ---------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | --------- | ---------------- | :---------: | ------------------------------------------- | +| [200](#route-get-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-get-mute-timing-200-schema) | +| [404](#route-get-mute-timing-404) | Not Found | Not found. | | [schema](#route-get-mute-timing-404-schema) | + +#### Responses + +##### 200 - MuteTimeInterval + +Status: OK + +###### Schema + +[MuteTimeInterval](#mute-time-interval) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get all the mute timings. (_RouteGetMuteTimings_) + +``` +GET /api/v1/provisioning/mute-timings +``` + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------- | ------ | ----------- | :---------: | -------------------------------------------- | +| [200](#route-get-mute-timings-200) | OK | MuteTimings | | [schema](#route-get-mute-timings-200-schema) | + +#### Responses + +##### 200 - MuteTimings + +Status: OK + +###### Schema + +[MuteTimings](#mute-timings) + +### Get the notification policy tree. (_RouteGetPolicyTree_) + +``` +GET /api/v1/provisioning/policies +``` + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ------ | ----------- | :---------: | ------------------------------------------- | +| [200](#route-get-policy-tree-200) | OK | Route | | [schema](#route-get-policy-tree-200-schema) | + +#### Responses + +##### 200 - Route + +Status: OK + +###### Schema + +[Route](#route) + +### Export the notification policy tree in provisioning file format. (_RouteGetPolicyTreeExport_) + +``` +GET /api/v1/provisioning/policies/export +``` + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------------- | --------- | ------------------ | :---------: | -------------------------------------------------- | +| [200](#route-get-policy-tree-export-200) | OK | AlertingFileExport | | [schema](#route-get-policy-tree-export-200-schema) | +| [404](#route-get-policy-tree-export-404) | Not Found | NotFound | | [schema](#route-get-policy-tree-export-404-schema) | + +#### Responses + +##### 200 - AlertingFileExport + +Status: OK + +###### Schema + +[AlertingFileExport](#alerting-file-export) + +##### 404 - NotFound + +Status: Not Found + +###### Schema + +[NotFound](#not-found) + +### Get a notification template. (_RouteGetTemplate_) + +``` +GET /api/v1/provisioning/templates/:name +``` + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| ---- | ------ | ------ | -------- | --------- | :------: | ------- | ------------- | +| name | `path` | string | `string` | | ✓ | | Template Name | + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ------------------------------ | --------- | -------------------- | :---------: | ---------------------------------------- | +| [200](#route-get-template-200) | OK | NotificationTemplate | | [schema](#route-get-template-200-schema) | +| [404](#route-get-template-404) | Not Found | Not found. | | [schema](#route-get-template-404-schema) | + +#### Responses + +##### 200 - NotificationTemplate + +Status: OK + +###### Schema + +[NotificationTemplate](#notification-template) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Get all notification templates. (_RouteGetTemplates_) + +``` +GET /api/v1/provisioning/templates +``` + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ------------------------------- | --------- | --------------------- | :---------: | ----------------------------------------- | +| [200](#route-get-templates-200) | OK | NotificationTemplates | | [schema](#route-get-templates-200-schema) | +| [404](#route-get-templates-404) | Not Found | Not found. | | [schema](#route-get-templates-404-schema) | + +#### Responses + +##### 200 - NotificationTemplates + +Status: OK + +###### Schema + +[NotificationTemplates](#notification-templates) + +##### 404 - Not found. + +Status: Not Found + +###### Schema + +### Create a new alert rule. (_RoutePostAlertRule_) + +``` +POST /api/v1/provisioning/alert-rules +``` + +#### Consumes + +- application/json + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------- | +| [201](#route-post-alert-rule-201) | Created | ProvisionedAlertRule | | [schema](#route-post-alert-rule-201-schema) | +| [400](#route-post-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-post-alert-rule-400-schema) | + +#### Responses + +##### 201 - ProvisionedAlertRule + +Status: Created + +###### Schema + +[ProvisionedAlertRule](#provisioned-alert-rule) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Create a contact point. (_RoutePostContactpoints_) + +``` +POST /api/v1/provisioning/contact-points +``` + +#### Consumes + +- application/json + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ------------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------------- | +| [202](#route-post-contactpoints-202) | Accepted | EmbeddedContactPoint | | [schema](#route-post-contactpoints-202-schema) | +| [400](#route-post-contactpoints-400) | Bad Request | ValidationError | | [schema](#route-post-contactpoints-400-schema) | + +#### Responses + +##### 202 - EmbeddedContactPoint + +Status: Accepted + +###### Schema + +[EmbeddedContactPoint](#embedded-contact-point) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Create a new mute timing. (_RoutePostMuteTiming_) + +``` +POST /api/v1/provisioning/mute-timings +``` + +#### Consumes + +- application/json + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------- | ----------- | ---------------- | :---------: | -------------------------------------------- | +| [201](#route-post-mute-timing-201) | Created | MuteTimeInterval | | [schema](#route-post-mute-timing-201-schema) | +| [400](#route-post-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-post-mute-timing-400-schema) | + +#### Responses + +##### 201 - MuteTimeInterval + +Status: Created + +###### Schema + +[MuteTimeInterval](#mute-time-interval) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Update an existing alert rule. (_RoutePutAlertRule_) + +``` +PUT /api/v1/provisioning/alert-rules/:uid +``` + +#### Consumes + +- application/json + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | +| UID | `path` | string | `string` | | ✓ | | Alert rule UID | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [ProvisionedAlertRule](#provisioned-alert-rule) | `models.ProvisionedAlertRule` | | | | | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| -------------------------------- | ----------- | -------------------- | :---------: | ------------------------------------------ | +| [200](#route-put-alert-rule-200) | OK | ProvisionedAlertRule | | [schema](#route-put-alert-rule-200-schema) | +| [400](#route-put-alert-rule-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-400-schema) | + +#### Responses + +##### 200 - ProvisionedAlertRule + +Status: OK + +###### Schema + +[ProvisionedAlertRule](#provisioned-alert-rule) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Update the interval or alert rules of a rule group. (_RoutePutAlertRuleGroup_) + +``` +PUT /api/v1/provisioning/folder/:folderUid/rule-groups/:group +``` + +#### Consumes + +- application/json + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | ----------------------------------- | ----------------------- | --------- | :------: | ------- | ------------------------------------------------------------------------------------------------------- | +| FolderUID | `path` | string | `string` | | ✓ | | | +| Group | `path` | string | `string` | | ✓ | | | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [AlertRuleGroup](#alert-rule-group) | `models.AlertRuleGroup` | | | | This action is idempotent and rules included in this body will overwrite configured rules for the group | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| -------------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------------ | +| [200](#route-put-alert-rule-group-200) | OK | AlertRuleGroup | | [schema](#route-put-alert-rule-group-200-schema) | +| [400](#route-put-alert-rule-group-400) | Bad Request | ValidationError | | [schema](#route-put-alert-rule-group-400-schema) | + +#### Responses + +##### 200 - AlertRuleGroup + +Status: OK + +###### Schema + +[AlertRuleGroup](#alert-rule-group) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Update an existing contact point. (_RoutePutContactpoint_) + +``` +PUT /api/v1/provisioning/contact-points/:uid +``` + +#### Consumes + +- application/json + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | ----------------------------------------------- | ----------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | +| UID | `path` | string | `string` | | ✓ | | UID is the contact point unique identifier | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [EmbeddedContactPoint](#embedded-contact-point) | `models.EmbeddedContactPoint` | | | | | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ---------------------------------- | ----------- | --------------- | :---------: | -------------------------------------------- | +| [202](#route-put-contactpoint-202) | Accepted | Ack | | [schema](#route-put-contactpoint-202-schema) | +| [400](#route-put-contactpoint-400) | Bad Request | ValidationError | | [schema](#route-put-contactpoint-400-schema) | + +#### Responses + +##### 202 - Ack + +Status: Accepted + +###### Schema + +[Ack](#ack) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Replace an existing mute timing. (_RoutePutMuteTiming_) + +``` +PUT /api/v1/provisioning/mute-timings/:name +``` + +#### Consumes + +- application/json + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | --------------------------------------- | ------------------------- | --------- | :------: | ------- | --------------------------------------------------------- | +| name | `path` | string | `string` | | ✓ | | Mute timing name | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [MuteTimeInterval](#mute-time-interval) | `models.MuteTimeInterval` | | | | | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ----------- | ---------------- | :---------: | ------------------------------------------- | +| [200](#route-put-mute-timing-200) | OK | MuteTimeInterval | | [schema](#route-put-mute-timing-200-schema) | +| [400](#route-put-mute-timing-400) | Bad Request | ValidationError | | [schema](#route-put-mute-timing-400-schema) | + +#### Responses + +##### 200 - MuteTimeInterval + +Status: OK + +###### Schema + +[MuteTimeInterval](#mute-time-interval) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Sets the notification policy tree. (_RoutePutPolicyTree_) + +``` +PUT /api/v1/provisioning/policies +``` + +#### Consumes + +- application/json + +#### Parameters + +{{% responsive-table %}} + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | --------------- | -------------- | --------- | :------: | ------- | --------------------------------------------------------- | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [Route](#route) | `models.Route` | | | | The new notification routing tree to use | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| --------------------------------- | ----------- | --------------- | :---------: | ------------------------------------------- | +| [202](#route-put-policy-tree-202) | Accepted | Ack | | [schema](#route-put-policy-tree-202-schema) | +| [400](#route-put-policy-tree-400) | Bad Request | ValidationError | | [schema](#route-put-policy-tree-400-schema) | + +#### Responses + +##### 202 - Ack + +Status: Accepted + +###### Schema + +[Ack](#ack) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Updates an existing notification template. (_RoutePutTemplate_) + +``` +PUT /api/v1/provisioning/templates/:name +``` + +#### Consumes + +- application/json + +{{% responsive-table %}} + +#### Parameters + +| Name | Source | Type | Go type | Separator | Required | Default | Description | +| -------------------------- | -------- | ------------------------------------------------------------- | ------------------------------------ | --------- | :------: | ------- | --------------------------------------------------------- | +| name | `path` | string | `string` | | ✓ | | Template Name | +| X-Disable-Provenance: true | `header` | string | `string` | | | | Allows editing of provisioned resources in the Grafana UI | +| Body | `body` | [NotificationTemplateContent](#notification-template-content) | `models.NotificationTemplateContent` | | | | | + +{{% /responsive-table %}} + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ------------------------------ | ----------- | -------------------- | :---------: | ---------------------------------------- | +| [202](#route-put-template-202) | Accepted | NotificationTemplate | | [schema](#route-put-template-202-schema) | +| [400](#route-put-template-400) | Bad Request | ValidationError | | [schema](#route-put-template-400-schema) | + +#### Responses + +##### 202 - NotificationTemplate + +Status: Accepted + +###### Schema + +[NotificationTemplate](#notification-template) + +##### 400 - ValidationError + +Status: Bad Request + +###### Schema + +[ValidationError](#validation-error) + +### Clears the notification policy tree. (_RouteResetPolicyTree_) + +``` +DELETE /api/v1/provisioning/policies +``` + +#### Consumes + +- application/json + +#### All responses + +| Code | Status | Description | Has headers | Schema | +| ----------------------------------- | -------- | ----------- | :---------: | --------------------------------------------- | +| [202](#route-reset-policy-tree-202) | Accepted | Ack | | [schema](#route-reset-policy-tree-202-schema) | + +#### Responses + +##### 202 - Ack + +Status: Accepted + +###### Schema + +[Ack](#ack) + +## Models + +### Ack + +[interface{}](#interface) + +### AlertQuery + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| --------------------------------------------------------- | ----------------------------------------- | ------------------- | :------: | ------- | ------------------------------------------------------------------------------------------------------ | ------- | +| datasourceUid | string | `string` | | | Grafana data source unique identifier; it should be '**expr**' for a Server Side Expression operation. | | +| model | [interface{}](#interface) | `interface{}` | | | JSON is the raw JSON query and includes the above properties as well as custom properties. | | +| queryType | string | `string` | | | QueryType is an optional identifier for the type of query. | +| It can be used to distinguish different types of queries. | | +| refId | string | `string` | | | RefID is the unique identifier of the query, set by the frontend call. | | +| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | + +{{% /responsive-table %}} + +### AlertQueryExport + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ----------------- | ----------------------------------------- | ------------------- | :------: | ------- | ----------- | ------- | +| datasourceUid | string | `string` | | | | | +| model | [interface{}](#interface) | `interface{}` | | | | | +| queryType | string | `string` | | | | | +| refId | string | `string` | | | | | +| relativeTimeRange | [RelativeTimeRange](#relative-time-range) | `RelativeTimeRange` | | | | | + +{{% /responsive-table %}} + +### AlertRuleExport + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------ | ----------------------------------------- | --------------------- | :------: | ------- | ----------- | ------- | +| annotations | map of string | `map[string]string` | | | | | +| condition | string | `string` | | | | | +| dashboardUid | string | `string` | | | | | +| data | [][AlertQueryExport](#alert-query-export) | `[]*AlertQueryExport` | | | | | +| execErrState | string | `string` | | | | | +| for | [Duration](#duration) | `Duration` | | | | | +| isPaused | boolean | `bool` | | | | | +| labels | map of string | `map[string]string` | | | | | +| noDataState | string | `string` | | | | | +| panelId | int64 (formatted integer) | `int64` | | | | | +| title | string | `string` | | | | | +| uid | string | `string` | | | | | + +{{% /responsive-table %}} + +### AlertRuleGroup + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| --------- | ------------------------------------------------- | ------------------------- | :------: | ------- | ----------- | ------- | +| folderUid | string | `string` | | | | | +| interval | int64 (formatted integer) | `int64` | | | | | +| rules | [][ProvisionedAlertRule](#provisioned-alert-rule) | `[]*ProvisionedAlertRule` | | | | | +| title | string | `string` | | | | | + +{{% /responsive-table %}} + +### AlertRuleGroupExport + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| -------- | --------------------------------------- | -------------------- | :------: | ------- | ----------- | ------- | +| folder | string | `string` | | | | | +| interval | [Duration](#duration) | `Duration` | | | | | +| name | string | `string` | | | | | +| orgId | int64 (formatted integer) | `int64` | | | | | +| rules | [][AlertRuleExport](#alert-rule-export) | `[]*AlertRuleExport` | | | | | + +{{% /responsive-table %}} + +### AlertingFileExport + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------- | --------------------------------------------------------- | ----------------------------- | :------: | ------- | ----------- | ------- | +| apiVersion | int64 (formatted integer) | `int64` | | | | | +| contactPoints | [][ContactPointExport](#contact-point-export) | `[]*ContactPointExport` | | | | | +| groups | [][AlertRuleGroupExport](#alert-rule-group-export) | `[]*AlertRuleGroupExport` | | | | | +| policies | [][NotificationPolicyExport](#notification-policy-export) | `[]*NotificationPolicyExport` | | | | | + +{{% /responsive-table %}} + +### ContactPointExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| --------- | ------------------------------------ | ------------------- | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| orgId | int64 (formatted integer) | `int64` | | | | | +| receivers | [][ReceiverExport](#receiver-export) | `[]*ReceiverExport` | | | | | + +### ContactPoints + +[][EmbeddedContactPoint](#embedded-contact-point) + +### Duration + +| Name | Type | Go type | Default | Description | Example | +| -------- | ------------------------- | ------- | ------- | ----------- | ------- | +| Duration | int64 (formatted integer) | int64 | | | | + +### EmbeddedContactPoint + +> EmbeddedContactPoint is the contact point type that is used +> by grafanas embedded alertmanager implementation. + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------------------------ | ----------------------- | -------- | :------: | ------- | ----------------------------------------------------------------- | --------- | +| disableResolveMessage | boolean | `bool` | | | | `false` | +| name | string | `string` | | | Name is used as grouping key in the UI. Contact points with the | +| same name will be grouped in the UI. | `webhook_1` | +| provenance | string | `string` | | | | | +| settings | [JSON](#json) | `JSON` | ✓ | | | | +| type | string | `string` | ✓ | | | `webhook` | +| uid | string | `string` | | | UID is the unique identifier of the contact point. The UID can be | +| set by the user. | `my_external_reference` | + +{{% /responsive-table %}} + +### Json + +[interface{}](#interface) + +### MatchRegexps + +[MatchRegexps](#match-regexps) + +### MatchType + +| Name | Type | Go type | Default | Description | Example | +| --------- | ------------------------- | ------- | ------- | ----------- | ------- | +| MatchType | int64 (formatted integer) | int64 | | | | + +### Matcher + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ----- | ------------------------ | ----------- | :------: | ------- | ----------- | ------- | +| Name | string | `string` | | | | | +| Type | [MatchType](#match-type) | `MatchType` | | | | | +| Value | string | `string` | | | | | + +{{% /responsive-table %}} + +### Matchers + +> Matchers is a slice of Matchers that is sortable, implements Stringer, and +> provides a Matches method to match a LabelSet against all Matchers in the +> slice. Note that some users of Matchers might require it to be sorted. + +[][Matcher](#matcher) + +### MuteTimeInterval + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| -------------- | -------------------------------- | ----------------- | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| time_intervals | [][TimeInterval](#time-interval) | `[]*TimeInterval` | | | | | + +{{% /responsive-table %}} + +### MuteTimings + +[][MuteTimeInterval](#mute-time-interval) + +### NotFound + +[interface{}](#interface) + +### NotificationPolicyExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ------ | ---------------------------- | ------------- | :------: | ------- | ----------- | ------- | +| Policy | [RouteExport](#route-export) | `RouteExport` | | | inline | | +| orgId | int64 (formatted integer) | `int64` | | | | | + +### NotificationTemplate + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ---------- | ------------------------- | ------------ | :------: | ------- | ----------- | ------- | +| name | string | `string` | | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| template | string | `string` | | | | | + +{{% /responsive-table %}} + +### NotificationTemplateContent + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| -------- | ------ | -------- | :------: | ------- | ----------- | ------- | +| template | string | `string` | | | | | + +{{% /responsive-table %}} + +### NotificationTemplates + +[][NotificationTemplate](#notification-template) + +### ObjectMatchers + +[Matchers](#matchers) + +#### Inlined models + +### PermissionDenied + +[interface{}](#interface) + +### Provenance + +| Name | Type | Go type | Default | Description | Example | +| ---------- | ------ | ------- | ------- | ----------- | ------- | +| Provenance | string | string | | | | + +### ProvisionedAlertRule + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------ | ---------------------------- | ------------------- | :------: | ------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| annotations | map of string | `map[string]string` | | | | `{"runbook_url":"https://supercoolrunbook.com/page/13"}` | +| condition | string | `string` | ✓ | | | `A` | +| data | [][AlertQuery](#alert-query) | `[]*AlertQuery` | ✓ | | | `[{"datasourceUid":"__expr__","model":{"conditions":[{"evaluator":{"params":[0,0],"type":"gt"},"operator":{"type":"and"},"query":{"params":[]},"reducer":{"params":[],"type":"avg"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1 == 1","hide":false,"intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"},"queryType":"","refId":"A","relativeTimeRange":{"from":0,"to":0}}]` | +| execErrState | string | `string` | ✓ | | | | +| folderUID | string | `string` | ✓ | | | `project_x` | +| for | [Duration](#duration) | `Duration` | ✓ | | | | +| id | int64 (formatted integer) | `int64` | | | | | +| isPaused | boolean | `bool` | | | | `false` | +| labels | map of string | `map[string]string` | | | | `{"team":"sre-team-1"}` | +| noDataState | string | `string` | ✓ | | | | +| orgID | int64 (formatted integer) | `int64` | ✓ | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| ruleGroup | string | `string` | ✓ | | | `eval_group_1` | +| title | string | `string` | ✓ | | | `Always firing` | +| uid | string | `string` | | | | | +| updated | date-time (formatted string) | `strfmt.DateTime` | | | | | + +{{% /responsive-table %}} + +### ProvisionedAlertRules + +[][ProvisionedAlertRule](#provisioned-alert-rule) + +### RawMessage + +[interface{}](#interface) + +### ReceiverExport + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| --------------------- | -------------------------- | ------------ | :------: | ------- | ----------- | ------- | +| disableResolveMessage | boolean | `bool` | | | | | +| settings | [RawMessage](#raw-message) | `RawMessage` | | | | | +| type | string | `string` | | | | | +| uid | string | `string` | | | | | + +### Regexp + +> A Regexp is safe for concurrent use by multiple goroutines, +> except for configuration methods, such as Longest. + +[interface{}](#interface) + +### RelativeTimeRange + +> RelativeTimeRange is the per query start and end time +> for requests. + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ---- | --------------------- | ---------- | :------: | ------- | ----------- | ------- | +| from | [Duration](#duration) | `Duration` | | | | | +| to | [Duration](#duration) | `Duration` | | | | | + +{{% /responsive-table %}} + +### Route + +> A Route is a node that contains definitions of how to handle alerts. This is modified +> from the upstream alertmanager in that it adds the ObjectMatchers property. + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- | +| continue | boolean | `bool` | | | | | +| group_by | []string | `[]string` | | | | | +| group_interval | string | `string` | | | | | +| group_wait | string | `string` | | | | | +| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | | +| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | | +| matchers | [Matchers](#matchers) | `Matchers` | | | | | +| mute_time_intervals | []string | `[]string` | | | | | +| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | +| provenance | [Provenance](#provenance) | `Provenance` | | | | | +| receiver | string | `string` | | | | | +| repeat_interval | string | `string` | | | | | +| routes | [][Route](#route) | `[]*Route` | | | | | + +{{% /responsive-table %}} + +### RouteExport + +> RouteExport is the provisioned file export of definitions.Route. This is needed to hide fields that aren't usable in +> provisioning file format. An alternative would be to define a custom MarshalJSON and MarshalYAML that excludes them. + +**Properties** + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------------- | ---------------------------------- | ------------------- | :------: | ------- | --------------------------------------- | ------- | +| continue | boolean | `bool` | | | | | +| group_by | []string | `[]string` | | | | | +| group_interval | string | `string` | | | | | +| group_wait | string | `string` | | | | | +| match | map of string | `map[string]string` | | | Deprecated. Remove before v1.0 release. | | +| match_re | [MatchRegexps](#match-regexps) | `MatchRegexps` | | | | | +| matchers | [Matchers](#matchers) | `Matchers` | | | | | +| mute_time_intervals | []string | `[]string` | | | | | +| object_matchers | [ObjectMatchers](#object-matchers) | `ObjectMatchers` | | | | | +| receiver | string | `string` | | | | | +| repeat_interval | string | `string` | | | | | +| routes | [][RouteExport](#route-export) | `[]*RouteExport` | | | | | + +### TimeInterval + +> TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained +> within the interval. + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ------------- | -------------------------- | -------------- | :------: | ------- | ----------- | ------- | +| days_of_month | []string | `[]string` | | | | | +| location | string | `string` | | | | | +| months | []string | `[]string` | | | | | +| times | [][TimeRange](#time-range) | `[]*TimeRange` | | | | | +| weekdays | []string | `[]string` | | | | | +| years | []string | `[]string` | | | | | + +{{% /responsive-table %}} + +### TimeRange + +> For example, 4:00PM to End of the day would Begin at 1020 and End at 1440. + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ----------- | ------------------------- | ------- | :------: | ------- | ----------- | ------- | +| EndMinute | int64 (formatted integer) | `int64` | | | | | +| StartMinute | int64 (formatted integer) | `int64` | | | | | + +{{% /responsive-table %}} + +### ValidationError + +**Properties** + +{{% responsive-table %}} + +| Name | Type | Go type | Required | Default | Description | Example | +| ---- | ------ | -------- | :------: | ------- | ----------- | --------------- | +| msg | string | `string` | | | | `error message` | + +{{% /responsive-table %}} From 00e96e45847dbf280e6a6ebeed92e2a8b44d62b0 Mon Sep 17 00:00:00 2001 From: Misi Date: Mon, 12 Feb 2024 11:12:08 +0100 Subject: [PATCH 39/50] Auth: SSO Settings UI frontend improvements (#82264) * Add frontend fixes * Update labels + link --- .../auth-config/AuthProvidersListPage.tsx | 5 ++- .../auth-config/ProviderConfigForm.test.tsx | 4 ++- .../auth-config/ProviderConfigForm.tsx | 6 ++-- public/app/features/auth-config/fields.tsx | 32 +++++++++++++------ public/app/features/auth-config/types.ts | 1 + 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/public/app/features/auth-config/AuthProvidersListPage.tsx b/public/app/features/auth-config/AuthProvidersListPage.tsx index 77acdcc2c01..07a2e54fbb3 100644 --- a/public/app/features/auth-config/AuthProvidersListPage.tsx +++ b/public/app/features/auth-config/AuthProvidersListPage.tsx @@ -62,7 +62,10 @@ export const AuthConfigPageUnconnected = ({ subTitle={ <> Manage your auth settings and configure single sign-on. Find out more in our{' '} - + documentation . diff --git a/public/app/features/auth-config/ProviderConfigForm.test.tsx b/public/app/features/auth-config/ProviderConfigForm.test.tsx index ac76732075c..0386ab6e710 100644 --- a/public/app/features/auth-config/ProviderConfigForm.test.tsx +++ b/public/app/features/auth-config/ProviderConfigForm.test.tsx @@ -44,6 +44,7 @@ jest.mock('app/core/components/FormPrompt/FormPrompt', () => ({ const testConfig: SSOProvider = { id: '300f9b7c-0488-40db-9763-a22ce8bf6b3e', provider: 'github', + source: 'database', settings: { ...emptySettings, name: 'GitHub', @@ -101,7 +102,8 @@ describe('ProviderConfigForm', () => { expect(putMock).toHaveBeenCalledWith( '/api/v1/sso-settings/github', { - ...testConfig, + id: '300f9b7c-0488-40db-9763-a22ce8bf6b3e', + provider: 'github', settings: { name: 'GitHub', allowedOrganizations: 'test-org1,test-org2', diff --git a/public/app/features/auth-config/ProviderConfigForm.tsx b/public/app/features/auth-config/ProviderConfigForm.tsx index 184bbce65d9..17e0f24a6d5 100644 --- a/public/app/features/auth-config/ProviderConfigForm.tsx +++ b/public/app/features/auth-config/ProviderConfigForm.tsx @@ -82,7 +82,6 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf payload: [message], }); setSubmitError(true); - } finally { setIsSaving(false); } }; @@ -182,7 +181,9 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf )} - + @@ -192,6 +193,7 @@ export const ProviderConfigForm = ({ config, provider, isLoading }: ProviderConf