Alerting: Support for active time intervals in notification policies (#104252)

* add active_time_intervals to route model

* update k8s compat layer

* update notification policies service to validate active time intervals

* update integration tests

* update openapi

* add active time interval to model

* update route generator to include active time interval

* Update storage list and rename methods to handle active intervals

* update api model

* update provisioning and export models

* update ui to allow active timing config

* update i18n

* fix snapshots for ui tests

* run prettier

* Alerting: Active time intervals UI naming (#104402)

* update naming in UI

* update naming in the edit page title

* update translations

* update alerting module

---------

Signed-off-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
Co-authored-by: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com>
Co-authored-by: Sonia Aguilar <soniaaguilarpeiron@gmail.com>
pull/104603/head^2
Yuri Tseretyan 2 weeks ago committed by GitHub
parent a8dee54aa4
commit 3e2296acd3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      apps/alerting/notifications/kinds/v0alpha1/routingtree_spec.cue
  2. 19
      apps/alerting/notifications/pkg/apis/routingtree/v0alpha1/routingtree_spec_gen.go
  3. 14
      apps/alerting/notifications/pkg/apis/routingtree/v0alpha1/zz_generated.openapi.go
  4. 2
      apps/alerting/notifications/pkg/apis/routingtree/v0alpha1/zz_generated.openapi_violation_exceptions.list
  5. 2
      go.mod
  6. 4
      go.sum
  7. 26
      pkg/registry/apps/alerting/notifications/routingtree/conversions.go
  8. 7
      pkg/services/ngalert/api/api_provisioning.go
  9. 55
      pkg/services/ngalert/api/api_provisioning_test.go
  10. 40
      pkg/services/ngalert/api/compat/compat.go
  11. 2
      pkg/services/ngalert/api/test-data/post-rulegroup-101-export.hcl
  12. 6
      pkg/services/ngalert/api/test-data/post-rulegroup-101-export.json
  13. 4
      pkg/services/ngalert/api/test-data/post-rulegroup-101-export.yaml
  14. 6
      pkg/services/ngalert/api/test-data/post-rulegroup-101.json
  15. 8
      pkg/services/ngalert/api/tooling/definitions/cortex-ruler.go
  16. 15
      pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go
  17. 1
      pkg/services/ngalert/api/tooling/definitions/provisioning_policies.go
  18. 16
      pkg/services/ngalert/api/validation/api_ruler_validation.go
  19. 16
      pkg/services/ngalert/models/alert_rule_test.go
  20. 19
      pkg/services/ngalert/models/notifications.go
  21. 17
      pkg/services/ngalert/models/notifications_test.go
  22. 23
      pkg/services/ngalert/models/testing.go
  23. 15
      pkg/services/ngalert/notifier/autogen_alertmanager.go
  24. 37
      pkg/services/ngalert/notifier/autogen_alertmanager_test.go
  25. 5
      pkg/services/ngalert/notifier/validation.go
  26. 2
      pkg/services/ngalert/provisioning/notification_policies.go
  27. 15
      pkg/services/ngalert/provisioning/notification_policies_test.go
  28. 9
      pkg/services/ngalert/store/alert_rule.go
  29. 15
      pkg/services/ngalert/store/alert_rule_test.go
  30. 36
      pkg/services/provisioning/alerting/rules_types.go
  31. 26
      pkg/services/provisioning/alerting/rules_types_test.go
  32. 33
      pkg/tests/api/alerting/api_ruler_test.go
  33. 30
      pkg/tests/apis/alerting/notifications/routingtree/routing_tree_test.go
  34. 7
      pkg/tests/apis/openapi_snapshots/notifications.alerting.grafana.app-v0alpha1.json
  35. 12
      public/app/features/alerting/unified/MuteTimings.test.tsx
  36. 22
      public/app/features/alerting/unified/NotificationPoliciesPage.tsx
  37. 2
      public/app/features/alerting/unified/components/Provisioning.tsx
  38. 16
      public/app/features/alerting/unified/components/alertmanager-entities/MuteTimingsSelector.tsx
  39. 2
      public/app/features/alerting/unified/components/mute-timings/EditMuteTiming.tsx
  40. 6
      public/app/features/alerting/unified/components/mute-timings/MuteTimingActionsButtons.tsx
  41. 19
      public/app/features/alerting/unified/components/mute-timings/MuteTimingForm.tsx
  42. 12
      public/app/features/alerting/unified/components/mute-timings/MuteTimingTimeInterval.tsx
  43. 12
      public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.test.tsx
  44. 35
      public/app/features/alerting/unified/components/mute-timings/MuteTimingsTable.tsx
  45. 2
      public/app/features/alerting/unified/components/mute-timings/NewMuteTiming.tsx
  46. 26
      public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx
  47. 2
      public/app/features/alerting/unified/components/notification-policies/Policy.tsx
  48. 2
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/AlertManagerRouting.tsx
  49. 1
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/SimplifiedRouting.tsx
  50. 51
      public/app/features/alerting/unified/components/rule-editor/alert-rule-form/simplifiedRouting/route-settings/ActiveTimingFields.tsx
  51. 98
      public/app/features/alerting/unified/hooks/__snapshots__/useAbilities.test.tsx.snap
  52. 24
      public/app/features/alerting/unified/hooks/useAbilities.ts
  53. 2
      public/app/features/alerting/unified/reducers/alertmanager/__snapshots__/notificationPolicyRoutes.test.ts.snap
  54. 1
      public/app/features/alerting/unified/types/amroutes.ts
  55. 1
      public/app/features/alerting/unified/types/rule-form.ts
  56. 3
      public/app/features/alerting/unified/utils/__snapshots__/routeTree.test.ts.snap
  57. 3
      public/app/features/alerting/unified/utils/amroutes.ts
  58. 5
      public/app/features/alerting/unified/utils/rule-form.test.ts
  59. 2
      public/app/features/alerting/unified/utils/rule-form.ts
  60. 1
      public/app/types/unified-alerting-dto.ts
  61. 66
      public/locales/en-US/grafana.json

@ -26,6 +26,7 @@ RouteTreeSpec: {
group_by?: [...string]
mute_time_intervals?: [...string]
active_time_intervals?: [...string]
routes?: [...#Route]
group_wait?: string
group_interval?: string

@ -18,15 +18,16 @@ func NewRouteDefaults() *RouteDefaults {
// +k8s:openapi-gen=true
type Route struct {
Receiver *string `json:"receiver,omitempty"`
Matchers []Matcher `json:"matchers,omitempty"`
Continue bool `json:"continue"`
GroupBy []string `json:"group_by,omitempty"`
MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"`
Routes []Route `json:"routes,omitempty"`
GroupWait *string `json:"group_wait,omitempty"`
GroupInterval *string `json:"group_interval,omitempty"`
RepeatInterval *string `json:"repeat_interval,omitempty"`
Receiver *string `json:"receiver,omitempty"`
Matchers []Matcher `json:"matchers,omitempty"`
Continue bool `json:"continue"`
GroupBy []string `json:"group_by,omitempty"`
MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"`
ActiveTimeIntervals []string `json:"active_time_intervals,omitempty"`
Routes []Route `json:"routes,omitempty"`
GroupWait *string `json:"group_wait,omitempty"`
GroupInterval *string `json:"group_interval,omitempty"`
RepeatInterval *string `json:"repeat_interval,omitempty"`
}
// NewRoute creates a new Route object.

@ -119,6 +119,20 @@ func schema_pkg_apis_routingtree_v0alpha1_Route(ref common.ReferenceCallback) co
},
},
},
"active_time_intervals": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},
Items: &spec.SchemaOrArray{
Schema: &spec.Schema{
SchemaProps: spec.SchemaProps{
Default: "",
Type: []string{"string"},
Format: "",
},
},
},
},
},
"routes": {
SchemaProps: spec.SchemaProps{
Type: []string{"array"},

@ -1,9 +1,11 @@
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,ActiveTimeIntervals
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,GroupBy
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,Matchers
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,MuteTimeIntervals
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,Routes
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,RouteDefaults,GroupBy
API rule violation: list_type_missing,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Spec,Routes
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,ActiveTimeIntervals
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,GroupBy
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,GroupInterval
API rule violation: names_match,github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/routingtree/v0alpha1,Route,GroupWait

@ -77,7 +77,7 @@ require (
github.com/googleapis/go-sql-spanner v1.11.1 // @grafana/grafana-search-and-storage
github.com/gorilla/mux v1.8.1 // @grafana/grafana-backend-group
github.com/gorilla/websocket v1.5.3 // @grafana/grafana-app-platform-squad
github.com/grafana/alerting v0.0.0-20250425150043-be11a2ae18bb // @grafana/alerting-backend
github.com/grafana/alerting v0.0.0-20250429131604-de176b4a0309 // @grafana/alerting-backend
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a // @grafana/identity-access-team
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d // @grafana/identity-access-team
github.com/grafana/dataplane/examples v0.0.1 // @grafana/observability-metrics

@ -1565,8 +1565,8 @@ github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7Fsg
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/alerting v0.0.0-20250425150043-be11a2ae18bb h1:Nkqatz7R7K/2YaK+7pw8CAYgylKfksPVeT7gMARKjwI=
github.com/grafana/alerting v0.0.0-20250425150043-be11a2ae18bb/go.mod h1:pMfhRxL2LZ3Pm8iy7VcVsb9CLYuBtjFYbf1oxgx7yFA=
github.com/grafana/alerting v0.0.0-20250429131604-de176b4a0309 h1:H2p3XKDHnTBGkMXLCgXiqb2dFnHbQ4zPDXOwKK4Ne3Y=
github.com/grafana/alerting v0.0.0-20250429131604-de176b4a0309/go.mod h1:pMfhRxL2LZ3Pm8iy7VcVsb9CLYuBtjFYbf1oxgx7yFA=
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a h1:irEH0Abl6mKbkPx/xtmB5Wai4ipzEB6hGPNsLya/p9Y=
github.com/grafana/authlib v0.0.0-20250422131730-e8482efe6b8a/go.mod h1:PBtQaXwkFu4BAt2aXsR7w8p8NVpdjV5aJYhqRDei9Us=
github.com/grafana/authlib/types v0.0.0-20250325095148-d6da9c164a7d h1:34E6btDAhdDOiSEyrMaYaHwnJpM8w9QKzVQZIBzLNmM=

@ -51,13 +51,14 @@ func ConvertToK8sResource(orgID int64, r definitions.Route, version string, name
func convertRouteToK8sSubRoute(r *definitions.Route) model.Route {
result := model.Route{
GroupBy: r.GroupByStr,
MuteTimeIntervals: r.MuteTimeIntervals,
Continue: r.Continue,
GroupWait: optionalPrometheusDurationToString(r.GroupWait),
GroupInterval: optionalPrometheusDurationToString(r.GroupInterval),
RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval),
Routes: make([]model.Route, 0, len(r.Routes)),
GroupBy: r.GroupByStr,
MuteTimeIntervals: r.MuteTimeIntervals,
ActiveTimeIntervals: r.ActiveTimeIntervals,
Continue: r.Continue,
GroupWait: optionalPrometheusDurationToString(r.GroupWait),
GroupInterval: optionalPrometheusDurationToString(r.GroupInterval),
RepeatInterval: optionalPrometheusDurationToString(r.RepeatInterval),
Routes: make([]model.Route, 0, len(r.Routes)),
}
if r.Receiver != "" {
result.Receiver = util.Pointer(r.Receiver)
@ -152,11 +153,12 @@ func convertToDomainModel(obj *model.RoutingTree) (definitions.Route, string, er
func convertK8sSubRouteToRoute(r model.Route, path string) (definitions.Route, []error) {
result := definitions.Route{
GroupByStr: r.GroupBy,
MuteTimeIntervals: r.MuteTimeIntervals,
Routes: make([]*definitions.Route, 0, len(r.Routes)),
Matchers: make(config.Matchers, 0, len(r.Matchers)),
Continue: r.Continue,
GroupByStr: r.GroupBy,
MuteTimeIntervals: r.MuteTimeIntervals,
ActiveTimeIntervals: r.ActiveTimeIntervals,
Routes: make([]*definitions.Route, 0, len(r.Routes)),
Matchers: make(config.Matchers, 0, len(r.Matchers)),
Continue: r.Continue,
}
if r.Receiver != nil {
result.Receiver = *r.Receiver

@ -630,6 +630,13 @@ func escapeRouteExport(r *definitions.RouteExport) {
}
r.MuteTimeIntervals = &muteTimeIntervals
}
if r.ActiveTimeIntervals != nil {
intervals := make([]string, len(*r.ActiveTimeIntervals))
for i, timeInterval := range *r.ActiveTimeIntervals {
intervals[i] = addEscapeCharactersToString(timeInterval)
}
r.ActiveTimeIntervals = &intervals
}
for i := range r.Routes {
escapeRouteExport(r.Routes[i])
}

@ -872,7 +872,7 @@ func TestProvisioningApi(t *testing.T) {
insertRule(t, sut, rule1)
insertRule(t, sut, createTestAlertRule("rule2", 1))
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n"
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n active_time_intervals:\n - test-active\n"
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
require.Equal(t, 200, response.Status())
@ -888,7 +888,7 @@ func TestProvisioningApi(t *testing.T) {
insertRule(t, sut, createTestAlertRule("rule2", 1))
rc.Req.Header.Add("Accept", "application/json")
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false},{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]}]}`
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
require.Equal(t, 200, response.Status())
@ -902,7 +902,7 @@ func TestProvisioningApi(t *testing.T) {
insertRule(t, sut, createTestAlertRule("rule2", 1))
rc.Req.Header.Add("Accept", "application/yaml")
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n"
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n active_time_intervals:\n - test-active\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n active_time_intervals:\n - test-active\n"
response := sut.RouteGetAlertRuleGroupExport(&rc, "folder-uid", "my-cool-group")
@ -986,6 +986,7 @@ func TestProvisioningApi(t *testing.T) {
group_interval = "5s"
repeat_interval = "5m"
mute_timings = ["test-mute"]
active_timings = ["test-active"]
}
}
}
@ -1120,7 +1121,7 @@ func TestProvisioningApi(t *testing.T) {
rc := createTestRequestCtx()
insertRule(t, sut, createTestAlertRule("rule1", 1))
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"my-cool-group","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]}]}`
rc.Req.Header.Add("Accept", "application/json")
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
@ -1135,7 +1136,7 @@ func TestProvisioningApi(t *testing.T) {
insertRule(t, sut, createTestAlertRule("rule1", 1))
rc.Req.Header.Add("Accept", "application/yaml")
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n"
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: my-cool-group\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n active_time_intervals:\n - test-active\n"
response := sut.RouteGetAlertRuleExport(&rc, "rule1")
@ -1245,7 +1246,7 @@ func TestProvisioningApi(t *testing.T) {
insertRule(t, sut, rule3)
rc.Req.Header.Add("Accept", "application/json")
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Email"}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Email"}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]}]}`
response := sut.RouteGetAlertRulesExport(&rc)
require.Equal(t, 200, response.Status())
@ -1265,7 +1266,7 @@ func TestProvisioningApi(t *testing.T) {
insertRule(t, sut, rule3)
rc.Req.Header.Add("Accept", "application/yaml")
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Email\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n"
expectedResponse := "apiVersion: 1\ngroups:\n - orgId: 1\n name: groupa\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule1\n title: rule1\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Email\n - orgId: 1\n name: groupb\n folder: Folder Title\n interval: 1m\n rules:\n - uid: rule2\n title: rule2\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n active_time_intervals:\n - test-active\n - orgId: 1\n name: groupb\n folder: Folder Title2\n interval: 1m\n rules:\n - uid: rule3\n title: rule3\n condition: A\n data:\n - refId: A\n relativeTimeRange:\n from: 60\n to: 0\n datasourceUid: \"\"\n model:\n conditions:\n - evaluator:\n params:\n - 3\n type: gt\n operator:\n type: and\n query:\n params:\n - A\n reducer:\n type: last\n type: query\n datasource:\n type: __expr__\n uid: __expr__\n expression: 1==0\n intervalMs: 1000\n maxDataPoints: 43200\n refId: A\n type: math\n noDataState: OK\n execErrState: OK\n for: 0s\n isPaused: false\n notification_settings:\n receiver: Test-Receiver\n group_by:\n - alertname\n - grafana_folder\n - test\n group_wait: 1s\n group_interval: 5s\n repeat_interval: 5m\n mute_time_intervals:\n - test-mute\n active_time_intervals:\n - test-active\n"
response := sut.RouteGetAlertRulesExport(&rc)
require.Equal(t, 200, response.Status())
@ -1281,7 +1282,7 @@ func TestProvisioningApi(t *testing.T) {
rc.Req.Header.Add("Accept", "application/json")
rc.Req.Form.Set("folderUid", "folder-uid")
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]}]}`
response := sut.RouteGetAlertRulesExport(&rc)
require.Equal(t, 200, response.Status())
@ -1298,7 +1299,7 @@ func TestProvisioningApi(t *testing.T) {
rc.Req.Header.Add("Accept", "application/json")
rc.Req.Form.Set("folder_uid", "folder-uid")
rc.Req.Form.Add("folder_uid", "folder-uid2")
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule2","title":"rule2","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]},{"orgId":1,"name":"groupb","folder":"Folder Title2","interval":"1m","rules":[{"uid":"rule3","title":"rule3","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]}]}`
response := sut.RouteGetAlertRulesExport(&rc)
require.Equal(t, 200, response.Status())
@ -1316,7 +1317,7 @@ func TestProvisioningApi(t *testing.T) {
rc.Req.Form.Set("folderUid", "folder-uid")
rc.Req.Form.Set("group", "groupa")
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]}]}`
response := sut.RouteGetAlertRulesExport(&rc)
require.Equal(t, 200, response.Status())
@ -1354,7 +1355,7 @@ func TestProvisioningApi(t *testing.T) {
rc.Req.Header.Add("Accept", "application/json")
rc.Req.Form.Set("ruleUid", "rule1")
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"]}}]}]}`
expectedResponse := `{"apiVersion":1,"groups":[{"orgId":1,"name":"groupa","folder":"Folder Title","interval":"1m","rules":[{"uid":"rule1","title":"rule1","condition":"A","data":[{"refId":"A","relativeTimeRange":{"from":60,"to":0},"datasourceUid":"","model":{"conditions":[{"evaluator":{"params":[3],"type":"gt"},"operator":{"type":"and"},"query":{"params":["A"]},"reducer":{"type":"last"},"type":"query"}],"datasource":{"type":"__expr__","uid":"__expr__"},"expression":"1==0","intervalMs":1000,"maxDataPoints":43200,"refId":"A","type":"math"}}],"noDataState":"OK","execErrState":"OK","for":"0s","isPaused":false,"notification_settings":{"receiver":"Test-Receiver","group_by":["alertname","grafana_folder","test"],"group_wait":"1s","group_interval":"5s","repeat_interval":"5m","mute_time_intervals":["test-mute"],"active_time_intervals":["test-active"]}}]}]}`
response := sut.RouteGetAlertRulesExport(&rc)
@ -1471,7 +1472,7 @@ func TestProvisioningApi(t *testing.T) {
rc := createTestRequestCtx()
rc.Req.Header.Add("Accept", "application/json")
expectedResponse := `{"apiVersion":1,"policies":[{"orgId":1,"receiver":"default-receiver","group_by":["g1","g2"],"routes":[{"receiver":"nested-receiver","group_by":["g3","g4"],"matchers":["a=\"b\""],"object_matchers":[["foo","=","bar"]],"mute_time_intervals":["interval"],"continue":true,"group_wait":"5m","group_interval":"5m","repeat_interval":"5m"}],"group_wait":"30s","group_interval":"5m","repeat_interval":"1h"}]}`
expectedResponse := `{"apiVersion":1,"policies":[{"orgId":1,"receiver":"default-receiver","group_by":["g1","g2"],"routes":[{"receiver":"nested-receiver","group_by":["g3","g4"],"matchers":["a=\"b\""],"object_matchers":[["foo","=","bar"]],"mute_time_intervals":["interval"],"active_time_intervals":["active"],"continue":true,"group_wait":"5m","group_interval":"5m","repeat_interval":"5m"}],"group_wait":"30s","group_interval":"5m","repeat_interval":"1h"}]}`
response := sut.RouteGetPolicyTreeExport(&rc)
@ -1485,7 +1486,7 @@ func TestProvisioningApi(t *testing.T) {
rc := createTestRequestCtx()
rc.Req.Header.Add("Accept", "application/yaml")
expectedResponse := "apiVersion: 1\npolicies:\n - orgId: 1\n receiver: default-receiver\n group_by:\n - g1\n - g2\n routes:\n - receiver: nested-receiver\n group_by:\n - g3\n - g4\n matchers:\n - a=\"b\"\n object_matchers:\n - - foo\n - =\n - bar\n mute_time_intervals:\n - interval\n continue: true\n group_wait: 5m\n group_interval: 5m\n repeat_interval: 5m\n group_wait: 30s\n group_interval: 5m\n repeat_interval: 1h\n"
expectedResponse := "apiVersion: 1\npolicies:\n - orgId: 1\n receiver: default-receiver\n group_by:\n - g1\n - g2\n routes:\n - receiver: nested-receiver\n group_by:\n - g3\n - g4\n matchers:\n - a=\"b\"\n object_matchers:\n - - foo\n - =\n - bar\n mute_time_intervals:\n - interval\n active_time_intervals:\n - active\n continue: true\n group_wait: 5m\n group_interval: 5m\n repeat_interval: 5m\n group_wait: 30s\n group_interval: 5m\n repeat_interval: 1h\n"
response := sut.RouteGetPolicyTreeExport(&rc)
@ -1499,7 +1500,7 @@ func TestProvisioningApi(t *testing.T) {
rc := createTestRequestCtx()
rc.Req.Form.Add("format", "hcl")
expectedResponse := "resource \"grafana_notification_policy\" \"notification_policy_1\" {\n contact_point = \"default-receiver\"\n group_by = [\"g1\", \"g2\"]\n\n policy {\n contact_point = \"nested-receiver\"\n group_by = [\"g3\", \"g4\"]\n\n matcher {\n label = \"foo\"\n match = \"=\"\n value = \"bar\"\n }\n\n mute_timings = [\"interval\"]\n continue = true\n group_wait = \"5m\"\n group_interval = \"5m\"\n repeat_interval = \"5m\"\n }\n\n group_wait = \"30s\"\n group_interval = \"5m\"\n repeat_interval = \"1h\"\n}\n"
expectedResponse := "resource \"grafana_notification_policy\" \"notification_policy_1\" {\n contact_point = \"default-receiver\"\n group_by = [\"g1\", \"g2\"]\n\n policy {\n contact_point = \"nested-receiver\"\n group_by = [\"g3\", \"g4\"]\n\n matcher {\n label = \"foo\"\n match = \"=\"\n value = \"bar\"\n }\n\n mute_timings = [\"interval\"]\n active_timings = [\"active\"]\n continue = true\n group_wait = \"5m\"\n group_interval = \"5m\"\n repeat_interval = \"5m\"\n }\n\n group_wait = \"30s\"\n group_interval = \"5m\"\n repeat_interval = \"1h\"\n}\n"
response := sut.RouteGetPolicyTreeExport(&rc)
@ -2152,12 +2153,13 @@ func createFakeNotificationPolicyService() *fakeNotificationPolicyService {
Value: "b",
},
},
ObjectMatchers: definitions.ObjectMatchers{{Type: 0, Name: "foo", Value: "bar"}},
MuteTimeIntervals: []string{"interval"},
Continue: true,
GroupWait: &minutes,
GroupInterval: &minutes,
RepeatInterval: &minutes,
ObjectMatchers: definitions.ObjectMatchers{{Type: 0, Name: "foo", Value: "bar"}},
MuteTimeIntervals: []string{"interval"},
ActiveTimeIntervals: []string{"active"},
Continue: true,
GroupWait: &minutes,
GroupInterval: &minutes,
RepeatInterval: &minutes,
}},
},
prov: models.ProvenanceAPI,
@ -2297,12 +2299,13 @@ func createTestAlertRule(title string, orgID int64) definitions.ProvisionedAlert
NoDataState: definitions.OK,
ExecErrState: definitions.OkErrState,
NotificationSettings: &definitions.AlertRuleNotificationSettings{
Receiver: "Test-Receiver",
GroupBy: []string{"alertname", "grafana_folder", "test"},
GroupWait: util.Pointer(model.Duration(1 * time.Second)),
GroupInterval: util.Pointer(model.Duration(5 * time.Second)),
RepeatInterval: util.Pointer(model.Duration(5 * time.Minute)),
MuteTimeIntervals: []string{"test-mute"},
Receiver: "Test-Receiver",
GroupBy: []string{"alertname", "grafana_folder", "test"},
GroupWait: util.Pointer(model.Duration(1 * time.Second)),
GroupInterval: util.Pointer(model.Duration(5 * time.Second)),
RepeatInterval: util.Pointer(model.Duration(5 * time.Minute)),
MuteTimeIntervals: []string{"test-mute"},
ActiveTimeIntervals: []string{"test-active"},
},
}
}

@ -367,6 +367,7 @@ func RouteExportFromRoute(route *definitions.Route) *definitions.RouteExport {
ObjectMatchers: route.ObjectMatchers,
ObjectMatchersSlice: matchers,
MuteTimeIntervals: NilIfEmpty(util.Pointer(route.MuteTimeIntervals)),
ActiveTimeIntervals: NilIfEmpty(util.Pointer(route.ActiveTimeIntervals)),
Continue: OmitDefault(util.Pointer(route.Continue)),
GroupWait: toStringIfNotNil(route.GroupWait),
GroupInterval: toStringIfNotNil(route.GroupInterval),
@ -455,12 +456,13 @@ func AlertRuleNotificationSettingsFromNotificationSettings(ns []models.Notificat
}
m := ns[0]
return &definitions.AlertRuleNotificationSettings{
Receiver: m.Receiver,
GroupBy: m.GroupBy,
GroupWait: m.GroupWait,
GroupInterval: m.GroupInterval,
RepeatInterval: m.RepeatInterval,
MuteTimeIntervals: m.MuteTimeIntervals,
Receiver: m.Receiver,
GroupBy: m.GroupBy,
GroupWait: m.GroupWait,
GroupInterval: m.GroupInterval,
RepeatInterval: m.RepeatInterval,
MuteTimeIntervals: m.MuteTimeIntervals,
ActiveTimeIntervals: m.ActiveTimeIntervals,
}
}
@ -480,12 +482,13 @@ func AlertRuleNotificationSettingsExportFromNotificationSettings(ns []models.Not
}
return &definitions.AlertRuleNotificationSettingsExport{
Receiver: m.Receiver,
GroupBy: m.GroupBy,
GroupWait: toStringIfNotNil(m.GroupWait),
GroupInterval: toStringIfNotNil(m.GroupInterval),
RepeatInterval: toStringIfNotNil(m.RepeatInterval),
MuteTimeIntervals: m.MuteTimeIntervals,
Receiver: m.Receiver,
GroupBy: m.GroupBy,
GroupWait: toStringIfNotNil(m.GroupWait),
GroupInterval: toStringIfNotNil(m.GroupInterval),
RepeatInterval: toStringIfNotNil(m.RepeatInterval),
MuteTimeIntervals: m.MuteTimeIntervals,
ActiveTimeIntervals: m.ActiveTimeIntervals,
}
}
@ -496,12 +499,13 @@ func NotificationSettingsFromAlertRuleNotificationSettings(ns *definitions.Alert
}
return []models.NotificationSettings{
{
Receiver: ns.Receiver,
GroupBy: ns.GroupBy,
GroupWait: ns.GroupWait,
GroupInterval: ns.GroupInterval,
RepeatInterval: ns.RepeatInterval,
MuteTimeIntervals: ns.MuteTimeIntervals,
Receiver: ns.Receiver,
GroupBy: ns.GroupBy,
GroupWait: ns.GroupWait,
GroupInterval: ns.GroupInterval,
RepeatInterval: ns.RepeatInterval,
MuteTimeIntervals: ns.MuteTimeIntervals,
ActiveTimeIntervals: ns.ActiveTimeIntervals,
},
}
}

@ -85,6 +85,7 @@ resource "grafana_rule_group" "rule_group_d3e8424bfbf66bc3" {
group_interval = "5s"
repeat_interval = "5m"
mute_timings = ["test-mute"]
active_timings = ["test-mute"]
}
}
rule {
@ -125,6 +126,7 @@ resource "grafana_rule_group" "rule_group_d3e8424bfbf66bc3" {
group_interval = "5s"
repeat_interval = "5m"
mute_timings = ["test-mute"]
active_timings = ["test-mute"]
}
}
rule {

@ -115,7 +115,8 @@
"group_wait":"1s",
"group_interval":"5s",
"repeat_interval":"5m",
"mute_time_intervals":["test-mute"]
"mute_time_intervals":["test-mute"],
"active_time_intervals":["test-mute"]
}
},
{
@ -169,7 +170,8 @@
"group_wait":"1s",
"group_interval":"5s",
"repeat_interval":"5m",
"mute_time_intervals":["test-mute"]
"mute_time_intervals":["test-mute"],
"active_time_intervals":["test-mute"]
}
},
{

@ -93,6 +93,8 @@ groups:
repeat_interval: 5m
mute_time_intervals:
- test-mute
active_time_intervals:
- test-mute
- uid: alert-with-uid
title: alert with uid
condition: B
@ -139,6 +141,8 @@ groups:
repeat_interval: 5m
mute_time_intervals:
- test-mute
active_time_intervals:
- test-mute
- title: recording rule
data:
- refId: query

@ -116,7 +116,8 @@
"group_wait":"1s",
"group_interval":"5s",
"repeat_interval":"5m",
"mute_time_intervals":["test-mute"]
"mute_time_intervals":["test-mute"],
"active_time_intervals":["test-mute"]
}
}
},
@ -171,7 +172,8 @@
"group_wait":"1s",
"group_interval":"5s",
"repeat_interval":"5m",
"mute_time_intervals":["test-mute"]
"mute_time_intervals":["test-mute"],
"active_time_intervals":["test-mute"]
}
}
},

@ -535,10 +535,16 @@ type AlertRuleNotificationSettings struct {
RepeatInterval *model.Duration `json:"repeat_interval,omitempty"`
// Override the times when notifications should be muted. These must match the name of a mute time interval defined
// in the alertmanager configuration mute_time_intervals section. When muted it will not send any notifications, but
// in the alertmanager configuration time_intervals section. When muted it will not send any notifications, but
// otherwise acts normally.
// example: ["maintenance"]
MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"`
// Override the times when notifications should not be muted. These must match the name of a mute time interval defined
// in the alertmanager configuration time_intervals section. All notifications will be suppressed unless they are sent
// at the time that matches any interval.
// example: ["maintenance"]
ActiveTimeIntervals []string `json:"active_time_intervals,omitempty"`
}
// swagger:model

@ -171,7 +171,7 @@ type ProvisionedAlertRule struct {
IsPaused bool `json:"isPaused"`
// example: {"receiver":"email","group_by":["alertname","grafana_folder","cluster"],"group_wait":"30s","group_interval":"1m","repeat_interval":"4d","mute_time_intervals":["Weekends","Holidays"]}
NotificationSettings *AlertRuleNotificationSettings `json:"notification_settings"`
//example: {"metric":"grafana_alerts_ratio", "from":"A"}
// example: {"metric":"grafana_alerts_ratio", "from":"A"}
Record *Record `json:"record"`
// example: 2
MissingSeriesEvalsToResolve *int `json:"missingSeriesEvalsToResolve,omitempty"`
@ -306,12 +306,13 @@ type RelativeTimeRangeExport struct {
type AlertRuleNotificationSettingsExport struct {
// Field name mismatches with Terraform provider schema are noted where applicable.
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` // TF -> `contact_point`
GroupBy []string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"`
GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"`
GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"`
RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"`
MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"` // TF -> `mute_timings`
Receiver string `yaml:"receiver,omitempty" json:"receiver,omitempty" hcl:"contact_point"` // TF -> `contact_point`
GroupBy []string `yaml:"group_by,omitempty" json:"group_by,omitempty" hcl:"group_by"`
GroupWait *string `yaml:"group_wait,omitempty" json:"group_wait,omitempty" hcl:"group_wait,optional"`
GroupInterval *string `yaml:"group_interval,omitempty" json:"group_interval,omitempty" hcl:"group_interval,optional"`
RepeatInterval *string `yaml:"repeat_interval,omitempty" json:"repeat_interval,omitempty" hcl:"repeat_interval,optional"`
MuteTimeIntervals []string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"` // TF -> `mute_timings`
ActiveTimeIntervals []string `yaml:"active_time_intervals,omitempty" json:"active_time_intervals,omitempty" hcl:"active_timings"` // TF -> `active_timings`
}
// Record is the provisioned export of models.Record.

@ -81,6 +81,7 @@ type RouteExport struct {
ObjectMatchers ObjectMatchers `yaml:"object_matchers,omitempty" json:"object_matchers,omitempty"`
ObjectMatchersSlice []*MatcherExport `yaml:"-" json:"-" hcl:"matcher,block"`
MuteTimeIntervals *[]string `yaml:"mute_time_intervals,omitempty" json:"mute_time_intervals,omitempty" hcl:"mute_timings"`
ActiveTimeIntervals *[]string `yaml:"active_time_intervals,omitempty" json:"active_time_intervals,omitempty" hcl:"active_timings"`
Continue *bool `yaml:"continue,omitempty" json:"continue,omitempty" hcl:"continue,optional"` // Added omitempty to yaml for a cleaner export.
Routes []*RouteExport `yaml:"routes,omitempty" json:"routes,omitempty" hcl:"policy,block"`

@ -7,6 +7,8 @@ import (
"strings"
"time"
prommodels "github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
. "github.com/grafana/grafana/pkg/services/ngalert/api/compat"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
@ -14,7 +16,6 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
prommodels "github.com/prometheus/common/model"
)
type RuleLimits struct {
@ -392,12 +393,13 @@ func ValidateRuleGroup(
func validateNotificationSettings(n *apimodels.AlertRuleNotificationSettings) ([]ngmodels.NotificationSettings, error) {
s := ngmodels.NotificationSettings{
Receiver: n.Receiver,
GroupBy: n.GroupBy,
GroupWait: n.GroupWait,
GroupInterval: n.GroupInterval,
RepeatInterval: n.RepeatInterval,
MuteTimeIntervals: n.MuteTimeIntervals,
Receiver: n.Receiver,
GroupBy: n.GroupBy,
GroupWait: n.GroupWait,
GroupInterval: n.GroupInterval,
RepeatInterval: n.RepeatInterval,
MuteTimeIntervals: n.MuteTimeIntervals,
ActiveTimeIntervals: n.ActiveTimeIntervals,
}
if err := s.Validate(); err != nil {

@ -850,6 +850,22 @@ func TestDiff(t *testing.T) {
},
},
},
{
name: "should detect changes in ActiveTimeIntervals",
notificationSettings: CopyNotificationSettings(baseSettings, NSMuts.WithActiveTimeIntervals(baseSettings.ActiveTimeIntervals[0]+"-modified", baseSettings.ActiveTimeIntervals[1]+"-modified")),
diffs: []cmputil.Diff{
{
Path: "NotificationSettings[0].ActiveTimeIntervals[0]",
Left: reflect.ValueOf(baseSettings.ActiveTimeIntervals[0]),
Right: reflect.ValueOf(baseSettings.ActiveTimeIntervals[0] + "-modified"),
},
{
Path: "NotificationSettings[0].ActiveTimeIntervals[1]",
Left: reflect.ValueOf(baseSettings.ActiveTimeIntervals[1]),
Right: reflect.ValueOf(baseSettings.ActiveTimeIntervals[1] + "-modified"),
},
},
},
}
for _, tt := range testCases {

@ -28,11 +28,12 @@ type ListNotificationSettingsQuery struct {
type NotificationSettings struct {
Receiver string `json:"receiver"`
GroupBy []string `json:"group_by,omitempty"`
GroupWait *model.Duration `json:"group_wait,omitempty"`
GroupInterval *model.Duration `json:"group_interval,omitempty"`
RepeatInterval *model.Duration `json:"repeat_interval,omitempty"`
MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"`
GroupBy []string `json:"group_by,omitempty"`
GroupWait *model.Duration `json:"group_wait,omitempty"`
GroupInterval *model.Duration `json:"group_interval,omitempty"`
RepeatInterval *model.Duration `json:"repeat_interval,omitempty"`
MuteTimeIntervals []string `json:"mute_time_intervals,omitempty"`
ActiveTimeIntervals []string `json:"active_time_intervals,omitempty"`
}
func (s *NotificationSettings) GetUID() string {
@ -136,6 +137,9 @@ func (s *NotificationSettings) Equals(other *NotificationSettings) bool {
if !slices.Equal(s.MuteTimeIntervals, other.MuteTimeIntervals) {
return false
}
if !slices.Equal(s.ActiveTimeIntervals, other.ActiveTimeIntervals) {
return false
}
sGr := s.GroupBy
oGr := other.GroupBy
return slices.Equal(sGr, oGr)
@ -143,7 +147,7 @@ func (s *NotificationSettings) Equals(other *NotificationSettings) bool {
// IsAllDefault checks if the NotificationSettings object has all default values for optional fields (all except Receiver) .
func (s *NotificationSettings) IsAllDefault() bool {
return len(s.GroupBy) == 0 && s.GroupWait == nil && s.GroupInterval == nil && s.RepeatInterval == nil && len(s.MuteTimeIntervals) == 0
return len(s.GroupBy) == 0 && s.GroupWait == nil && s.GroupInterval == nil && s.RepeatInterval == nil && len(s.MuteTimeIntervals) == 0 && len(s.ActiveTimeIntervals) == 0
}
// NewDefaultNotificationSettings creates a new default NotificationSettings with the specified receiver.
@ -186,5 +190,8 @@ func (s *NotificationSettings) Fingerprint() data.Fingerprint {
for _, interval := range s.MuteTimeIntervals {
writeString(interval)
}
for _, interval := range s.ActiveTimeIntervals {
writeString(interval)
}
return data.Fingerprint(h.Sum64())
}

@ -154,6 +154,23 @@ func TestNotificationSettingsLabels(t *testing.T) {
AutogeneratedRouteSettingsHashLabel: "47164c92f2986a35",
},
},
{
name: "custom notification settings with active time interval",
notificationSettings: NotificationSettings{
Receiver: "receiver name",
GroupBy: []string{"label1", "label2"},
GroupWait: util.Pointer(model.Duration(1 * time.Minute)),
GroupInterval: util.Pointer(model.Duration(2 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)),
MuteTimeIntervals: []string{"maintenance1", "maintenance2"},
ActiveTimeIntervals: []string{"active1", "active2"},
},
labels: data.Labels{
AutogeneratedRouteLabel: "true",
AutogeneratedRouteReceiverNameLabel: "receiver name",
AutogeneratedRouteSettingsHashLabel: "a173df6210e43af0",
},
},
}
for _, tt := range testCases {

@ -925,6 +925,10 @@ func CopyNotificationSettings(ns NotificationSettings, mutators ...Mutator[Notif
c.MuteTimeIntervals = make([]string, len(ns.MuteTimeIntervals))
copy(c.MuteTimeIntervals, ns.MuteTimeIntervals)
}
if ns.ActiveTimeIntervals != nil {
c.ActiveTimeIntervals = make([]string, len(ns.ActiveTimeIntervals))
copy(c.ActiveTimeIntervals, ns.ActiveTimeIntervals)
}
for _, mutator := range mutators {
mutator(&c)
}
@ -935,12 +939,13 @@ func CopyNotificationSettings(ns NotificationSettings, mutators ...Mutator[Notif
func NotificationSettingsGen(mutators ...Mutator[NotificationSettings]) func() NotificationSettings {
return func() NotificationSettings {
c := NotificationSettings{
Receiver: util.GenerateShortUID(),
GroupBy: []string{model.AlertNameLabel, FolderTitleLabel, util.GenerateShortUID()},
GroupWait: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
GroupInterval: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
RepeatInterval: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
MuteTimeIntervals: []string{util.GenerateShortUID(), util.GenerateShortUID()},
Receiver: util.GenerateShortUID(),
GroupBy: []string{model.AlertNameLabel, FolderTitleLabel, util.GenerateShortUID()},
GroupWait: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
GroupInterval: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
RepeatInterval: util.Pointer(model.Duration(time.Duration(rand.Intn(100)+1) * time.Second)),
MuteTimeIntervals: []string{util.GenerateShortUID(), util.GenerateShortUID()},
ActiveTimeIntervals: []string{util.GenerateShortUID(), util.GenerateShortUID()},
}
for _, mutator := range mutators {
mutator(&c)
@ -1002,6 +1007,12 @@ func (n NotificationSettingsMutators) WithMuteTimeIntervals(muteTimeIntervals ..
}
}
func (n NotificationSettingsMutators) WithActiveTimeIntervals(activeTimeIntervals ...string) Mutator[NotificationSettings] {
return func(ns *NotificationSettings) {
ns.ActiveTimeIntervals = activeTimeIntervals
}
}
// Silences
// CopySilenceWith creates a deep copy of Silence and then applies mutators to it.

@ -146,13 +146,14 @@ func generateRouteFromSettings(defaultReceiver string, settings map[data.Fingerp
ObjectMatchers: definitions.ObjectMatchers{settingMatcher},
Continue: false, // Only a single setting-specific route should match.
GroupByStr: normalized,
GroupBy: groupBy,
GroupByAll: groupByAll,
MuteTimeIntervals: s.MuteTimeIntervals,
GroupWait: s.GroupWait,
GroupInterval: s.GroupInterval,
RepeatInterval: s.RepeatInterval,
GroupByStr: normalized,
GroupBy: groupBy,
GroupByAll: groupByAll,
MuteTimeIntervals: s.MuteTimeIntervals,
ActiveTimeIntervals: s.ActiveTimeIntervals,
GroupWait: s.GroupWait,
GroupInterval: s.GroupInterval,
RepeatInterval: s.RepeatInterval,
})
}

@ -108,20 +108,22 @@ func TestAddAutogenConfig(t *testing.T) {
},
{
name: "settings with custom options, add option-specific routes",
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3", "receiver4", "receiver5"}, []string{"maintenance"}),
existingConfig: configGen([]string{"receiver1", "receiver2", "receiver3", "receiver4", "receiver5"}, []string{"maintenance", "active"}),
storeSettings: []models.NotificationSettings{
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver1"), models.NSMuts.WithGroupInterval(util.Pointer(1*time.Minute))),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver2"), models.NSMuts.WithGroupWait(util.Pointer(2*time.Minute))),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver3"), models.NSMuts.WithRepeatInterval(util.Pointer(3*time.Minute))),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver4"), models.NSMuts.WithGroupBy(model.AlertNameLabel, models.FolderTitleLabel, "custom")),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver5"), models.NSMuts.WithMuteTimeIntervals("maintenance")),
models.CopyNotificationSettings(models.NewDefaultNotificationSettings("receiver5"), models.NSMuts.WithActiveTimeIntervals("active")),
{
Receiver: "receiver1",
GroupBy: []string{model.AlertNameLabel, models.FolderTitleLabel, "custom"},
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)),
MuteTimeIntervals: []string{"maintenance"},
Receiver: "receiver1",
GroupBy: []string{model.AlertNameLabel, models.FolderTitleLabel, "custom"},
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)),
MuteTimeIntervals: []string{"maintenance"},
ActiveTimeIntervals: []string{"active"},
},
},
expRoute: withChildRoutes(rootRoute(), &definitions.Route{
@ -132,19 +134,24 @@ func TestAddAutogenConfig(t *testing.T) {
Receiver: "receiver5",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "030d6474aec0b553"),
MuteTimeIntervals: []string{"maintenance"},
}, &definitions.Route{
Receiver: "receiver5",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "cd6cd2089632453c"),
ActiveTimeIntervals: []string{"active"},
}),
withChildRoutes(basicContactRoute("receiver1"), &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "4f095749ddf3eeeb"),
GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel, "custom"},
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)),
MuteTimeIntervals: []string{"maintenance"},
}, &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "dde34b8127e68f31"),
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
}, &definitions.Route{
Receiver: "receiver1",
ObjectMatchers: matcher(models.AutogeneratedRouteSettingsHashLabel, "f134b8faf7db083c"),
GroupByStr: []string{models.FolderTitleLabel, model.AlertNameLabel, "custom"},
GroupInterval: util.Pointer(model.Duration(1 * time.Minute)),
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Minute)),
MuteTimeIntervals: []string{"maintenance"},
ActiveTimeIntervals: []string{"active"},
}),
withChildRoutes(basicContactRoute("receiver2"), &definitions.Route{
Receiver: "receiver2",

@ -91,6 +91,11 @@ func (n staticValidator) Validate(settings models.NotificationSettings) error {
errs = append(errs, ErrorTimeIntervalDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: interval}})
}
}
for _, interval := range settings.ActiveTimeIntervals {
if _, ok := n.availableTimeIntervals[interval]; !ok {
errs = append(errs, ErrorTimeIntervalDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: interval}})
}
}
return errors.Join(errs...)
}

@ -104,7 +104,7 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI
for _, mt := range revision.Config.AlertmanagerConfig.TimeIntervals {
timeIntervals[mt.Name] = struct{}{}
}
err = tree.ValidateMuteTimes(timeIntervals)
err = tree.ValidateTimeIntervals(timeIntervals)
if err != nil {
return definitions.Route{}, "", MakeErrRouteInvalidFormat(err)
}

@ -92,6 +92,21 @@ func TestUpdatePolicyTree(t *testing.T) {
require.ErrorIs(t, err, ErrRouteInvalidFormat)
})
t.Run("ErrValidation if referenced active time interval does not exist", func(t *testing.T) {
sut, store, _ := createNotificationPolicyServiceSut()
store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) {
return &rev, nil
}
newRoute := definitions.Route{
Receiver: rev.Config.AlertmanagerConfig.Receivers[0].Name,
ActiveTimeIntervals: []string{
"not-existing",
},
}
_, _, err := sut.UpdatePolicyTree(context.Background(), orgID, newRoute, models.ProvenanceNone, defaultVersion)
require.ErrorIs(t, err, ErrRouteInvalidFormat)
})
t.Run("ErrValidation if root route has no receiver", func(t *testing.T) {
rev := getDefaultConfigRevision()
sut, store, _ := createNotificationPolicyServiceSut()

@ -673,7 +673,7 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
}
if query.TimeIntervalName != "" {
if !slices.ContainsFunc(converted.NotificationSettings, func(settings ngmodels.NotificationSettings) bool {
return slices.Contains(settings.MuteTimeIntervals, query.TimeIntervalName)
return slices.Contains(settings.MuteTimeIntervals, query.TimeIntervalName) || slices.Contains(settings.ActiveTimeIntervals, query.TimeIntervalName)
}) {
continue
}
@ -985,7 +985,7 @@ func (st DBstore) ListNotificationSettings(ctx context.Context, q ngmodels.ListN
if q.ReceiverName != "" && q.ReceiverName != setting.Receiver { // currently, there can be only one setting. If in future there are more, we will return all settings of a rule that has a setting with receiver
continue
}
if q.TimeIntervalName != "" && !slices.Contains(setting.MuteTimeIntervals, q.TimeIntervalName) {
if q.TimeIntervalName != "" && !slices.Contains(setting.MuteTimeIntervals, q.TimeIntervalName) && !slices.Contains(setting.ActiveTimeIntervals, q.TimeIntervalName) {
continue
}
ns = append(ns, setting)
@ -1157,6 +1157,11 @@ func (st DBstore) RenameTimeIntervalInNotificationSettings(
r.NotificationSettings[idx].MuteTimeIntervals[mtIdx] = newTimeInterval
}
}
for mtIdx := range r.NotificationSettings[idx].ActiveTimeIntervals {
if r.NotificationSettings[idx].ActiveTimeIntervals[mtIdx] == oldTimeInterval {
r.NotificationSettings[idx].ActiveTimeIntervals[mtIdx] = newTimeInterval
}
}
}
updates = append(updates, ngmodels.UpdateRule{

@ -907,7 +907,9 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) {
gen = gen.With(gen.WithOrgID(1), gen.WithIntervalMatching(store.Cfg.BaseInterval))
rules := gen.GenerateManyRef(3)
receiveRules := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(receiverName)))).GenerateManyRef(3)
timeIntervalRules := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithMuteTimeIntervals(timeIntervalName)))).GenerateManyRef(3)
mutetimeIntervalRules := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithMuteTimeIntervals(timeIntervalName)))).GenerateManyRef(3)
activeTimeIntervalRules := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithActiveTimeIntervals(timeIntervalName)))).GenerateManyRef(3)
timeIntervalRules := append(mutetimeIntervalRules, activeTimeIntervalRules...)
noise := gen.With(gen.WithNotificationSettingsGen(models.NotificationSettingsGen(models.NSMuts.WithReceiver(timeIntervalName), models.NSMuts.WithMuteTimeIntervals(receiverName)))).GenerateManyRef(3)
deref := make([]models.AlertRule, 0, len(rules)+len(receiveRules)+len(timeIntervalRules)+len(noise))
@ -959,16 +961,21 @@ func TestIntegrationAlertRulesNotificationSettings(t *testing.T) {
deref[i], deref[j] = deref[j], deref[i]
})
for _, rule := range deref {
if len(rule.NotificationSettings) == 0 || rule.NotificationSettings[0].Receiver == "" || len(rule.NotificationSettings[0].MuteTimeIntervals) == 0 {
if len(rule.NotificationSettings) == 0 || rule.NotificationSettings[0].Receiver == "" || len(rule.NotificationSettings[0].MuteTimeIntervals) == 0 || len(rule.NotificationSettings[0].ActiveTimeIntervals) == 0 {
continue
}
if len(expected) > 0 {
if rule.NotificationSettings[0].Receiver == receiver && slices.Contains(rule.NotificationSettings[0].MuteTimeIntervals, intervalName) {
if rule.NotificationSettings[0].Receiver == receiver && (slices.Contains(rule.NotificationSettings[0].MuteTimeIntervals, intervalName) || slices.Contains(rule.NotificationSettings[0].ActiveTimeIntervals, intervalName)) {
expected = append(expected, rule.GetKey())
}
} else {
receiver = rule.NotificationSettings[0].Receiver
intervalName = rule.NotificationSettings[0].MuteTimeIntervals[0]
if len(rule.NotificationSettings[0].MuteTimeIntervals) > 0 {
intervalName = rule.NotificationSettings[0].MuteTimeIntervals[0]
}
if len(rule.NotificationSettings[0].ActiveTimeIntervals) > 0 {
intervalName = rule.NotificationSettings[0].ActiveTimeIntervals[0]
}
expected = append(expected, rule.GetKey())
}
}

@ -203,12 +203,13 @@ func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) {
}
type NotificationSettingsV1 struct {
Receiver values.StringValue `json:"receiver" yaml:"receiver"`
GroupBy []values.StringValue `json:"group_by,omitempty" yaml:"group_by"`
GroupWait values.StringValue `json:"group_wait,omitempty" yaml:"group_wait"`
GroupInterval values.StringValue `json:"group_interval,omitempty" yaml:"group_interval"`
RepeatInterval values.StringValue `json:"repeat_interval,omitempty" yaml:"repeat_interval"`
MuteTimeIntervals []values.StringValue `json:"mute_time_intervals,omitempty" yaml:"mute_time_intervals"`
Receiver values.StringValue `json:"receiver" yaml:"receiver"`
GroupBy []values.StringValue `json:"group_by,omitempty" yaml:"group_by"`
GroupWait values.StringValue `json:"group_wait,omitempty" yaml:"group_wait"`
GroupInterval values.StringValue `json:"group_interval,omitempty" yaml:"group_interval"`
RepeatInterval values.StringValue `json:"repeat_interval,omitempty" yaml:"repeat_interval"`
MuteTimeIntervals []values.StringValue `json:"mute_time_intervals,omitempty" yaml:"mute_time_intervals"`
ActiveTimeIntervals []values.StringValue `json:"active_time_intervals,omitempty" yaml:"active_time_intervals"`
}
func (nsV1 *NotificationSettingsV1) mapToModel() (models.NotificationSettings, error) {
@ -259,14 +260,25 @@ func (nsV1 *NotificationSettingsV1) mapToModel() (models.NotificationSettings, e
mute = append(mute, value.Value())
}
}
var active []string
if len(nsV1.ActiveTimeIntervals) > 0 {
active = make([]string, 0, len(nsV1.ActiveTimeIntervals))
for _, value := range nsV1.ActiveTimeIntervals {
if value.Value() == "" {
continue
}
active = append(active, value.Value())
}
}
return models.NotificationSettings{
Receiver: nsV1.Receiver.Value(),
GroupBy: groupBy,
GroupWait: gw,
GroupInterval: gi,
RepeatInterval: ri,
MuteTimeIntervals: mute,
Receiver: nsV1.Receiver.Value(),
GroupBy: groupBy,
GroupWait: gw,
GroupInterval: gi,
RepeatInterval: ri,
MuteTimeIntervals: mute,
ActiveTimeIntervals: active,
}, nil
}

@ -212,20 +212,22 @@ func TestNotificationsSettingsV1MapToModel(t *testing.T) {
{
name: "Valid Input",
input: NotificationSettingsV1{
Receiver: stringToStringValue("test-receiver"),
GroupBy: []values.StringValue{stringToStringValue("test-group_by")},
GroupWait: stringToStringValue("1s"),
GroupInterval: stringToStringValue("2s"),
RepeatInterval: stringToStringValue("3s"),
MuteTimeIntervals: []values.StringValue{stringToStringValue("test-mute")},
Receiver: stringToStringValue("test-receiver"),
GroupBy: []values.StringValue{stringToStringValue("test-group_by")},
GroupWait: stringToStringValue("1s"),
GroupInterval: stringToStringValue("2s"),
RepeatInterval: stringToStringValue("3s"),
MuteTimeIntervals: []values.StringValue{stringToStringValue("test-mute")},
ActiveTimeIntervals: []values.StringValue{stringToStringValue("test-active")},
},
expected: models.NotificationSettings{
Receiver: "test-receiver",
GroupBy: []string{"test-group_by"},
GroupWait: util.Pointer(model.Duration(1 * time.Second)),
GroupInterval: util.Pointer(model.Duration(2 * time.Second)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Second)),
MuteTimeIntervals: []string{"test-mute"},
Receiver: "test-receiver",
GroupBy: []string{"test-group_by"},
GroupWait: util.Pointer(model.Duration(1 * time.Second)),
GroupInterval: util.Pointer(model.Duration(2 * time.Second)),
RepeatInterval: util.Pointer(model.Duration(3 * time.Second)),
MuteTimeIntervals: []string{"test-mute"},
ActiveTimeIntervals: []string{"test-active"},
},
},
{

@ -4415,16 +4415,27 @@ func TestIntegrationRuleNotificationSettings(t *testing.T) {
t.Log(body)
})
t.Run("create should fail if mute timing does not exist", func(t *testing.T) {
var copyD testData
err = json.Unmarshal(testDataRaw, &copyD)
group := copyD.RuleGroup
ns := group.Rules[0].GrafanaManagedAlert.NotificationSettings
ns.MuteTimeIntervals = []string{"random-time-interval"}
_, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &group, false)
require.Equalf(t, http.StatusBadRequest, status, body)
t.Log(body)
t.Run("create should fail if time interval does not exist", func(t *testing.T) {
t.Run("mute time interval", func(t *testing.T) {
var copyD testData
err = json.Unmarshal(testDataRaw, &copyD)
group := copyD.RuleGroup
ns := group.Rules[0].GrafanaManagedAlert.NotificationSettings
ns.MuteTimeIntervals = []string{"random-time-interval"}
_, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &group, false)
require.Equalf(t, http.StatusBadRequest, status, body)
})
t.Run("active time interval", func(t *testing.T) {
var copyD testData
err = json.Unmarshal(testDataRaw, &copyD)
group := copyD.RuleGroup
ns := group.Rules[0].GrafanaManagedAlert.NotificationSettings
ns.ActiveTimeIntervals = []string{"random-time-interval"}
_, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &group, false)
require.Equalf(t, http.StatusBadRequest, status, body)
})
})
t.Run("create should not fail if group_by is missing required labels but they should still be used", func(t *testing.T) {
@ -4483,6 +4494,7 @@ func TestIntegrationRuleNotificationSettings(t *testing.T) {
assert.Nil(c, autogenRoute.GroupInterval)
assert.Nil(c, autogenRoute.RepeatInterval)
assert.Empty(c, autogenRoute.MuteTimeIntervals)
assert.Empty(c, autogenRoute.ActiveTimeIntervals)
assert.Empty(c, autogenRoute.GroupBy)
if !canContinue {
return
@ -4511,6 +4523,7 @@ func TestIntegrationRuleNotificationSettings(t *testing.T) {
assert.Nil(c, receiverRoute.GroupInterval)
assert.Nil(c, receiverRoute.RepeatInterval)
assert.Empty(c, receiverRoute.MuteTimeIntervals)
assert.Empty(c, receiverRoute.ActiveTimeIntervals)
var groupBy []string
for _, name := range receiverRoute.GroupBy {
groupBy = append(groupBy, string(name))

@ -538,13 +538,14 @@ func TestIntegrationDataConsistency(t *testing.T) {
ensureMatcher(t, labels.MatchRegexp, "o", "1"),
ensureMatcher(t, labels.MatchNotRegexp, "p", "1"),
},
Receiver: receiver,
GroupByStr: []string{"test-789"},
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
GroupInterval: util.Pointer(model.Duration(5 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(30 * time.Hour)),
MuteTimeIntervals: []string{timeInterval},
Continue: true,
Receiver: receiver,
GroupByStr: []string{"test-789"},
GroupWait: util.Pointer(model.Duration(2 * time.Minute)),
GroupInterval: util.Pointer(model.Duration(5 * time.Minute)),
RepeatInterval: util.Pointer(model.Duration(30 * time.Hour)),
MuteTimeIntervals: []string{timeInterval},
ActiveTimeIntervals: []string{timeInterval},
Continue: true,
},
},
}
@ -562,13 +563,14 @@ func TestIntegrationDataConsistency(t *testing.T) {
}, tree.Spec.Defaults)
assert.Len(t, tree.Spec.Routes, 1)
assert.Equal(t, v0alpha1.Route{
Continue: true,
Receiver: util.Pointer(receiver),
GroupBy: []string{"test-789"},
GroupWait: util.Pointer("2m"),
GroupInterval: util.Pointer("5m"),
RepeatInterval: util.Pointer("1d6h"),
MuteTimeIntervals: []string{timeInterval},
Continue: true,
Receiver: util.Pointer(receiver),
GroupBy: []string{"test-789"},
GroupWait: util.Pointer("2m"),
GroupInterval: util.Pointer("5m"),
RepeatInterval: util.Pointer("1d6h"),
MuteTimeIntervals: []string{timeInterval},
ActiveTimeIntervals: []string{timeInterval},
Matchers: []v0alpha1.Matcher{
{
Label: "m",

@ -3599,6 +3599,13 @@
"continue"
],
"properties": {
"active_time_intervals": {
"type": "array",
"items": {
"type": "string",
"default": ""
}
},
"continue": {
"type": "boolean",
"default": false

@ -145,7 +145,7 @@ const fillOutForm = async ({
const saveMuteTiming = async () => {
const user = userEvent.setup();
await user.click(await screen.findByText(/save mute timing/i));
await user.click(await screen.findByText(/save time interval/i));
};
setupMswServer();
@ -177,7 +177,7 @@ describe('Mute timings', () => {
const capture = captureRequests();
renderMuteTimings({ pathname: '/alerting/routes/new', search: `?alertmanager=${dataSources.am.name}` });
await screen.findByText(/add mute timing/i);
await screen.findByText(/add time interval/i);
await fillOutForm({
name: 'maintenance period',
@ -314,13 +314,13 @@ describe('Mute timings', () => {
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${'does not exist'}`,
});
expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument();
expect(await screen.findByText(/No matching time interval found/i)).toBeInTheDocument();
});
it('allows creation of new mute timings', async () => {
renderMuteTimings('/alerting/routes/new');
await fillOutForm({ name: 'a new mute timing' });
await fillOutForm({ name: 'a new time interval' });
await saveMuteTiming();
await expectToHaveRedirectedToRoutesRoute();
@ -332,7 +332,7 @@ describe('Mute timings', () => {
search: `?alertmanager=${GRAFANA_RULES_SOURCE_NAME}&muteName=${TIME_INTERVAL_NAME_HAPPY_PATH + '_force_breakage'}`,
});
expect(await screen.findByText(/No matching mute timing found/i)).toBeInTheDocument();
expect(await screen.findByText(/No matching time interval found/i)).toBeInTheDocument();
});
it('loads edit form correctly and allows saving', async () => {
@ -351,6 +351,6 @@ describe('Mute timings', () => {
search: `?muteName=${TIME_INTERVAL_NAME_FILE_PROVISIONED}`,
});
expect(await screen.findByText(/This mute timing cannot be edited through the UI/i)).toBeInTheDocument();
expect(await screen.findByText(/This time interval cannot be edited through the UI/i)).toBeInTheDocument();
});
});

@ -11,13 +11,13 @@ import { AlertmanagerAction, useAlertmanagerAbility } from 'app/features/alertin
import { AlertmanagerPageWrapper } from './components/AlertingPageWrapper';
import { GrafanaAlertmanagerDeliveryWarning } from './components/GrafanaAlertmanagerDeliveryWarning';
import { MuteTimingsTable } from './components/mute-timings/MuteTimingsTable';
import { TimeIntervalsTable } from './components/mute-timings/MuteTimingsTable';
import { useAlertmanager } from './state/AlertmanagerContext';
import { withPageErrorBoundary } from './withPageErrorBoundary';
enum ActiveTab {
NotificationPolicies = 'notification_policies',
MuteTimings = 'mute_timings',
TimeIntervals = 'time_intervals',
}
const NotificationPoliciesTabs = () => {
@ -26,10 +26,10 @@ const NotificationPoliciesTabs = () => {
// Alertmanager logic and data hooks
const { selectedAlertmanager = '' } = useAlertmanager();
const [policiesSupported, canSeePoliciesTab] = useAlertmanagerAbility(AlertmanagerAction.ViewNotificationPolicyTree);
const [timingsSupported, canSeeTimingsTab] = useAlertmanagerAbility(AlertmanagerAction.ViewMuteTiming);
const [timingsSupported, canSeeTimingsTab] = useAlertmanagerAbility(AlertmanagerAction.ViewTimeInterval);
const availableTabs = [
canSeePoliciesTab && ActiveTab.NotificationPolicies,
canSeeTimingsTab && ActiveTab.MuteTimings,
canSeeTimingsTab && ActiveTab.TimeIntervals,
].filter((tab) => !!tab);
const { data: muteTimings = [] } = useMuteTimings({
alertmanager: selectedAlertmanager,
@ -41,7 +41,7 @@ const NotificationPoliciesTabs = () => {
const { tab } = getActiveTabFromUrl(queryParams, availableTabs[0]);
const [activeTab, setActiveTab] = useState<ActiveTab>(tab);
const muteTimingsTabActive = activeTab === ActiveTab.MuteTimings;
const muteTimingsTabActive = activeTab === ActiveTab.TimeIntervals;
const policyTreeTabActive = activeTab === ActiveTab.NotificationPolicies;
const numberOfMuteTimings = muteTimings.length;
@ -62,19 +62,19 @@ const NotificationPoliciesTabs = () => {
)}
{timingsSupported && canSeeTimingsTab && (
<Tab
label={t('alerting.notification-policies-tabs.label-mute-timings', 'Mute Timings')}
label={t('alerting.notification-policies-tabs.label-time-intervals', 'Time intervals')}
active={muteTimingsTabActive}
counter={numberOfMuteTimings}
onChangeTab={() => {
setActiveTab(ActiveTab.MuteTimings);
setQueryParams({ tab: ActiveTab.MuteTimings });
setActiveTab(ActiveTab.TimeIntervals);
setQueryParams({ tab: ActiveTab.TimeIntervals });
}}
/>
)}
</TabsBar>
<TabContent className={styles.tabContent}>
{policyTreeTabActive && <NotificationPoliciesList />}
{muteTimingsTabActive && <MuteTimingsTable />}
{muteTimingsTabActive && <TimeIntervalsTable />}
</TabContent>
</>
);
@ -97,8 +97,8 @@ function getActiveTabFromUrl(queryParams: UrlQueryMap, defaultTab: ActiveTab): Q
tab = ActiveTab.NotificationPolicies;
}
if (queryParams.tab === ActiveTab.MuteTimings) {
tab = ActiveTab.MuteTimings;
if (queryParams.tab === ActiveTab.TimeIntervals) {
tab = ActiveTab.TimeIntervals;
}
return {

@ -6,7 +6,7 @@ import { Trans, t } from 'app/core/internationalization';
export enum ProvisionedResource {
ContactPoint = 'contact point',
Template = 'template',
MuteTiming = 'mute timing',
MuteTiming = 'time interval',
AlertRule = 'alert rule',
RootNotificationPolicy = 'root notification policy',
}

@ -6,29 +6,29 @@ import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks'
import { timeIntervalToString } from 'app/features/alerting/unified/utils/alertmanager';
import { MuteTimeInterval } from 'app/plugins/datasource/alertmanager/types';
const mapMuteTiming = ({ name, time_intervals }: MuteTimeInterval): SelectableValue<string> => ({
const mapTimeInterval = ({ name, time_intervals }: MuteTimeInterval): SelectableValue<string> => ({
value: name,
label: name,
description: time_intervals.map((interval) => timeIntervalToString(interval)).join(', AND '),
});
/** Provides a MultiSelect with available mute timings for the given alertmanager */
const MuteTimingsSelector = ({
/** Provides a MultiSelect with available time intervals for the given alertmanager */
const TimeIntervalSelector = ({
alertmanager,
selectProps,
}: BaseAlertmanagerArgs & { selectProps: MultiSelectCommonProps<string> }) => {
const { data } = useMuteTimings({ alertmanager, skip: selectProps.disabled });
const muteTimingOptions = data?.map((value) => mapMuteTiming(value)) || [];
const timeIntervalOptions = data?.map((value) => mapTimeInterval(value)) || [];
return (
<MultiSelect
aria-label={t('alerting.mute-timings-selector.aria-label-mute-timings', 'Mute timings')}
options={muteTimingOptions}
placeholder={t('alerting.mute-timings-selector.placeholder-select-mute-timings', 'Select mute timings...')}
aria-label={t('alerting.time-intervals-selector.aria-label-time-intervals', 'Time intervals')}
options={timeIntervalOptions}
placeholder={t('alerting.time-intervals-selector.placeholder-select-time-intervals', 'Select time intervals...')}
{...selectProps}
/>
);
};
export default MuteTimingsSelector;
export default TimeIntervalSelector;

@ -42,7 +42,7 @@ function EditMuteTimingPage() {
return (
<AlertmanagerPageWrapper
navId="am-routes"
pageNav={{ id: 'alert-policy-edit', text: 'Edit mute timing' }}
pageNav={{ id: 'alert-policy-edit', text: 'Edit time interval' }}
accessType="notification"
>
<EditTimingRoute />

@ -24,7 +24,7 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
});
const [showDeleteDrawer, setShowDeleteDrawer] = useState(false);
const [ExportDrawer, showExportDrawer] = useExportMuteTimingsDrawer();
const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportMuteTimings);
const [exportSupported, exportAllowed] = useAlertmanagerAbility(AlertmanagerAction.ExportTimeIntervals);
const closeDeleteModal = () => setShowDeleteDrawer(false);
@ -55,7 +55,7 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
{!isGrafanaDataSource && isDisabled(muteTiming) && (
<Badge text={t('alerting.mute-timing-actions-buttons.text-disabled', 'Disabled')} color="orange" />
)}
<Authorize actions={[AlertmanagerAction.UpdateMuteTiming]}>{viewOrEditButton}</Authorize>
<Authorize actions={[AlertmanagerAction.UpdateTimeInterval]}>{viewOrEditButton}</Authorize>
{exportSupported && (
<LinkButton
@ -71,7 +71,7 @@ export const MuteTimingActionsButtons = ({ muteTiming, alertManagerSourceName }:
)}
{!muteTiming.provisioned && (
<Authorize actions={[AlertmanagerAction.DeleteMuteTiming]}>
<Authorize actions={[AlertmanagerAction.DeleteTimeInterval]}>
<LinkButton
icon="trash-alt"
variant="secondary"

@ -70,7 +70,7 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
const formApi = useForm({ defaultValues, values: defaultValues });
const updating = formApi.formState.isSubmitting;
const returnLink = makeAMLink('/alerting/routes/', selectedAlertmanager!, { tab: 'mute_timings' });
const returnLink = makeAMLink('/alerting/routes/', selectedAlertmanager!, { tab: 'time_intervals' });
const onSubmit = async (values: MuteTimingFields) => {
const interval = createMuteTiming(values);
@ -88,13 +88,18 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
};
if (loading) {
return <LoadingPlaceholder text={t('alerting.mute-timing-form.text-loading-mute-timing', 'Loading mute timing')} />;
return (
<LoadingPlaceholder text={t('alerting.time-interval-form.text-loading-time-interval', 'Loading time interval')} />
);
}
if (showError) {
return (
<Alert
title={t('alerting.mute-timing-form.title-no-matching-mute-timing-found', 'No matching mute timing found')}
title={t(
'alerting.time-interval-form.title-no-matching-time-interval-found',
'No matching time interval found'
)}
/>
);
}
@ -109,8 +114,8 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
required
label={t('alerting.mute-timing-form.label-name', 'Name')}
description={t(
'alerting.mute-timing-form.description-unique-timing',
'A unique name for the mute timing'
'alerting.time-interval-form.description-unique-time-interval',
'A unique name for the time interval'
)}
invalid={!!formApi.formState.errors?.name}
error={formApi.formState.errors.name?.message}
@ -135,9 +140,9 @@ const MuteTimingForm = ({ muteTiming, showError, loading, provisioned, editMode
icon={updating ? 'spinner' : undefined}
>
{updating ? (
<Trans i18nKey="alerting.mute-timings.saving">Saving mute timing</Trans>
<Trans i18nKey="alerting.time-interval.saving">Saving time interval</Trans>
) : (
<Trans i18nKey="alerting.mute-timings.save">Save mute timing</Trans>
<Trans i18nKey="alerting.time-interval.save">Save time interval</Trans>
)}
</Button>
<LinkButton type="button" variant="secondary" fill="outline" href={returnLink} disabled={updating}>

@ -31,10 +31,10 @@ export const MuteTimingTimeInterval = () => {
<>
<p>
<Trans i18nKey="alerting.mute-timing-time-interval.description">
A time interval is a definition for a moment in time. All fields are lists, and at least one list element
must be satisfied to match the field. If a field is left blank, any moment of time will match the field. For
an instant of time to match a complete time interval, all fields must match. A mute timing can contain
multiple time intervals.
A time interval item is a definition for a moment in time. All fields are lists, and at least one list
element must be satisfied to match the field. If a field is left blank, any moment of time will match the
field. For an instant of time to match a complete time interval, all fields must match. A time interval can
contain multiple time interval items.
</Trans>
</p>
<Stack direction="column" gap={2}>
@ -182,8 +182,8 @@ export const MuteTimingTimeInterval = () => {
}}
icon="plus"
>
<Trans i18nKey="alerting.mute-timing-time-interval.add-another-time-interval">
Add another time interval
<Trans i18nKey="alerting.mute-timing-time-interval.add-another-time-interval-item">
Add another time interval item
</Trans>
</Button>
</>

@ -14,13 +14,13 @@ import { TIME_INTERVAL_UID_HAPPY_PATH } from '../../mocks/server/handlers/k8s/ti
import { AlertmanagerProvider } from '../../state/AlertmanagerContext';
import { GRAFANA_RULES_SOURCE_NAME } from '../../utils/datasource';
import { MuteTimingsTable } from './MuteTimingsTable';
import { TimeIntervalsTable } from './MuteTimingsTable';
import { defaultConfig } from './mocks';
const renderWithProvider = (alertManagerSource = GRAFANA_RULES_SOURCE_NAME) => {
return render(
<AlertmanagerProvider accessType={'notification'} alertmanagerSourceName={alertManagerSource}>
<MuteTimingsTable />
<TimeIntervalsTable />
</AlertmanagerProvider>
);
};
@ -92,14 +92,14 @@ describe('MuteTimingsTable', () => {
expect(await screen.findByTestId('dynamic-table')).toBeInTheDocument();
expect(await screen.findByText('Provisioned')).toBeInTheDocument();
expect(screen.queryByText(/no mute timings configured/i)).not.toBeInTheDocument();
expect(screen.queryByText(/you haven't created any mute timings yet/i)).not.toBeInTheDocument();
expect(screen.queryByText(/no time intervals configured/i)).not.toBeInTheDocument();
expect(screen.queryByText(/you haven't created any time in intervals yet/i)).not.toBeInTheDocument();
});
it('shows error when mute timings cannot load', async () => {
setMuteTimingsListError();
renderWithProvider();
expect(await screen.findByText(/error loading mute timings/i)).toBeInTheDocument();
expect(await screen.findByText(/error loading time intervals/i)).toBeInTheDocument();
});
it('deletes interval', async () => {
@ -123,7 +123,7 @@ describe('MuteTimingsTable', () => {
it('shows empty state when no mute timings are configured', async () => {
setTimeIntervalsListEmpty();
renderWithProvider();
expect(await screen.findByText(/you haven't created any mute timings yet/i)).toBeInTheDocument();
expect(await screen.findByText(/you haven't created any time intervals yet/i)).toBeInTheDocument();
});
});

@ -28,7 +28,7 @@ type TableItem = {
data: MuteTiming;
};
export const MuteTimingsTable = () => {
export const TimeIntervalsTable = () => {
const { selectedAlertmanager: alertManagerSourceName = '', hasConfigurationAPI } = useAlertmanager();
const hideActions = !hasConfigurationAPI;
const styles = useStyles2(getStyles);
@ -47,26 +47,26 @@ export const MuteTimingsTable = () => {
});
}, [data]);
const [_, allowedToCreateMuteTiming] = useAlertmanagerAbility(AlertmanagerAction.CreateMuteTiming);
const [_, allowedToCreateMuteTiming] = useAlertmanagerAbility(AlertmanagerAction.CreateTimeInterval);
const [exportMuteTimingsSupported, exportMuteTimingsAllowed] = useAlertmanagerAbility(
AlertmanagerAction.ExportMuteTimings
AlertmanagerAction.ExportTimeIntervals
);
const columns = useColumns(alertManagerSourceName, hideActions);
if (isLoading) {
return (
<LoadingPlaceholder
text={t('alerting.mute-timings-table.text-loading-mute-timings', 'Loading mute timings...')}
text={t('alerting.time-intervals-table.text-loading-time-intervals', 'Loading time intervals...')}
/>
);
}
if (error) {
return (
<Alert severity="error" title={t('alerting.mute_timings.error-loading.title', 'Error loading mute timings')}>
<Trans i18nKey="alerting.mute_timings.error-loading.description">
Could not load mute timings. Please try again later.
<Alert severity="error" title={t('alerting.time-intervals.error-loading.title', 'Error loading time intervals')}>
<Trans i18nKey="alerting.time-intervals.error-loading.description">
Could not load time intervals. Please try again later.
</Trans>
</Alert>
);
@ -75,20 +75,20 @@ export const MuteTimingsTable = () => {
return (
<div className={styles.container}>
<Stack direction="row" alignItems="center">
<Trans i18nKey="alerting.mute-timings.description">
<Trans i18nKey="alerting.time-intervals.description">
Enter specific time intervals when not to send notifications or freeze notifications for recurring periods of
time.
</Trans>
<Spacer />
{!hideActions && items.length > 0 && (
<Authorize actions={[AlertmanagerAction.CreateMuteTiming]}>
<Authorize actions={[AlertmanagerAction.CreateTimeInterval]}>
<LinkButton
className={styles.muteTimingsButtons}
icon="plus"
variant="primary"
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
>
<Trans i18nKey="alerting.mute-timings.add-mute-timing">Add mute timing</Trans>
<Trans i18nKey="alerting.time-interval.add-time-interval">Add time interval</Trans>
</LinkButton>
</Authorize>
)}
@ -113,10 +113,10 @@ export const MuteTimingsTable = () => {
{!hideActions ? (
<EmptyAreaWithCTA
text={t(
'alerting.mute-timings-table.text-havent-created-timings',
"You haven't created any mute timings yet"
'alerting.time-intervals-table.text-havent-created-time-intervals',
"You haven't created any time intervals yet"
)}
buttonLabel="Add mute timing"
buttonLabel="Add time interval"
buttonIcon="plus"
buttonSize="lg"
href={makeAMLink('alerting/routes/mute-timing/new', alertManagerSourceName)}
@ -124,7 +124,10 @@ export const MuteTimingsTable = () => {
/>
) : (
<EmptyAreaWithCTA
text={t('alerting.mute-timings-table.text-no-mute-timings-configured', 'No mute timings configured')}
text={t(
'alerting.time-intervals-table.text-no-time-intervals-configured',
'No time intervals configured'
)}
buttonLabel={''}
showButton={false}
/>
@ -137,8 +140,8 @@ export const MuteTimingsTable = () => {
function useColumns(alertManagerSourceName: string, hideActions = false) {
const [[_editSupported, allowedToEdit], [_deleteSupported, allowedToDelete]] = useAlertmanagerAbilities([
AlertmanagerAction.UpdateMuteTiming,
AlertmanagerAction.DeleteMuteTiming,
AlertmanagerAction.UpdateTimeInterval,
AlertmanagerAction.DeleteTimeInterval,
]);
const showActions = !hideActions && (allowedToEdit || allowedToDelete);

@ -7,7 +7,7 @@ function NewMuteTimingPage() {
return (
<AlertmanagerPageWrapper
navId="am-routes"
pageNav={{ id: 'alert-policy-new', text: 'Add mute timing' }}
pageNav={{ id: 'alert-policy-new', text: 'Add time interval' }}
accessType="notification"
>
<MuteTimingForm />

@ -52,7 +52,7 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
const styles = useStyles2(getStyles);
const formStyles = useStyles2(getFormStyles);
const { selectedAlertmanager } = useAlertmanager();
const [, canSeeMuteTimings] = useAlertmanagerAbility(AlertmanagerAction.ViewMuteTiming);
const [, canSeeMuteTimings] = useAlertmanagerAbility(AlertmanagerAction.ViewTimeInterval);
const [groupByOptions, setGroupByOptions] = useState(stringsToSelectableValues(route?.group_by));
const emptyMatcher = [{ name: '', operator: MatcherOperator.equal, value: '' }];
@ -309,6 +309,30 @@ export const AmRoutesExpandedForm = ({ actionButtons, route, onSubmit, defaults
name="muteTimeIntervals"
/>
</Field>
<Field
label={t('alerting.am-routes-expanded-form.am-active-timing-select-label-active-timings', 'Active timings')}
data-testid="am-active-timing-select"
description={t(
'alerting.am-routes-expanded-form.am-mute-timing-select-description-add-active-timing-to-policy',
'Add active timing to policy'
)}
invalid={!!errors.activeTimeIntervals}
>
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<MuteTimingsSelector
alertmanager={selectedAlertmanager!}
selectProps={{
...field,
disabled: !canSeeMuteTimings,
onChange: (value) => onChange(mapMultiSelectValueToStrings(value)),
}}
/>
)}
control={control}
name="activeTimeIntervals"
/>
</Field>
{actionButtons}
</form>
);

@ -736,7 +736,7 @@ const TimeIntervals: FC<{ timings: string[]; alertManagerSourceName: string }> =
timings,
alertManagerSourceName,
}) => {
const [, canSeeMuteTimings] = useAlertmanagerAbility(AlertmanagerAction.ViewMuteTiming);
const [, canSeeMuteTimings] = useAlertmanagerAbility(AlertmanagerAction.ViewTimeInterval);
/* TODO make a better mute timing overview, allow combining multiple in to one overview */
/*
<HoverCard

@ -14,6 +14,7 @@ import { NeedHelpInfo } from '../../NeedHelpInfo';
import { ContactPointDetails } from './contactPoint/ContactPointDetails';
import { ContactPointSelector } from './contactPoint/ContactPointSelector';
import { ActiveTimingFields } from './route-settings/ActiveTimingFields';
import { MuteTimingFields } from './route-settings/MuteTimingFields';
import { RoutingSettings } from './route-settings/RouteSettings';
@ -114,6 +115,7 @@ export function AlertManagerManualRouting({ alertManager }: AlertManagerManualRo
/>
</Stack>
<MuteTimingFields alertmanager={alertManagerName} />
<ActiveTimingFields alertmanager={alertManagerName} />
<RoutingSettings alertManager={alertManagerName} />
</Stack>
</CollapsableSection>

@ -32,6 +32,7 @@ export function SimplifiedRouting() {
selectedContactPoint: selectedContactPoint?.selectedContactPoint ?? '',
routeSettings: {
muteTimeIntervals: selectedContactPoint?.muteTimeIntervals ?? [],
activeTimeIntervals: selectedContactPoint?.activeTimeIntervals ?? [],
overrideGrouping: selectedContactPoint?.overrideGrouping ?? false,
groupBy: selectedContactPoint?.groupBy ?? [],
overrideTimings: selectedContactPoint?.overrideTimings ?? false,

@ -0,0 +1,51 @@
import { css } from '@emotion/css';
import { Controller, useFormContext } from 'react-hook-form';
import { GrafanaTheme2 } from '@grafana/data';
import { Field, useStyles2 } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import MuteTimingsSelector from 'app/features/alerting/unified/components/alertmanager-entities/MuteTimingsSelector';
import { BaseAlertmanagerArgs } from 'app/features/alerting/unified/types/hooks';
import { RuleFormValues } from 'app/features/alerting/unified/types/rule-form';
import { mapMultiSelectValueToStrings } from 'app/features/alerting/unified/utils/amroutes';
/** Provides a form field for use in simplified routing, for selecting appropriate mute timings */
export function ActiveTimingFields({ alertmanager }: BaseAlertmanagerArgs) {
const styles = useStyles2(getStyles);
const {
control,
formState: { errors },
} = useFormContext<RuleFormValues>();
return (
<Field
label={t('alerting.active-timing-fields.am-active-timing-select-label-active-timings', 'Active timings')}
data-testid="am-active-timing-select"
description={t(
'alerting.mute-timing-fields.am-active-timing-select-description-active-timings',
'Select a time interval to define when not to send notifications for this alert rule'
)}
className={styles.muteTimingField}
invalid={!!errors.contactPoints?.[alertmanager]?.activeTimeIntervals}
>
<Controller
render={({ field: { onChange, ref, ...field } }) => (
<MuteTimingsSelector
alertmanager={alertmanager}
selectProps={{
...field,
onChange: (value) => onChange(mapMultiSelectValueToStrings(value)),
}}
/>
)}
control={control}
name={`contactPoints.${alertmanager}.activeTimeIntervals`}
/>
</Field>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
muteTimingField: css({
marginTop: theme.spacing(1),
}),
});

@ -96,10 +96,6 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
false,
false,
],
"create-mute-timing": [
false,
false,
],
"create-notification-policy": [
false,
false,
@ -112,15 +108,15 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
true,
false,
],
"decrypt-secrets": [
"create-time-interval": [
false,
false,
],
"delete-contact-point": [
"decrypt-secrets": [
false,
false,
],
"delete-mute-timing": [
"delete-contact-point": [
false,
false,
],
@ -132,6 +128,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
false,
false,
],
"delete-time-interval": [
false,
false,
],
"edit-contact-points": [
false,
false,
@ -144,11 +144,11 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
false,
false,
],
"export-mute-timings": [
"export-notification-policies": [
false,
false,
],
"export-notification-policies": [
"export-time-intervals": [
false,
false,
],
@ -160,10 +160,6 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
false,
false,
],
"update-mute-timing": [
false,
false,
],
"update-notification-policy-tree": [
false,
false,
@ -172,6 +168,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
true,
false,
],
"update-time-interval": [
false,
false,
],
"view-alert-groups": [
true,
false,
@ -188,10 +188,6 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
true,
false,
],
"view-mute-timing": [
true,
false,
],
"view-notification-policy-tree": [
true,
false,
@ -204,6 +200,10 @@ exports[`alertmanager abilities should report Create / Update / Delete actions a
true,
false,
],
"view-time-interval": [
true,
false,
],
}
`;
@ -213,10 +213,6 @@ exports[`alertmanager abilities should report everything except exporting for Mi
true,
true,
],
"create-mute-timing": [
true,
true,
],
"create-notification-policy": [
true,
true,
@ -229,6 +225,10 @@ exports[`alertmanager abilities should report everything except exporting for Mi
true,
true,
],
"create-time-interval": [
true,
true,
],
"decrypt-secrets": [
false,
false,
@ -237,15 +237,15 @@ exports[`alertmanager abilities should report everything except exporting for Mi
true,
true,
],
"delete-mute-timing": [
"delete-notification-policy": [
true,
true,
],
"delete-notification-policy": [
"delete-notification-template": [
true,
true,
],
"delete-notification-template": [
"delete-time-interval": [
true,
true,
],
@ -261,11 +261,11 @@ exports[`alertmanager abilities should report everything except exporting for Mi
false,
false,
],
"export-mute-timings": [
"export-notification-policies": [
false,
true,
],
"export-notification-policies": [
"export-time-intervals": [
false,
true,
],
@ -277,15 +277,15 @@ exports[`alertmanager abilities should report everything except exporting for Mi
true,
true,
],
"update-mute-timing": [
"update-notification-policy-tree": [
true,
true,
],
"update-notification-policy-tree": [
"update-silence": [
true,
true,
],
"update-silence": [
"update-time-interval": [
true,
true,
],
@ -305,10 +305,6 @@ exports[`alertmanager abilities should report everything except exporting for Mi
true,
true,
],
"view-mute-timing": [
true,
true,
],
"view-notification-policy-tree": [
true,
true,
@ -321,6 +317,10 @@ exports[`alertmanager abilities should report everything except exporting for Mi
true,
true,
],
"view-time-interval": [
true,
true,
],
}
`;
@ -330,10 +330,6 @@ exports[`alertmanager abilities should report everything is supported for builti
true,
false,
],
"create-mute-timing": [
true,
false,
],
"create-notification-policy": [
true,
false,
@ -346,15 +342,15 @@ exports[`alertmanager abilities should report everything is supported for builti
true,
false,
],
"decrypt-secrets": [
"create-time-interval": [
true,
false,
],
"delete-contact-point": [
"decrypt-secrets": [
true,
false,
],
"delete-mute-timing": [
"delete-contact-point": [
true,
false,
],
@ -366,6 +362,10 @@ exports[`alertmanager abilities should report everything is supported for builti
true,
false,
],
"delete-time-interval": [
true,
false,
],
"edit-contact-points": [
true,
false,
@ -378,11 +378,11 @@ exports[`alertmanager abilities should report everything is supported for builti
true,
true,
],
"export-mute-timings": [
"export-notification-policies": [
true,
true,
],
"export-notification-policies": [
"export-time-intervals": [
true,
true,
],
@ -394,15 +394,15 @@ exports[`alertmanager abilities should report everything is supported for builti
true,
false,
],
"update-mute-timing": [
"update-notification-policy-tree": [
true,
false,
],
"update-notification-policy-tree": [
"update-silence": [
true,
false,
],
"update-silence": [
"update-time-interval": [
true,
false,
],
@ -422,10 +422,6 @@ exports[`alertmanager abilities should report everything is supported for builti
true,
false,
],
"view-mute-timing": [
true,
true,
],
"view-notification-policy-tree": [
true,
true,
@ -438,5 +434,9 @@ exports[`alertmanager abilities should report everything is supported for builti
true,
true,
],
"view-time-interval": [
true,
true,
],
}
`;

@ -66,12 +66,12 @@ export enum AlertmanagerAction {
UpdateSilence = 'update-silence',
PreviewSilencedInstances = 'preview-silenced-alerts',
// mute timings
ViewMuteTiming = 'view-mute-timing',
CreateMuteTiming = 'create-mute-timing',
UpdateMuteTiming = 'update-mute-timing',
DeleteMuteTiming = 'delete-mute-timing',
ExportMuteTimings = 'export-mute-timings',
// time intervals
ViewTimeInterval = 'view-time-interval',
CreateTimeInterval = 'create-time-interval',
UpdateTimeInterval = 'update-time-interval',
DeleteTimeInterval = 'delete-time-interval',
ExportTimeIntervals = 'export-time-intervals',
// Alert groups
ViewAlertGroups = 'view-alert-groups',
@ -421,28 +421,28 @@ export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
[AlertmanagerAction.ViewSilence]: toAbility(AlwaysSupported, instancePermissions.read),
[AlertmanagerAction.UpdateSilence]: toAbility(AlwaysSupported, instancePermissions.update),
[AlertmanagerAction.PreviewSilencedInstances]: toAbility(AlwaysSupported, instancePermissions.read),
// -- mute timings --
[AlertmanagerAction.CreateMuteTiming]: toAbility(
// -- time intervals --
[AlertmanagerAction.CreateTimeInterval]: toAbility(
hasConfigurationAPI,
notificationsPermissions.create,
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
),
[AlertmanagerAction.ViewMuteTiming]: toAbility(
[AlertmanagerAction.ViewTimeInterval]: toAbility(
AlwaysSupported,
notificationsPermissions.read,
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_READ : [])
),
[AlertmanagerAction.UpdateMuteTiming]: toAbility(
[AlertmanagerAction.UpdateTimeInterval]: toAbility(
hasConfigurationAPI,
notificationsPermissions.update,
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
),
[AlertmanagerAction.DeleteMuteTiming]: toAbility(
[AlertmanagerAction.DeleteTimeInterval]: toAbility(
hasConfigurationAPI,
notificationsPermissions.delete,
...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
),
[AlertmanagerAction.ExportMuteTimings]: toAbility(isGrafanaFlavoredAlertmanager, notificationsPermissions.read),
[AlertmanagerAction.ExportTimeIntervals]: toAbility(isGrafanaFlavoredAlertmanager, notificationsPermissions.read),
[AlertmanagerAction.ViewAlertGroups]: toAbility(AlwaysSupported, instancePermissions.read),
};

@ -59,6 +59,7 @@ exports[`routes Should add a new route with receiver E as a child of default rou
"routes": [],
},
{
"active_time_intervals": undefined,
"continue": undefined,
"group_by": undefined,
"group_interval": undefined,
@ -126,6 +127,7 @@ exports[`routes Should update an existing route A with receiver B 1`] = `
"receiver": "ROOT",
"routes": [
{
"active_time_intervals": undefined,
"continue": undefined,
"group_by": undefined,
"group_interval": undefined,

@ -12,5 +12,6 @@ export interface FormAmRoute {
groupIntervalValue: string;
repeatIntervalValue: string;
muteTimeIntervals: string[];
activeTimeIntervals: string[];
routes: FormAmRoute[];
}

@ -16,6 +16,7 @@ export interface ContactPoint {
groupIntervalValue: string;
repeatIntervalValue: string;
muteTimeIntervals: string[];
activeTimeIntervals: string[];
}
// key: name of alert manager, value ContactPoint

@ -8,6 +8,7 @@ exports[`addRouteToReferenceRoute should be able to add above 1`] = `
"id": "route-2",
},
{
"active_time_intervals": undefined,
"continue": undefined,
"group_by": undefined,
"group_interval": undefined,
@ -39,6 +40,7 @@ exports[`addRouteToReferenceRoute should be able to add as child 1`] = `
"id": "route-3",
"routes": [
{
"active_time_intervals": undefined,
"continue": undefined,
"group_by": undefined,
"group_interval": undefined,
@ -69,6 +71,7 @@ exports[`addRouteToReferenceRoute should be able to add below 1`] = `
"id": "route-3",
},
{
"active_time_intervals": undefined,
"continue": undefined,
"group_by": undefined,
"group_interval": undefined,

@ -60,6 +60,7 @@ export const emptyRoute: FormAmRoute = {
groupIntervalValue: '',
repeatIntervalValue: '',
muteTimeIntervals: [],
activeTimeIntervals: [],
};
// add unique identifiers to each route in the route tree, that way we can figure out what route we've edited / deleted
@ -128,6 +129,7 @@ export const amRouteToFormAmRoute = (route: RouteWithID | undefined): FormAmRout
repeatIntervalValue: route.repeat_interval ?? '',
routes: formRoutes,
muteTimeIntervals: route.mute_time_intervals ?? [],
activeTimeIntervals: route.active_time_intervals ?? [],
};
};
@ -184,6 +186,7 @@ export const formAmRouteToAmRoute = (
repeat_interval,
routes: routes,
mute_time_intervals: formAmRoute.muteTimeIntervals,
active_time_intervals: formAmRoute.activeTimeIntervals,
receiver: receiver,
};

@ -158,6 +158,7 @@ describe('getContactPointsFromDTO', () => {
notification_settings: {
receiver: 'receiver',
mute_time_intervals: ['mute_timing'],
active_time_intervals: ['active_timing'],
group_by: ['group_by'],
group_wait: 'group_wait',
group_interval: 'group_interval',
@ -170,6 +171,7 @@ describe('getContactPointsFromDTO', () => {
[GRAFANA_RULES_SOURCE_NAME]: {
selectedContactPoint: 'receiver',
muteTimeIntervals: ['mute_timing'],
activeTimeIntervals: ['active_timing'],
overrideGrouping: true,
overrideTimings: true,
groupBy: ['group_by'],
@ -188,6 +190,7 @@ describe('getNotificationSettingsForDTO', () => {
grafana: {
selectedContactPoint: 'receiver',
muteTimeIntervals: ['mute_timing'],
activeTimeIntervals: ['active_timing'],
overrideGrouping: true,
overrideTimings: true,
groupBy: ['group_by'],
@ -214,6 +217,7 @@ describe('getNotificationSettingsForDTO', () => {
grafana: {
selectedContactPoint: 'receiver',
muteTimeIntervals: ['mute_timing'],
activeTimeIntervals: ['active_timing'],
overrideGrouping: true,
overrideTimings: true,
groupBy: ['group_by'],
@ -227,6 +231,7 @@ describe('getNotificationSettingsForDTO', () => {
expect(result).toEqual({
receiver: 'receiver',
mute_time_intervals: ['mute_timing'],
active_time_intervals: ['active_timing'],
group_by: ['group_by'],
group_wait: 'group_wait',
group_interval: 'group_interval',

@ -112,6 +112,7 @@ export function getNotificationSettingsForDTO(
return {
receiver: contactPoints?.grafana?.selectedContactPoint,
mute_time_intervals: contactPoints?.grafana?.muteTimeIntervals,
active_time_intervals: contactPoints?.grafana?.activeTimeIntervals,
group_by: contactPoints?.grafana?.overrideGrouping ? contactPoints?.grafana?.groupBy : undefined,
group_wait:
contactPoints?.grafana?.overrideTimings && contactPoints?.grafana?.groupWaitValue
@ -233,6 +234,7 @@ export function getContactPointsFromDTO(ga: GrafanaRuleDefinition): AlertManager
? {
selectedContactPoint: ga.notification_settings.receiver,
muteTimeIntervals: ga.notification_settings.mute_time_intervals ?? [],
activeTimeIntervals: ga.notification_settings.active_time_intervals ?? [],
overrideGrouping:
Array.isArray(ga.notification_settings.group_by) && ga.notification_settings.group_by.length > 0,
overrideTimings: [

@ -245,6 +245,7 @@ export interface GrafanaNotificationSettings {
group_interval?: string;
repeat_interval?: string;
mute_time_intervals?: string[];
active_time_intervals?: string[];
}
export interface GrafanaEditorSettings {

@ -320,6 +320,9 @@
}
},
"alerting": {
"active-timing-fields": {
"am-active-timing-select-label-active-timings": "Active timings"
},
"add-button": {
"add-more": "Add more"
},
@ -581,6 +584,8 @@
},
"am-routes-expanded-form": {
"add-matcher": "Add matcher",
"am-active-timing-select-label-active-timings": "Active timings",
"am-mute-timing-select-description-add-active-timing-to-policy": "Add active timing to policy",
"am-mute-timing-select-description-add-mute-timing-to-policy": "Add mute timing to policy",
"am-mute-timing-select-label-mute-timings": "Mute timings",
"aria-label-group-by": "Group by",
@ -1489,29 +1494,21 @@
"aria-label": "More",
"button-text": "More"
},
"mute_timings": {
"error-loading": {
"description": "Could not load mute timings. Please try again later.",
"title": "Error loading mute timings"
}
},
"mute-timing-actions-buttons": {
"text-disabled": "Disabled",
"title-delete-mute-timing": "Delete mute timing"
},
"mute-timing-fields": {
"am-active-timing-select-description-active-timings": "Select a time interval to define when not to send notifications for this alert rule",
"am-mute-timing-select-description-mute-timings": "Select a mute timing to define when not to send notifications for this alert rule",
"am-mute-timing-select-label-mute-timings": "Mute timings"
},
"mute-timing-form": {
"description-unique-timing": "A unique name for the mute timing",
"label-name": "Name",
"text-loading-mute-timing": "Loading mute timing",
"title-no-matching-mute-timing-found": "No matching mute timing found"
"label-name": "Name"
},
"mute-timing-time-interval": {
"add-another-time-interval": "Add another time interval",
"description": "A time interval is a definition for a moment in time. All fields are lists, and at least one list element must be satisfied to match the field. If a field is left blank, any moment of time will match the field. For an instant of time to match a complete time interval, all fields must match. A mute timing can contain multiple time intervals.",
"add-another-time-interval-item": "Add another time interval item",
"description": "A time interval item is a definition for a moment in time. All fields are lists, and at least one list element must be satisfied to match the field. If a field is left blank, any moment of time will match the field. For an instant of time to match a complete time interval, all fields must match. A time interval can contain multiple time interval items.",
"description-dats-of-the-month": "The days of the month, 1:31, of a month. Negative values can be used to represent days which begin at the end of the month",
"description-months": "The months of the year in either numerical or the full calendar month",
"label-days-of-the-month": "Days of the month",
@ -1538,21 +1535,6 @@
"title-remove": "Remove",
"tooltip-remove-time-range": "Remove time range"
},
"mute-timings": {
"add-mute-timing": "Add mute timing",
"description": "Enter specific time intervals when not to send notifications or freeze notifications for recurring periods of time.",
"save": "Save mute timing",
"saving": "Saving mute timing"
},
"mute-timings-selector": {
"aria-label-mute-timings": "Mute timings",
"placeholder-select-mute-timings": "Select mute timings..."
},
"mute-timings-table": {
"text-havent-created-timings": "You haven't created any mute timings yet",
"text-loading-mute-timings": "Loading mute timings...",
"text-no-mute-timings-configured": "No mute timings configured"
},
"namespace": {
"title-alert-rules": "Alert rules"
},
@ -1593,8 +1575,8 @@
"title-notification-policies-have-changed": "Notification policies have changed"
},
"notification-policies-tabs": {
"label-mute-timings": "Mute Timings",
"label-notification-policies": "Notification Policies"
"label-notification-policies": "Notification Policies",
"label-time-intervals": "Time intervals"
},
"notification-policy-matchers": {
"default-policy": "Default policy",
@ -2430,6 +2412,32 @@
"input": "Input",
"stop-alerting-when": "Stop alerting (or pending state) when "
},
"time-interval": {
"add-time-interval": "Add time interval",
"save": "Save time interval",
"saving": "Saving time interval"
},
"time-interval-form": {
"description-unique-time-interval": "A unique name for the time interval",
"text-loading-time-interval": "Loading time interval",
"title-no-matching-time-interval-found": "No matching time interval found"
},
"time-intervals": {
"description": "Enter specific time intervals when not to send notifications or freeze notifications for recurring periods of time.",
"error-loading": {
"description": "Could not load time intervals. Please try again later.",
"title": "Error loading time intervals"
}
},
"time-intervals-selector": {
"aria-label-time-intervals": "Time intervals",
"placeholder-select-time-intervals": "Select time intervals..."
},
"time-intervals-table": {
"text-havent-created-time-intervals": "You haven't created any time intervals yet",
"text-loading-time-intervals": "Loading time intervals...",
"text-no-time-intervals-configured": "No time intervals configured"
},
"timeseries-row": {
"time-series-data": "Time series data",
"timestamp": "Timestamp",

Loading…
Cancel
Save