mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Add read-only GMA rules to the new list view (#98116)
* Reuse prom groups generator between GMA, external DS and list view * Improve generators, add initial support for GMA in grouped view components * Improve handling of GMA rules * Split componentes into files * Improve error handling, simplify groups grouping * Extract grafana rules component * Reset yarn.lock * Reset yarn.lock 2 * Update filters, adjust file names, add folder display name to GMA rules * Re-enable filtering for cloud rules * Rename AlertRuleLoader * Add missing translations, fix lint errors * Remove unused imports, update translations * Fix responses in BE tests * Update backend tests * Update integration test * Tidy up group page size constants * Add error throwing to getGroups endpoint to prevent grafana usage * Refactor FilterView to remove exhaustive check * Refactor common props for grafana rule rendering * Unify identifiers' discriminators, add comments, minor refactor * Update translations * Remove unnecessary prev page condition, add a few explanations --------- Co-authored-by: fayzal-g <fayzal.ghantiwala@grafana.com> Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
This commit is contained in:
@@ -2792,16 +2792,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/alerting/unified/rule-list/FilterView.tsx:5381": [
|
"public/app/features/alerting/unified/rule-list/FilterView.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/alerting/unified/rule-list/GroupedView.tsx:5381": [
|
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "1"],
|
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "2"],
|
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "3"],
|
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "4"],
|
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "5"],
|
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "6"],
|
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "7"]
|
|
||||||
],
|
|
||||||
"public/app/features/alerting/unified/rule-list/RuleList.v1.tsx:5381": [
|
"public/app/features/alerting/unified/rule-list/RuleList.v1.tsx:5381": [
|
||||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"]
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -198,7 +198,6 @@ func (_m *FakeDashboardService) GetAllDashboards(ctx context.Context) ([]*Dashbo
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func (_m *FakeDashboardService) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error) {
|
func (_m *FakeDashboardService) GetAllDashboardsByOrgId(ctx context.Context, orgID int64) ([]*Dashboard, error) {
|
||||||
ret := _m.Called(ctx, orgID)
|
ret := _m.Called(ctx, orgID)
|
||||||
|
|
||||||
|
|||||||
@@ -432,7 +432,7 @@ func (ss *FolderUnifiedStoreImpl) GetDescendants(ctx context.Context, orgID int6
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getDescendants(nodes map[string]*folder.Folder, tree map[string]map[string]*folder.Folder, ancestor_uid string, descendantsMap map[string]*folder.Folder) {
|
func getDescendants(nodes map[string]*folder.Folder, tree map[string]map[string]*folder.Folder, ancestor_uid string, descendantsMap map[string]*folder.Folder) {
|
||||||
for uid, _ := range tree[ancestor_uid] {
|
for uid := range tree[ancestor_uid] {
|
||||||
descendantsMap[uid] = nodes[uid]
|
descendantsMap[uid] = nodes[uid]
|
||||||
getDescendants(nodes, tree, uid, descendantsMap)
|
getDescendants(nodes, tree, uid, descendantsMap)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -489,7 +489,8 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, sr StatusRe
|
|||||||
newGroup := &apimodels.RuleGroup{
|
newGroup := &apimodels.RuleGroup{
|
||||||
Name: groupKey.RuleGroup,
|
Name: groupKey.RuleGroup,
|
||||||
// file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana.
|
// file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana.
|
||||||
File: folderFullPath,
|
File: folderFullPath,
|
||||||
|
FolderUID: groupKey.NamespaceUID,
|
||||||
}
|
}
|
||||||
|
|
||||||
rulesTotals := make(map[string]int64, len(rules))
|
rulesTotals := make(map[string]int64, len(rules))
|
||||||
@@ -514,7 +515,9 @@ func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, sr StatusRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
newRule := apimodels.Rule{
|
newRule := apimodels.Rule{
|
||||||
|
UID: rule.UID,
|
||||||
Name: rule.Title,
|
Name: rule.Title,
|
||||||
|
FolderUID: rule.NamespaceUID,
|
||||||
Labels: apimodels.LabelsFromMap(rule.GetLabels(labelOptions...)),
|
Labels: apimodels.LabelsFromMap(rule.GetLabels(labelOptions...)),
|
||||||
Health: status.Health,
|
Health: status.Health,
|
||||||
LastError: errorOrEmpty(status.LastError),
|
LastError: errorOrEmpty(status.LastError),
|
||||||
|
|||||||
@@ -314,9 +314,12 @@ func TestRouteGetRuleStatuses(t *testing.T) {
|
|||||||
"groups": [{
|
"groups": [{
|
||||||
"name": "rule-group",
|
"name": "rule-group",
|
||||||
"file": "%s",
|
"file": "%s",
|
||||||
|
"folderUid": "namespaceUID",
|
||||||
"rules": [{
|
"rules": [{
|
||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiring",
|
"name": "AlwaysFiring",
|
||||||
|
"folderUid": "namespaceUID",
|
||||||
|
"uid": "RuleUID",
|
||||||
"query": "vector(1)",
|
"query": "vector(1)",
|
||||||
"alerts": [{
|
"alerts": [{
|
||||||
"labels": {
|
"labels": {
|
||||||
@@ -377,10 +380,13 @@ func TestRouteGetRuleStatuses(t *testing.T) {
|
|||||||
"groups": [{
|
"groups": [{
|
||||||
"name": "rule-group",
|
"name": "rule-group",
|
||||||
"file": "%s",
|
"file": "%s",
|
||||||
|
"folderUid": "namespaceUID",
|
||||||
"rules": [{
|
"rules": [{
|
||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiring",
|
"name": "AlwaysFiring",
|
||||||
"query": "vector(1)",
|
"query": "vector(1)",
|
||||||
|
"folderUid": "namespaceUID",
|
||||||
|
"uid": "RuleUID",
|
||||||
"alerts": [{
|
"alerts": [{
|
||||||
"labels": {
|
"labels": {
|
||||||
"job": "prometheus",
|
"job": "prometheus",
|
||||||
@@ -439,10 +445,13 @@ func TestRouteGetRuleStatuses(t *testing.T) {
|
|||||||
"groups": [{
|
"groups": [{
|
||||||
"name": "rule-group",
|
"name": "rule-group",
|
||||||
"file": "%s",
|
"file": "%s",
|
||||||
|
"folderUid": "namespaceUID",
|
||||||
"rules": [{
|
"rules": [{
|
||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiring",
|
"name": "AlwaysFiring",
|
||||||
"query": "vector(1) | vector(1)",
|
"query": "vector(1) | vector(1)",
|
||||||
|
"folderUid": "namespaceUID",
|
||||||
|
"uid": "RuleUID",
|
||||||
"alerts": [{
|
"alerts": [{
|
||||||
"labels": {
|
"labels": {
|
||||||
"job": "prometheus"
|
"job": "prometheus"
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ type RuleGroup struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// required: true
|
// required: true
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
|
// required: true
|
||||||
|
FolderUID string `json:"folderUid"`
|
||||||
// In order to preserve rule ordering, while exposing type (alerting or recording)
|
// In order to preserve rule ordering, while exposing type (alerting or recording)
|
||||||
// specific properties, both alerting and recording rules are exposed in the
|
// specific properties, both alerting and recording rules are exposed in the
|
||||||
// same array.
|
// same array.
|
||||||
@@ -165,9 +167,13 @@ type AlertingRule struct {
|
|||||||
// adapted from cortex
|
// adapted from cortex
|
||||||
// swagger:model
|
// swagger:model
|
||||||
type Rule struct {
|
type Rule struct {
|
||||||
|
// required: true
|
||||||
|
UID string `json:"uid"`
|
||||||
// required: true
|
// required: true
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// required: true
|
// required: true
|
||||||
|
FolderUID string `json:"folderUid"`
|
||||||
|
// required: true
|
||||||
Query string `json:"query"`
|
Query string `json:"query"`
|
||||||
Labels promlabels.Labels `json:"labels,omitempty"`
|
Labels promlabels.Labels `json:"labels,omitempty"`
|
||||||
// required: true
|
// required: true
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Declare respModel at the function level
|
||||||
|
var respModel apimodels.UpdateRuleGroupResponse
|
||||||
|
|
||||||
func TestIntegrationPrometheusRules(t *testing.T) {
|
func TestIntegrationPrometheusRules(t *testing.T) {
|
||||||
testinfra.SQLiteIntegrationTest(t)
|
testinfra.SQLiteIntegrationTest(t)
|
||||||
|
|
||||||
@@ -157,7 +160,6 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
|
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||||
var respModel apimodels.UpdateRuleGroupResponse
|
|
||||||
require.NoError(t, json.Unmarshal(b, &respModel))
|
require.NoError(t, json.Unmarshal(b, &respModel))
|
||||||
require.Len(t, respModel.Created, len(rules.Rules))
|
require.Len(t, respModel.Created, len(rules.Rules))
|
||||||
}
|
}
|
||||||
@@ -235,18 +237,21 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
|
|
||||||
require.JSONEq(t, `
|
require.JSONEq(t, fmt.Sprintf(`
|
||||||
{
|
{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"groups": [{
|
"groups": [{
|
||||||
"name": "arulegroup",
|
"name": "arulegroup",
|
||||||
"file": "default",
|
"file": "default",
|
||||||
|
"folderUid": "default",
|
||||||
"rules": [{
|
"rules": [{
|
||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiring",
|
"name": "AlwaysFiring",
|
||||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||||
"duration": 10,
|
"duration": 10,
|
||||||
|
"folderUid": "default",
|
||||||
|
"uid": "%s",
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"annotation1": "val1"
|
"annotation1": "val1"
|
||||||
},
|
},
|
||||||
@@ -261,6 +266,8 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
|||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiringButSilenced",
|
"name": "AlwaysFiringButSilenced",
|
||||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||||
|
"folderUid": "default",
|
||||||
|
"uid": "%s",
|
||||||
"health": "ok",
|
"health": "ok",
|
||||||
"type": "alerting",
|
"type": "alerting",
|
||||||
"lastEvaluation": "0001-01-01T00:00:00Z",
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
||||||
@@ -277,7 +284,7 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
|||||||
"inactive": 2
|
"inactive": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`, string(b))
|
}`, respModel.Created[0], respModel.Created[1]), string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -293,18 +300,21 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
|||||||
b, err := io.ReadAll(resp.Body)
|
b, err := io.ReadAll(resp.Body)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, 200, resp.StatusCode)
|
require.Equal(t, 200, resp.StatusCode)
|
||||||
require.JSONEq(t, `
|
require.JSONEq(t, fmt.Sprintf(`
|
||||||
{
|
{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"data": {
|
"data": {
|
||||||
"groups": [{
|
"groups": [{
|
||||||
"name": "arulegroup",
|
"name": "arulegroup",
|
||||||
"file": "default",
|
"file": "default",
|
||||||
|
"folderUid": "default",
|
||||||
"rules": [{
|
"rules": [{
|
||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiring",
|
"name": "AlwaysFiring",
|
||||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||||
"duration": 10,
|
"duration": 10,
|
||||||
|
"folderUid": "default",
|
||||||
|
"uid": "%s",
|
||||||
"annotations": {
|
"annotations": {
|
||||||
"annotation1": "val1"
|
"annotation1": "val1"
|
||||||
},
|
},
|
||||||
@@ -319,6 +329,8 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
|||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiringButSilenced",
|
"name": "AlwaysFiringButSilenced",
|
||||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||||
|
"folderUid": "default",
|
||||||
|
"uid": "%s",
|
||||||
"health": "ok",
|
"health": "ok",
|
||||||
"type": "alerting",
|
"type": "alerting",
|
||||||
"lastEvaluation": "0001-01-01T00:00:00Z",
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
||||||
@@ -335,7 +347,7 @@ func TestIntegrationPrometheusRules(t *testing.T) {
|
|||||||
"inactive": 2
|
"inactive": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`, string(b))
|
}`, respModel.Created[0], respModel.Created[1]), string(b))
|
||||||
return true
|
return true
|
||||||
}, 18*time.Second, 2*time.Second)
|
}, 18*time.Second, 2*time.Second)
|
||||||
}
|
}
|
||||||
@@ -441,7 +453,6 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
|
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||||
var respModel apimodels.UpdateRuleGroupResponse
|
|
||||||
require.NoError(t, json.Unmarshal(b, &respModel))
|
require.NoError(t, json.Unmarshal(b, &respModel))
|
||||||
require.Len(t, respModel.Created, len(rules.Rules))
|
require.Len(t, respModel.Created, len(rules.Rules))
|
||||||
}
|
}
|
||||||
@@ -453,9 +464,12 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
|||||||
"groups": [{
|
"groups": [{
|
||||||
"name": "anotherrulegroup",
|
"name": "anotherrulegroup",
|
||||||
"file": "default",
|
"file": "default",
|
||||||
|
"folderUid": "default",
|
||||||
"rules": [{
|
"rules": [{
|
||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiring",
|
"name": "AlwaysFiring",
|
||||||
|
"uid": "%s",
|
||||||
|
"folderUid": "default",
|
||||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||||
"duration": 10,
|
"duration": 10,
|
||||||
"annotations": {
|
"annotations": {
|
||||||
@@ -469,6 +483,8 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
|||||||
}, {
|
}, {
|
||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiringButSilenced",
|
"name": "AlwaysFiringButSilenced",
|
||||||
|
"uid": "%s",
|
||||||
|
"folderUid": "default",
|
||||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||||
"health": "ok",
|
"health": "ok",
|
||||||
"type": "alerting",
|
"type": "alerting",
|
||||||
@@ -486,7 +502,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
|||||||
"inactive": 2
|
"inactive": 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`, dashboardUID)
|
}`, respModel.Created[0], dashboardUID, respModel.Created[1])
|
||||||
expectedFilteredByJSON := fmt.Sprintf(`
|
expectedFilteredByJSON := fmt.Sprintf(`
|
||||||
{
|
{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
@@ -494,9 +510,12 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
|||||||
"groups": [{
|
"groups": [{
|
||||||
"name": "anotherrulegroup",
|
"name": "anotherrulegroup",
|
||||||
"file": "default",
|
"file": "default",
|
||||||
|
"folderUid": "default",
|
||||||
"rules": [{
|
"rules": [{
|
||||||
"state": "inactive",
|
"state": "inactive",
|
||||||
"name": "AlwaysFiring",
|
"name": "AlwaysFiring",
|
||||||
|
"uid": "%s",
|
||||||
|
"folderUid": "default",
|
||||||
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
||||||
"duration": 10,
|
"duration": 10,
|
||||||
"annotations": {
|
"annotations": {
|
||||||
@@ -519,7 +538,7 @@ func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
|||||||
"inactive": 1
|
"inactive": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}`, dashboardUID)
|
}`, respModel.Created[0], dashboardUID)
|
||||||
expectedNoneJSON := `
|
expectedNoneJSON := `
|
||||||
{
|
{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { produce } from 'immer';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
@@ -89,10 +90,9 @@ export function paramsWithMatcherAndState(
|
|||||||
return paramsResult;
|
return paramsResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const groupRulesByFileName = (groups: PromRuleGroupDTO[], dataSourceName: string) => {
|
export function normalizeRuleGroup(group: PromRuleGroupDTO): PromRuleGroupDTO {
|
||||||
const nsMap: { [key: string]: RuleNamespace } = {};
|
return produce(group, (draft) => {
|
||||||
groups.forEach((group) => {
|
draft.rules.forEach((rule) => {
|
||||||
group.rules.forEach((rule) => {
|
|
||||||
rule.query = rule.query || '';
|
rule.query = rule.query || '';
|
||||||
if (rule.type === PromRuleType.Alerting) {
|
if (rule.type === PromRuleType.Alerting) {
|
||||||
// There's a possibility that a custom/unexpected datasource might response with
|
// There's a possibility that a custom/unexpected datasource might response with
|
||||||
@@ -100,11 +100,19 @@ export const groupRulesByFileName = (groups: PromRuleGroupDTO[], dataSourceName:
|
|||||||
// In this case, we fall back to `Inactive` state so that elsewhere in the UI we don't fail/have to handle the edge case
|
// In this case, we fall back to `Inactive` state so that elsewhere in the UI we don't fail/have to handle the edge case
|
||||||
// and log a message so we can identify how frequently this might be happening
|
// and log a message so we can identify how frequently this might be happening
|
||||||
if (!rule.state) {
|
if (!rule.state) {
|
||||||
logInfo('prom rule with type=alerting is missing a state', { dataSourceName, ruleName: rule.name });
|
logInfo('prom rule with type=alerting is missing a state', { ruleName: rule.name });
|
||||||
rule.state = PromAlertingRuleState.Inactive;
|
rule.state = PromAlertingRuleState.Inactive;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const groupRulesByFileName = (groups: PromRuleGroupDTO[], dataSourceName: string) => {
|
||||||
|
const normalizedGroups = groups.map(normalizeRuleGroup);
|
||||||
|
|
||||||
|
const nsMap: { [key: string]: RuleNamespace } = {};
|
||||||
|
normalizedGroups.forEach((group) => {
|
||||||
if (!nsMap[group.file]) {
|
if (!nsMap[group.file]) {
|
||||||
nsMap[group.file] = {
|
nsMap[group.file] = {
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
@@ -118,6 +126,7 @@ export const groupRulesByFileName = (groups: PromRuleGroupDTO[], dataSourceName:
|
|||||||
|
|
||||||
return Object.values(nsMap);
|
return Object.values(nsMap);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ungroupRulesByFileName = (namespaces: RuleNamespace[] = []): PromRuleGroupDTO[] => {
|
export const ungroupRulesByFileName = (namespaces: RuleNamespace[] = []): PromRuleGroupDTO[] => {
|
||||||
return namespaces?.flatMap((namespace) =>
|
return namespaces?.flatMap((namespace) =>
|
||||||
namespace.groups.flatMap((group) => ruleGroupToPromRuleGroupDTO(group, namespace.name))
|
namespace.groups.flatMap((group) => ruleGroupToPromRuleGroupDTO(group, namespace.name))
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
import { GrafanaPromRuleGroupDTO, PromRuleDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||||
|
|
||||||
import { alertingApi } from './alertingApi';
|
import { alertingApi } from './alertingApi';
|
||||||
|
import { normalizeRuleGroup } from './prometheus';
|
||||||
|
|
||||||
interface PromRulesResponse {
|
export interface PromRulesResponse<TRuleGroup> {
|
||||||
status: string;
|
status: string;
|
||||||
data: {
|
data: {
|
||||||
groups: PromRuleGroupDTO[];
|
groups: TRuleGroup[];
|
||||||
groupNextToken?: string;
|
groupNextToken?: string;
|
||||||
};
|
};
|
||||||
errorType?: string;
|
errorType?: string;
|
||||||
@@ -22,11 +25,37 @@ interface PromRulesOptions {
|
|||||||
groupNextToken?: string;
|
groupNextToken?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GrafanaPromRulesOptions = Omit<PromRulesOptions, 'ruleSource'> & {
|
||||||
|
dashboardUid?: string;
|
||||||
|
panelId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
export const prometheusApi = alertingApi.injectEndpoints({
|
export const prometheusApi = alertingApi.injectEndpoints({
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
groups: build.query<PromRulesResponse, PromRulesOptions>({
|
getGroups: build.query<PromRulesResponse<PromRuleGroupDTO<PromRuleDTO>>, PromRulesOptions>({
|
||||||
query: ({ ruleSource, namespace, groupName, ruleName, groupLimit, excludeAlerts, groupNextToken }) => ({
|
query: ({ ruleSource, namespace, groupName, ruleName, groupLimit, excludeAlerts, groupNextToken }) => {
|
||||||
url: `api/prometheus/${ruleSource.uid}/api/v1/rules`,
|
if (ruleSource.uid === GRAFANA_RULES_SOURCE_NAME) {
|
||||||
|
throw new Error('Please use getGrafanaGroups endpoint for grafana rules');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
url: `api/prometheus/${ruleSource.uid}/api/v1/rules`,
|
||||||
|
params: {
|
||||||
|
'file[]': namespace,
|
||||||
|
'group[]': groupName,
|
||||||
|
'rule[]': ruleName,
|
||||||
|
exclude_alerts: excludeAlerts?.toString(),
|
||||||
|
group_limit: groupLimit?.toFixed(0),
|
||||||
|
group_next_token: groupNextToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
transformResponse: (response: PromRulesResponse<PromRuleGroupDTO<PromRuleDTO>>) => {
|
||||||
|
return { ...response, data: { ...response.data, groups: response.data.groups.map(normalizeRuleGroup) } };
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getGrafanaGroups: build.query<PromRulesResponse<GrafanaPromRuleGroupDTO>, GrafanaPromRulesOptions>({
|
||||||
|
query: ({ namespace, groupName, ruleName, groupLimit, excludeAlerts, groupNextToken }) => ({
|
||||||
|
url: `api/prometheus/grafana/api/v1/rules`,
|
||||||
params: {
|
params: {
|
||||||
'file[]': namespace,
|
'file[]': namespace,
|
||||||
'group[]': groupName,
|
'group[]': groupName,
|
||||||
|
|||||||
@@ -14,12 +14,15 @@ import { ActionsLoader, RuleActionsButtons } from './components/RuleActionsButto
|
|||||||
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
||||||
const { useGetRuleGroupForNamespaceQuery } = alertRuleApi;
|
const { useGetRuleGroupForNamespaceQuery } = alertRuleApi;
|
||||||
|
|
||||||
interface AlertRuleLoaderProps {
|
interface DataSourceRuleLoaderProps {
|
||||||
rule: Rule;
|
rule: Rule;
|
||||||
groupIdentifier: DataSourceRuleGroupIdentifier;
|
groupIdentifier: DataSourceRuleGroupIdentifier;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AlertRuleLoader = memo(function AlertRuleLoader({ rule, groupIdentifier }: AlertRuleLoaderProps) {
|
export const DataSourceRuleLoader = memo(function DataSourceRuleLoader({
|
||||||
|
rule,
|
||||||
|
groupIdentifier,
|
||||||
|
}: DataSourceRuleLoaderProps) {
|
||||||
const { rulesSource, namespace, groupName } = groupIdentifier;
|
const { rulesSource, namespace, groupName } = groupIdentifier;
|
||||||
|
|
||||||
const ruleIdentifier = fromRule(rulesSource.name, namespace.name, groupName, rule);
|
const ruleIdentifier = fromRule(rulesSource.name, namespace.name, groupName, rule);
|
||||||
@@ -9,12 +9,19 @@ import { isLoading, useAsync } from '../hooks/useAsync';
|
|||||||
import { RulesFilter } from '../search/rulesSearchParser';
|
import { RulesFilter } from '../search/rulesSearchParser';
|
||||||
import { hashRule } from '../utils/rule-id';
|
import { hashRule } from '../utils/rule-id';
|
||||||
|
|
||||||
import { AlertRuleLoader } from './AlertRuleLoader';
|
import { DataSourceRuleLoader } from './DataSourceRuleLoader';
|
||||||
|
import { GrafanaRuleLoader } from './GrafanaRuleLoader';
|
||||||
import LoadMoreHelper from './LoadMoreHelper';
|
import LoadMoreHelper from './LoadMoreHelper';
|
||||||
|
import { UnknownRuleListItem } from './components/AlertRuleListItem';
|
||||||
import { ListItem } from './components/ListItem';
|
import { ListItem } from './components/ListItem';
|
||||||
import { ActionsLoader } from './components/RuleActionsButtons.V2';
|
import { ActionsLoader } from './components/RuleActionsButtons.V2';
|
||||||
import { RuleListIcon } from './components/RuleListIcon';
|
import { RuleListIcon } from './components/RuleListIcon';
|
||||||
import { RuleWithOrigin, useFilteredRulesIteratorProvider } from './hooks/useFilteredRulesIterator';
|
import {
|
||||||
|
GrafanaRuleWithOrigin,
|
||||||
|
PromRuleWithOrigin,
|
||||||
|
RuleWithOrigin,
|
||||||
|
useFilteredRulesIteratorProvider,
|
||||||
|
} from './hooks/useFilteredRulesIterator';
|
||||||
|
|
||||||
interface FilterViewProps {
|
interface FilterViewProps {
|
||||||
filterState: RulesFilter;
|
filterState: RulesFilter;
|
||||||
@@ -30,13 +37,13 @@ export function FilterView({ filterState }: FilterViewProps) {
|
|||||||
return <FilterViewResults filterState={filterState} key={JSON.stringify(filterState)} />;
|
return <FilterViewResults filterState={filterState} key={JSON.stringify(filterState)} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface KeyedRuleWithOrigin extends RuleWithOrigin {
|
type KeyedRuleWithOrigin = RuleWithOrigin & {
|
||||||
/**
|
/**
|
||||||
* Artificial frontend-only identifier for the rule.
|
* Artificial frontend-only identifier for the rule.
|
||||||
* It's used as a key for the rule in the rule list to prevent key duplication
|
* It's used as a key for the rule in the rule list to prevent key duplication
|
||||||
*/
|
*/
|
||||||
key: string;
|
key: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Renders the list of rules that match the filter.
|
* Renders the list of rules that match the filter.
|
||||||
@@ -107,9 +114,25 @@ function FilterViewResults({ filterState }: FilterViewProps) {
|
|||||||
return (
|
return (
|
||||||
<Stack direction="column" gap={0}>
|
<Stack direction="column" gap={0}>
|
||||||
<ul aria-label="filtered-rule-list">
|
<ul aria-label="filtered-rule-list">
|
||||||
{rules.map(({ key, rule, groupIdentifier }) => (
|
{rules.map((ruleWithOrigin) => {
|
||||||
<AlertRuleLoader key={key} rule={rule} groupIdentifier={groupIdentifier} />
|
const { key, rule, groupIdentifier, origin } = ruleWithOrigin;
|
||||||
))}
|
|
||||||
|
switch (origin) {
|
||||||
|
case 'grafana':
|
||||||
|
return (
|
||||||
|
<GrafanaRuleLoader
|
||||||
|
key={key}
|
||||||
|
rule={rule}
|
||||||
|
groupName={groupIdentifier.groupName}
|
||||||
|
namespaceName={ruleWithOrigin.namespaceName}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'datasource':
|
||||||
|
return <DataSourceRuleLoader key={key} rule={rule} groupIdentifier={groupIdentifier} />;
|
||||||
|
default:
|
||||||
|
return <UnknownRuleListItem key={key} rule={rule} groupIdentifier={groupIdentifier} />;
|
||||||
|
}
|
||||||
|
})}
|
||||||
{loading && (
|
{loading && (
|
||||||
<>
|
<>
|
||||||
<AlertRuleListItemLoader />
|
<AlertRuleListItemLoader />
|
||||||
@@ -146,7 +169,22 @@ function onFinished<T>(fn: () => void) {
|
|||||||
return tap<T>(undefined, undefined, fn);
|
return tap<T>(undefined, undefined, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRuleKey(ruleWithOrigin: RuleWithOrigin) {
|
function getRuleKey(ruleWithOrigin: RuleWithOrigin): string {
|
||||||
|
if (ruleWithOrigin.origin === 'grafana') {
|
||||||
|
return getGrafanaRuleKey(ruleWithOrigin);
|
||||||
|
}
|
||||||
|
return getDataSourceRuleKey(ruleWithOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGrafanaRuleKey(ruleWithOrigin: GrafanaRuleWithOrigin) {
|
||||||
|
const {
|
||||||
|
groupIdentifier: { namespace, groupName },
|
||||||
|
rule,
|
||||||
|
} = ruleWithOrigin;
|
||||||
|
return `grafana-${namespace.uid}-${groupName}-${rule.uid}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataSourceRuleKey(ruleWithOrigin: PromRuleWithOrigin) {
|
||||||
const {
|
const {
|
||||||
rule,
|
rule,
|
||||||
groupIdentifier: { rulesSource, namespace, groupName },
|
groupIdentifier: { rulesSource, namespace, groupName },
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { GrafanaPromRuleDTO, PromRuleType } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { GrafanaRulesSource } from '../utils/datasource';
|
||||||
|
import { createRelativeUrl } from '../utils/url';
|
||||||
|
|
||||||
|
import { AlertRuleListItem, RecordingRuleListItem, UnknownRuleListItem } from './components/AlertRuleListItem';
|
||||||
|
|
||||||
|
interface GrafanaRuleLoaderProps {
|
||||||
|
rule: GrafanaPromRuleDTO;
|
||||||
|
groupName: string;
|
||||||
|
namespaceName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GrafanaRuleLoader({ rule, groupName, namespaceName }: GrafanaRuleLoaderProps) {
|
||||||
|
const { folderUid } = rule;
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
name: rule.name,
|
||||||
|
rulesSource: GrafanaRulesSource,
|
||||||
|
group: groupName,
|
||||||
|
namespace: namespaceName,
|
||||||
|
href: createRelativeUrl(`/alerting/grafana/${rule.uid}/view`),
|
||||||
|
health: rule.health,
|
||||||
|
error: rule.lastError,
|
||||||
|
labels: rule.labels,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (rule.type === PromRuleType.Alerting) {
|
||||||
|
return (
|
||||||
|
<AlertRuleListItem
|
||||||
|
{...commonProps}
|
||||||
|
application="grafana"
|
||||||
|
summary={rule.annotations?.summary}
|
||||||
|
state={rule.state}
|
||||||
|
isProvisioned={undefined}
|
||||||
|
instancesCount={rule.alerts?.length}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.type === PromRuleType.Recording) {
|
||||||
|
return <RecordingRuleListItem {...commonProps} application="grafana" isProvisioned={undefined} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UnknownRuleListItem
|
||||||
|
rule={rule}
|
||||||
|
groupIdentifier={{
|
||||||
|
rulesSource: GrafanaRulesSource,
|
||||||
|
groupName,
|
||||||
|
namespace: { uid: folderUid },
|
||||||
|
groupOrigin: 'grafana',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,53 +1,46 @@
|
|||||||
import { css } from '@emotion/css';
|
import { useMemo } from 'react';
|
||||||
import { PropsWithChildren, ReactNode, useMemo } from 'react';
|
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { Stack } from '@grafana/ui';
|
||||||
import { Button, Dropdown, Icon, IconButton, LinkButton, Menu, Stack, Text, useStyles2 } from '@grafana/ui';
|
import { DataSourceRulesSourceIdentifier } from 'app/types/unified-alerting';
|
||||||
import { Trans } from 'app/core/internationalization';
|
|
||||||
import { DataSourceNamespaceIdentifier, DataSourceRuleGroupIdentifier, RuleGroup } from 'app/types/unified-alerting';
|
|
||||||
import { RulesSourceApplication } from 'app/types/unified-alerting-dto';
|
|
||||||
|
|
||||||
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
import { featureDiscoveryApi } from '../api/featureDiscoveryApi';
|
||||||
import { Spacer } from '../components/Spacer';
|
import { GrafanaRulesSource, getExternalRulesSources } from '../utils/datasource';
|
||||||
import { WithReturnButton } from '../components/WithReturnButton';
|
|
||||||
import { getDatasourceAPIUid, getExternalRulesSources } from '../utils/datasource';
|
|
||||||
import { hashRule } from '../utils/rule-id';
|
|
||||||
|
|
||||||
import { AlertRuleLoader } from './AlertRuleLoader';
|
import { PaginatedDataSourceLoader } from './PaginatedDataSourceLoader';
|
||||||
import { ListGroup } from './components/ListGroup';
|
import { PaginatedGrafanaLoader } from './PaginatedGrafanaLoader';
|
||||||
import { ListSection } from './components/ListSection';
|
import { DataSourceErrorBoundary } from './components/DataSourceErrorBoundary';
|
||||||
import { DataSourceIcon } from './components/Namespace';
|
import { DataSourceSection } from './components/DataSourceSection';
|
||||||
import { LoadingIndicator } from './components/RuleGroup';
|
|
||||||
import { usePaginatedPrometheusRuleNamespaces } from './hooks/usePaginatedPrometheusRuleNamespaces';
|
|
||||||
|
|
||||||
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
const { useDiscoverDsFeaturesQuery } = featureDiscoveryApi;
|
||||||
const GROUP_PAGE_SIZE = 40;
|
|
||||||
|
|
||||||
export function GroupedView() {
|
export function GroupedView() {
|
||||||
const externalRuleSources = useMemo(() => getExternalRulesSources(), []);
|
const externalRuleSources = useMemo(() => getExternalRulesSources(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack direction="column" gap={1} role="list">
|
<Stack direction="column" gap={1} role="list">
|
||||||
<GrafanaDataSourceLoader />
|
<DataSourceErrorBoundary rulesSourceIdentifier={GrafanaRulesSource}>
|
||||||
|
<PaginatedGrafanaLoader />
|
||||||
|
</DataSourceErrorBoundary>
|
||||||
{externalRuleSources.map((ruleSource) => {
|
{externalRuleSources.map((ruleSource) => {
|
||||||
return <DataSourceLoader key={ruleSource.uid} uid={ruleSource.uid} name={ruleSource.name} />;
|
return <DataSourceLoader key={ruleSource.uid} rulesSourceIdentifier={ruleSource} />;
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DataSourceLoaderProps {
|
interface DataSourceLoaderProps {
|
||||||
name: string;
|
rulesSourceIdentifier: DataSourceRulesSourceIdentifier;
|
||||||
uid: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GrafanaDataSourceLoader() {
|
export function GrafanaDataSourceLoader() {
|
||||||
return <DataSourceSection name="Grafana" application="grafana" uid="grafana" isLoading={true} />;
|
return <DataSourceSection name="Grafana" application="grafana" uid="grafana" isLoading={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DataSourceLoader({ uid, name }: DataSourceLoaderProps) {
|
function DataSourceLoader({ rulesSourceIdentifier }: DataSourceLoaderProps) {
|
||||||
const { data: dataSourceInfo, isLoading } = useDiscoverDsFeaturesQuery({ uid });
|
const { data: dataSourceInfo, isLoading } = useDiscoverDsFeaturesQuery({ uid: rulesSourceIdentifier.uid });
|
||||||
|
|
||||||
|
const { uid, name } = rulesSourceIdentifier;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <DataSourceSection loader={<Skeleton width={250} height={16} />} uid={uid} name={name} />;
|
return <DataSourceSection loader={<Skeleton width={250} height={16} />} uid={uid} name={name} />;
|
||||||
@@ -56,223 +49,15 @@ export function DataSourceLoader({ uid, name }: DataSourceLoaderProps) {
|
|||||||
// 2. grab prometheus rule groups with max_groups if supported
|
// 2. grab prometheus rule groups with max_groups if supported
|
||||||
if (dataSourceInfo) {
|
if (dataSourceInfo) {
|
||||||
return (
|
return (
|
||||||
<PaginatedDataSourceLoader
|
<DataSourceErrorBoundary rulesSourceIdentifier={rulesSourceIdentifier}>
|
||||||
ruleSourceName={dataSourceInfo.name}
|
<PaginatedDataSourceLoader
|
||||||
uid={uid}
|
key={rulesSourceIdentifier.uid}
|
||||||
name={name}
|
rulesSourceIdentifier={rulesSourceIdentifier}
|
||||||
application={dataSourceInfo.application}
|
application={dataSourceInfo.application}
|
||||||
/>
|
/>
|
||||||
|
</DataSourceErrorBoundary>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Try to use a better rules source identifier
|
|
||||||
interface PaginatedDataSourceLoaderProps
|
|
||||||
extends Required<Pick<DataSourceSectionProps, 'application' | 'uid' | 'name'>> {
|
|
||||||
ruleSourceName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PaginatedDataSourceLoader({ ruleSourceName, name, uid, application }: PaginatedDataSourceLoaderProps) {
|
|
||||||
const {
|
|
||||||
page: ruleNamespaces,
|
|
||||||
nextPage,
|
|
||||||
previousPage,
|
|
||||||
canMoveForward,
|
|
||||||
canMoveBackward,
|
|
||||||
isLoading,
|
|
||||||
} = usePaginatedPrometheusRuleNamespaces(ruleSourceName, GROUP_PAGE_SIZE);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DataSourceSection name={name} application={application} uid={uid} isLoading={isLoading}>
|
|
||||||
<Stack direction="column" gap={1}>
|
|
||||||
{ruleNamespaces.map((namespace) => (
|
|
||||||
<ListSection
|
|
||||||
key={namespace.name}
|
|
||||||
title={
|
|
||||||
<Stack direction="row" gap={1} alignItems="center">
|
|
||||||
<Icon name="folder" />{' '}
|
|
||||||
<Text variant="body" element="h3">
|
|
||||||
{namespace.name}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{namespace.groups.map((group) => (
|
|
||||||
<RuleGroupListItem
|
|
||||||
key={`${ruleSourceName}-${namespace.name}-${group.name}`}
|
|
||||||
group={group}
|
|
||||||
ruleSourceName={ruleSourceName}
|
|
||||||
namespaceId={namespace}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ListSection>
|
|
||||||
))}
|
|
||||||
<LazyPagination
|
|
||||||
nextPage={nextPage}
|
|
||||||
previousPage={previousPage}
|
|
||||||
canMoveForward={canMoveForward}
|
|
||||||
canMoveBackward={canMoveBackward}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</DataSourceSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RuleGroupListItemProps {
|
|
||||||
group: RuleGroup;
|
|
||||||
ruleSourceName: string;
|
|
||||||
namespaceId: DataSourceNamespaceIdentifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
function RuleGroupListItem({ group, ruleSourceName, namespaceId }: RuleGroupListItemProps) {
|
|
||||||
const rulesWithGroupId = useMemo(
|
|
||||||
() =>
|
|
||||||
group.rules.map((rule) => {
|
|
||||||
const groupIdentifier: DataSourceRuleGroupIdentifier = {
|
|
||||||
rulesSource: { uid: getDatasourceAPIUid(ruleSourceName), name: ruleSourceName },
|
|
||||||
namespace: namespaceId,
|
|
||||||
groupName: group.name,
|
|
||||||
groupOrigin: 'datasource',
|
|
||||||
};
|
|
||||||
return { rule, groupIdentifier };
|
|
||||||
}),
|
|
||||||
[group, namespaceId, ruleSourceName]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListGroup
|
|
||||||
key={group.name}
|
|
||||||
name={group.name}
|
|
||||||
isOpen={false}
|
|
||||||
actions={
|
|
||||||
<>
|
|
||||||
<Dropdown
|
|
||||||
overlay={
|
|
||||||
<Menu>
|
|
||||||
<Menu.Item label="Edit" icon="pen" data-testid="edit-group-action" />
|
|
||||||
<Menu.Item label="Re-order rules" icon="flip" />
|
|
||||||
<Menu.Divider />
|
|
||||||
<Menu.Item label="Export" icon="download-alt" />
|
|
||||||
<Menu.Item label="Delete" icon="trash-alt" destructive />
|
|
||||||
</Menu>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton name="ellipsis-h" aria-label="rule group actions" />
|
|
||||||
</Dropdown>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{rulesWithGroupId.map(({ rule, groupIdentifier }) => (
|
|
||||||
<AlertRuleLoader key={hashRule(rule)} rule={rule} groupIdentifier={groupIdentifier} />
|
|
||||||
))}
|
|
||||||
</ListGroup>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DataSourceSectionProps extends PropsWithChildren {
|
|
||||||
uid: string;
|
|
||||||
name: string;
|
|
||||||
loader?: ReactNode;
|
|
||||||
application?: RulesSourceApplication;
|
|
||||||
isLoading?: boolean;
|
|
||||||
description?: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DataSourceSection = ({
|
|
||||||
uid,
|
|
||||||
name,
|
|
||||||
application,
|
|
||||||
children,
|
|
||||||
loader,
|
|
||||||
isLoading = false,
|
|
||||||
description = null,
|
|
||||||
}: DataSourceSectionProps) => {
|
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section aria-labelledby={`datasource-${uid}-heading`} role="listitem">
|
|
||||||
<Stack direction="column" gap={1}>
|
|
||||||
<Stack direction="column" gap={0}>
|
|
||||||
{isLoading && <LoadingIndicator datasourceUid={uid} />}
|
|
||||||
<div className={styles.dataSourceSectionTitle}>
|
|
||||||
{loader ?? (
|
|
||||||
<Stack alignItems="center">
|
|
||||||
{application && <DataSourceIcon application={application} />}
|
|
||||||
<Text variant="body" weight="bold" element="h2" id={`datasource-${uid}-heading`}>
|
|
||||||
{name}
|
|
||||||
</Text>
|
|
||||||
{description && (
|
|
||||||
<>
|
|
||||||
{'·'}
|
|
||||||
{description}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Spacer />
|
|
||||||
<WithReturnButton
|
|
||||||
title="alert rules"
|
|
||||||
component={
|
|
||||||
<LinkButton variant="secondary" size="sm" href={`/connections/datasources/edit/${uid}`}>
|
|
||||||
<Trans i18nKey="alerting.rule-list.configure-datasource">Configure</Trans>
|
|
||||||
</LinkButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</Stack>
|
|
||||||
<div className={styles.itemsWrapper}>{children}</div>
|
|
||||||
</Stack>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getStyles = (theme: GrafanaTheme2) => ({
|
|
||||||
itemsWrapper: css({
|
|
||||||
position: 'relative',
|
|
||||||
marginLeft: theme.spacing(1.5),
|
|
||||||
|
|
||||||
'&:before': {
|
|
||||||
content: "''",
|
|
||||||
position: 'absolute',
|
|
||||||
height: '100%',
|
|
||||||
|
|
||||||
marginLeft: `-${theme.spacing(1.5)}`,
|
|
||||||
borderLeft: `solid 1px ${theme.colors.border.weak}`,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
dataSourceSectionTitle: css({
|
|
||||||
background: theme.colors.background.secondary,
|
|
||||||
padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
|
|
||||||
|
|
||||||
border: `solid 1px ${theme.colors.border.weak}`,
|
|
||||||
borderRadius: theme.shape.radius.default,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
interface LazyPaginationProps {
|
|
||||||
canMoveForward: boolean;
|
|
||||||
canMoveBackward: boolean;
|
|
||||||
nextPage: () => void;
|
|
||||||
previousPage: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function LazyPagination({ canMoveForward, canMoveBackward, nextPage, previousPage }: LazyPaginationProps) {
|
|
||||||
return (
|
|
||||||
<Stack direction="row" gap={1}>
|
|
||||||
<Button
|
|
||||||
aria-label={`previous page`}
|
|
||||||
size="sm"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={previousPage}
|
|
||||||
disabled={!canMoveBackward}
|
|
||||||
>
|
|
||||||
<Icon name="angle-left" />
|
|
||||||
</Button>
|
|
||||||
<Button aria-label={`next page`} size="sm" variant="secondary" onClick={nextPage} disabled={!canMoveForward}>
|
|
||||||
<Icon name="angle-right" />
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { groupBy } from 'lodash';
|
||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { Icon, Stack, Text } from '@grafana/ui';
|
||||||
|
import { DataSourceRuleGroupIdentifier, DataSourceRulesSourceIdentifier, RuleGroup } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { hashRule } from '../utils/rule-id';
|
||||||
|
|
||||||
|
import { DataSourceRuleLoader } from './DataSourceRuleLoader';
|
||||||
|
import { DataSourceSection, DataSourceSectionProps } from './components/DataSourceSection';
|
||||||
|
import { LazyPagination } from './components/LazyPagination';
|
||||||
|
import { ListGroup } from './components/ListGroup';
|
||||||
|
import { ListSection } from './components/ListSection';
|
||||||
|
import { RuleGroupActionsMenu } from './components/RuleGroupActionsMenu';
|
||||||
|
import { usePrometheusGroupsGenerator } from './hooks/prometheusGroupsGenerator';
|
||||||
|
import { usePaginatedPrometheusGroups } from './hooks/usePaginatedPrometheusGroups';
|
||||||
|
|
||||||
|
const DATA_SOURCE_GROUP_PAGE_SIZE = 40;
|
||||||
|
|
||||||
|
interface PaginatedDataSourceLoaderProps extends Required<Pick<DataSourceSectionProps, 'application'>> {
|
||||||
|
rulesSourceIdentifier: DataSourceRulesSourceIdentifier;
|
||||||
|
}
|
||||||
|
export function PaginatedDataSourceLoader({ rulesSourceIdentifier, application }: PaginatedDataSourceLoaderProps) {
|
||||||
|
const { uid, name } = rulesSourceIdentifier;
|
||||||
|
const prometheusGroupsGenerator = usePrometheusGroupsGenerator();
|
||||||
|
|
||||||
|
const groupsGenerator = useRef(prometheusGroupsGenerator(rulesSourceIdentifier, DATA_SOURCE_GROUP_PAGE_SIZE));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentGenerator = groupsGenerator.current;
|
||||||
|
return () => {
|
||||||
|
currentGenerator.return();
|
||||||
|
};
|
||||||
|
}, [groupsGenerator]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
page: groupsPage,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
canMoveForward,
|
||||||
|
canMoveBackward,
|
||||||
|
isLoading,
|
||||||
|
} = usePaginatedPrometheusGroups(groupsGenerator.current, DATA_SOURCE_GROUP_PAGE_SIZE);
|
||||||
|
|
||||||
|
const groupsByNamespace = useMemo(() => groupBy(groupsPage, 'file'), [groupsPage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataSourceSection name={name} application={application} uid={uid} isLoading={isLoading}>
|
||||||
|
<Stack direction="column" gap={1}>
|
||||||
|
{Object.entries(groupsByNamespace).map(([namespace, groups]) => (
|
||||||
|
<ListSection
|
||||||
|
key={namespace}
|
||||||
|
title={
|
||||||
|
<Stack direction="row" gap={1} alignItems="center">
|
||||||
|
<Icon name="folder" />{' '}
|
||||||
|
<Text variant="body" element="h3">
|
||||||
|
{namespace}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<RuleGroupListItem
|
||||||
|
key={`${rulesSourceIdentifier.uid}-${namespace}-${group.name}`}
|
||||||
|
group={group}
|
||||||
|
rulesSourceIdentifier={rulesSourceIdentifier}
|
||||||
|
namespaceName={namespace}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ListSection>
|
||||||
|
))}
|
||||||
|
<LazyPagination
|
||||||
|
nextPage={nextPage}
|
||||||
|
previousPage={previousPage}
|
||||||
|
canMoveForward={canMoveForward}
|
||||||
|
canMoveBackward={canMoveBackward}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</DataSourceSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RuleGroupListItemProps {
|
||||||
|
group: RuleGroup;
|
||||||
|
rulesSourceIdentifier: DataSourceRulesSourceIdentifier;
|
||||||
|
namespaceName: string;
|
||||||
|
}
|
||||||
|
function RuleGroupListItem({ rulesSourceIdentifier, group, namespaceName }: RuleGroupListItemProps) {
|
||||||
|
const rulesWithGroupId = useMemo(() => {
|
||||||
|
return group.rules.map((rule) => {
|
||||||
|
const groupIdentifier: DataSourceRuleGroupIdentifier = {
|
||||||
|
rulesSource: rulesSourceIdentifier,
|
||||||
|
namespace: { name: namespaceName },
|
||||||
|
groupName: group.name,
|
||||||
|
groupOrigin: 'datasource',
|
||||||
|
};
|
||||||
|
return { rule, groupIdentifier };
|
||||||
|
});
|
||||||
|
}, [group, namespaceName, rulesSourceIdentifier]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ListGroup key={group.name} name={group.name} isOpen={false} actions={<RuleGroupActionsMenu />}>
|
||||||
|
{rulesWithGroupId.map(({ rule, groupIdentifier }) => (
|
||||||
|
<DataSourceRuleLoader key={hashRule(rule)} rule={rule} groupIdentifier={groupIdentifier} />
|
||||||
|
))}
|
||||||
|
</ListGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { groupBy } from 'lodash';
|
||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { Icon, Stack, Text } from '@grafana/ui';
|
||||||
|
import { GrafanaRulesSourceSymbol } from 'app/types/unified-alerting';
|
||||||
|
import { GrafanaPromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { GrafanaRuleLoader } from './GrafanaRuleLoader';
|
||||||
|
import { DataSourceSection } from './components/DataSourceSection';
|
||||||
|
import { LazyPagination } from './components/LazyPagination';
|
||||||
|
import { ListGroup } from './components/ListGroup';
|
||||||
|
import { ListSection } from './components/ListSection';
|
||||||
|
import { RuleGroupActionsMenu } from './components/RuleGroupActionsMenu';
|
||||||
|
import { useGrafanaGroupsGenerator } from './hooks/prometheusGroupsGenerator';
|
||||||
|
import { usePaginatedPrometheusGroups } from './hooks/usePaginatedPrometheusGroups';
|
||||||
|
|
||||||
|
const GRAFANA_GROUP_PAGE_SIZE = 40;
|
||||||
|
|
||||||
|
export function PaginatedGrafanaLoader() {
|
||||||
|
const grafanaGroupsGenerator = useGrafanaGroupsGenerator();
|
||||||
|
|
||||||
|
const groupsGenerator = useRef(grafanaGroupsGenerator(GRAFANA_GROUP_PAGE_SIZE));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentGenerator = groupsGenerator.current;
|
||||||
|
return () => {
|
||||||
|
currentGenerator.return();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const {
|
||||||
|
page: groupsPage,
|
||||||
|
nextPage,
|
||||||
|
previousPage,
|
||||||
|
canMoveForward,
|
||||||
|
canMoveBackward,
|
||||||
|
isLoading,
|
||||||
|
} = usePaginatedPrometheusGroups(groupsGenerator.current, GRAFANA_GROUP_PAGE_SIZE);
|
||||||
|
|
||||||
|
const groupsByFolder = useMemo(() => groupBy(groupsPage, 'folderUid'), [groupsPage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataSourceSection name="Grafana" application="grafana" uid={GrafanaRulesSourceSymbol} isLoading={isLoading}>
|
||||||
|
<Stack direction="column" gap={1}>
|
||||||
|
{Object.entries(groupsByFolder).map(([folderUid, groups]) => {
|
||||||
|
// Groups are grouped by folder, so we can use the first group to get the folder name
|
||||||
|
const folderName = groups[0].file;
|
||||||
|
return (
|
||||||
|
<ListSection
|
||||||
|
key={folderUid}
|
||||||
|
title={
|
||||||
|
<Stack direction="row" gap={1} alignItems="center">
|
||||||
|
<Icon name="folder" />{' '}
|
||||||
|
<Text variant="body" element="h3">
|
||||||
|
{folderName}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{groups.map((group) => (
|
||||||
|
<GrafanaRuleGroupListItem
|
||||||
|
key={`grafana-ns-${folderUid}-${group.name}`}
|
||||||
|
group={group}
|
||||||
|
namespaceName={folderName}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ListSection>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<LazyPagination
|
||||||
|
nextPage={nextPage}
|
||||||
|
previousPage={previousPage}
|
||||||
|
canMoveForward={canMoveForward}
|
||||||
|
canMoveBackward={canMoveBackward}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</DataSourceSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GrafanaRuleGroupListItemProps {
|
||||||
|
group: GrafanaPromRuleGroupDTO;
|
||||||
|
namespaceName: string;
|
||||||
|
}
|
||||||
|
export function GrafanaRuleGroupListItem({ group, namespaceName }: GrafanaRuleGroupListItemProps) {
|
||||||
|
return (
|
||||||
|
<ListGroup key={group.name} name={group.name} isOpen={false} actions={<RuleGroupActionsMenu />}>
|
||||||
|
{group.rules.map((rule) => {
|
||||||
|
return <GrafanaRuleLoader key={rule.uid} rule={rule} groupName={group.name} namespaceName={namespaceName} />;
|
||||||
|
})}
|
||||||
|
</ListGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import pluralize from 'pluralize';
|
import pluralize from 'pluralize';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useEffect } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Alert, Icon, Stack, Text, TextLink, Tooltip, useStyles2 } from '@grafana/ui';
|
import { Alert, Icon, Stack, Text, TextLink, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
@@ -266,15 +266,17 @@ interface UnknownRuleListItemProps {
|
|||||||
|
|
||||||
export const UnknownRuleListItem = ({ rule, groupIdentifier }: UnknownRuleListItemProps) => {
|
export const UnknownRuleListItem = ({ rule, groupIdentifier }: UnknownRuleListItemProps) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const { rulesSource, namespace, groupName } = groupIdentifier;
|
|
||||||
|
|
||||||
const ruleContext = {
|
useEffect(() => {
|
||||||
name: rule.name,
|
const { rulesSource, namespace, groupName } = groupIdentifier;
|
||||||
groupName,
|
const ruleContext = {
|
||||||
namespace: JSON.stringify(namespace),
|
name: rule.name,
|
||||||
rulesSource: rulesSource.uid === GrafanaRulesSourceSymbol ? GRAFANA_RULES_SOURCE_NAME : rulesSource.uid,
|
groupName,
|
||||||
};
|
namespace: JSON.stringify(namespace),
|
||||||
logError(new Error('unknown rule type'), ruleContext);
|
rulesSource: rulesSource.uid === GrafanaRulesSourceSymbol ? GRAFANA_RULES_SOURCE_NAME : rulesSource.uid,
|
||||||
|
};
|
||||||
|
logError(new Error('unknown rule type'), ruleContext);
|
||||||
|
}, [rule, groupIdentifier]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert title={'Unknown rule type'} className={styles.resetMargin}>
|
<Alert title={'Unknown rule type'} className={styles.resetMargin}>
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Alert, ErrorBoundary, ErrorWithStack, Text } from '@grafana/ui';
|
||||||
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
|
import { RulesSourceIdentifier } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { DataSourceSection } from './DataSourceSection';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some more exotic Prometheus data sources might not be 100% compatible with Prometheus API
|
||||||
|
* We don't want them to break the whole page, so we wrap them in an error boundary
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function DataSourceErrorBoundary({
|
||||||
|
children,
|
||||||
|
rulesSourceIdentifier,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
rulesSourceIdentifier: RulesSourceIdentifier;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
{({ error, errorInfo }) => {
|
||||||
|
if (error || errorInfo) {
|
||||||
|
const { uid, name } = rulesSourceIdentifier;
|
||||||
|
return (
|
||||||
|
<DataSourceSection uid={uid} name={name}>
|
||||||
|
<Alert
|
||||||
|
title={t('alerting.rule-list.ds-error-boundary.title', 'Unable to load rules from this data source')}
|
||||||
|
>
|
||||||
|
<Text>
|
||||||
|
<Trans i18nKey="alerting.rule-list.ds-error-boundary.description">
|
||||||
|
Check the data source configuration. Does the data source support Prometheus API?
|
||||||
|
</Trans>
|
||||||
|
</Text>
|
||||||
|
<ErrorWithStack
|
||||||
|
error={error}
|
||||||
|
errorInfo={errorInfo}
|
||||||
|
title={t('alerting.rule-list.ds-error-boundary.title', 'Unable to load rules from this data source')}
|
||||||
|
/>
|
||||||
|
</Alert>
|
||||||
|
</DataSourceSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return children;
|
||||||
|
}}
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { PropsWithChildren, ReactNode } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { LinkButton, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||||
|
import { Trans, t } from 'app/core/internationalization';
|
||||||
|
import { RulesSourceIdentifier } from 'app/types/unified-alerting';
|
||||||
|
import { RulesSourceApplication } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { Spacer } from '../../components/Spacer';
|
||||||
|
import { WithReturnButton } from '../../components/WithReturnButton';
|
||||||
|
|
||||||
|
import { DataSourceIcon } from './Namespace';
|
||||||
|
import { LoadingIndicator } from './RuleGroup';
|
||||||
|
|
||||||
|
export interface DataSourceSectionProps extends PropsWithChildren {
|
||||||
|
uid: RulesSourceIdentifier['uid'];
|
||||||
|
name: string;
|
||||||
|
loader?: ReactNode;
|
||||||
|
application?: RulesSourceApplication;
|
||||||
|
isLoading?: boolean;
|
||||||
|
description?: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DataSourceSection = ({
|
||||||
|
uid,
|
||||||
|
name,
|
||||||
|
application,
|
||||||
|
children,
|
||||||
|
loader,
|
||||||
|
isLoading = false,
|
||||||
|
description = null,
|
||||||
|
}: DataSourceSectionProps) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section aria-labelledby={`datasource-${String(uid)}-heading`} role="listitem">
|
||||||
|
<Stack direction="column" gap={1}>
|
||||||
|
<Stack direction="column" gap={0}>
|
||||||
|
{isLoading && <LoadingIndicator datasourceUid={String(uid)} />}
|
||||||
|
<div className={styles.dataSourceSectionTitle}>
|
||||||
|
{loader ?? (
|
||||||
|
<Stack alignItems="center">
|
||||||
|
{application && <DataSourceIcon application={application} />}
|
||||||
|
<Text variant="body" weight="bold" element="h2" id={`datasource-${String(uid)}-heading`}>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
{description && (
|
||||||
|
<>
|
||||||
|
{'·'}
|
||||||
|
{description}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Spacer />
|
||||||
|
<WithReturnButton
|
||||||
|
title={t('alerting.rule-list.return-button.title', 'Alert rules')}
|
||||||
|
component={
|
||||||
|
<LinkButton variant="secondary" size="sm" href={`/connections/datasources/edit/${String(uid)}`}>
|
||||||
|
<Trans i18nKey="alerting.rule-list.configure-datasource">Configure</Trans>
|
||||||
|
</LinkButton>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
<div className={styles.itemsWrapper}>{children}</div>
|
||||||
|
</Stack>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
itemsWrapper: css({
|
||||||
|
position: 'relative',
|
||||||
|
marginLeft: theme.spacing(1.5),
|
||||||
|
|
||||||
|
'&:before': {
|
||||||
|
content: "''",
|
||||||
|
position: 'absolute',
|
||||||
|
height: '100%',
|
||||||
|
|
||||||
|
marginLeft: `-${theme.spacing(1.5)}`,
|
||||||
|
borderLeft: `solid 1px ${theme.colors.border.weak}`,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
dataSourceSectionTitle: css({
|
||||||
|
background: theme.colors.background.secondary,
|
||||||
|
padding: `${theme.spacing(1)} ${theme.spacing(1.5)}`,
|
||||||
|
|
||||||
|
border: `solid 1px ${theme.colors.border.weak}`,
|
||||||
|
borderRadius: theme.shape.radius.default,
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Button, Icon, Stack } from '@grafana/ui';
|
||||||
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
|
interface LazyPaginationProps {
|
||||||
|
canMoveForward: boolean;
|
||||||
|
canMoveBackward: boolean;
|
||||||
|
nextPage: () => void;
|
||||||
|
previousPage: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LazyPagination({ canMoveForward, canMoveBackward, nextPage, previousPage }: LazyPaginationProps) {
|
||||||
|
return (
|
||||||
|
<Stack direction="row" gap={1}>
|
||||||
|
<Button
|
||||||
|
aria-label={t('alerting.rule-list.pagination.previous-page', 'previous page')}
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={previousPage}
|
||||||
|
disabled={!canMoveBackward}
|
||||||
|
>
|
||||||
|
<Icon name="angle-left" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
aria-label={t('alerting.rule-list.pagination.next-page', 'next page')}
|
||||||
|
size="sm"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={nextPage}
|
||||||
|
disabled={!canMoveForward}
|
||||||
|
>
|
||||||
|
<Icon name="angle-right" />
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Dropdown, IconButton, Menu } from '@grafana/ui';
|
||||||
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
|
export function RuleGroupActionsMenu() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dropdown
|
||||||
|
overlay={
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item label={t('alerting.group-actions.edit', 'Edit')} icon="pen" data-testid="edit-group-action" />
|
||||||
|
<Menu.Item label={t('alerting.group-actions.reorder', 'Re-order rules')} icon="flip" />
|
||||||
|
<Menu.Divider />
|
||||||
|
<Menu.Item label={t('alerting.group-actions.export', 'Export')} icon="download-alt" />
|
||||||
|
<Menu.Item label={t('alerting.group-actions.delete', 'Delete')} icon="trash-alt" destructive />
|
||||||
|
</Menu>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton name="ellipsis-h" aria-label={t('alerting.group-actions.actions-trigger', 'Rule group actions')} />
|
||||||
|
</Dropdown>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { BaseQueryFn } from '@reduxjs/toolkit/query';
|
||||||
|
import { TypedLazyQueryTrigger } from '@reduxjs/toolkit/query/react';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { DataSourceRulesSourceIdentifier } from 'app/types/unified-alerting';
|
||||||
|
|
||||||
|
import { BaseQueryFnArgs } from '../../api/alertingApi';
|
||||||
|
import { PromRulesResponse, prometheusApi } from '../../api/prometheusApi';
|
||||||
|
|
||||||
|
const { useLazyGetGroupsQuery, useLazyGetGrafanaGroupsQuery } = prometheusApi;
|
||||||
|
|
||||||
|
interface FetchGroupsOptions {
|
||||||
|
groupLimit?: number;
|
||||||
|
groupNextToken?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePrometheusGroupsGenerator() {
|
||||||
|
const [getGroups] = useLazyGetGroupsQuery();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async function* (ruleSource: DataSourceRulesSourceIdentifier, groupLimit: number) {
|
||||||
|
const getRuleSourceGroups = (options: FetchGroupsOptions) =>
|
||||||
|
getGroups({ ruleSource: { uid: ruleSource.uid }, ...options });
|
||||||
|
|
||||||
|
yield* genericGroupsGenerator(getRuleSourceGroups, groupLimit);
|
||||||
|
},
|
||||||
|
[getGroups]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGrafanaGroupsGenerator() {
|
||||||
|
const [getGrafanaGroups] = useLazyGetGrafanaGroupsQuery();
|
||||||
|
|
||||||
|
return useCallback(
|
||||||
|
async function* (groupLimit: number) {
|
||||||
|
yield* genericGroupsGenerator(getGrafanaGroups, groupLimit);
|
||||||
|
},
|
||||||
|
[getGrafanaGroups]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generator lazily provides groups one by one only when needed
|
||||||
|
// This might look a bit complex but it allows us to have one API for paginated and non-paginated Prometheus data sources
|
||||||
|
// For unpaginated data sources we fetch everything in one go
|
||||||
|
// For paginated we fetch the next page when needed
|
||||||
|
async function* genericGroupsGenerator<TGroup>(
|
||||||
|
fetchGroups: TypedLazyQueryTrigger<PromRulesResponse<TGroup>, FetchGroupsOptions, BaseQueryFn<BaseQueryFnArgs>>,
|
||||||
|
groupLimit: number
|
||||||
|
) {
|
||||||
|
const response = await fetchGroups({ groupLimit });
|
||||||
|
|
||||||
|
if (!response.isSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.data) {
|
||||||
|
yield* response.data.data.groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastToken: string | undefined = undefined;
|
||||||
|
if (response.data?.data?.groupNextToken) {
|
||||||
|
lastToken = response.data.data.groupNextToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (lastToken) {
|
||||||
|
const response = await fetchGroups({
|
||||||
|
groupNextToken: lastToken,
|
||||||
|
groupLimit: groupLimit,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.isSuccess) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.data?.data) {
|
||||||
|
yield* response.data.data.groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastToken = response.data?.data?.groupNextToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,97 +1,95 @@
|
|||||||
|
import { AsyncIterableX, from } from 'ix/asynciterable/index';
|
||||||
import { merge } from 'ix/asynciterable/merge';
|
import { merge } from 'ix/asynciterable/merge';
|
||||||
import { filter, flatMap, map } from 'ix/asynciterable/operators';
|
import { filter, flatMap, map } from 'ix/asynciterable/operators';
|
||||||
import { compact } from 'lodash';
|
import { compact } from 'lodash';
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
|
import { Matcher } from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { DataSourceRuleGroupIdentifier, ExternalRulesSourceIdentifier } from 'app/types/unified-alerting';
|
import {
|
||||||
import { PromRuleDTO, PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
DataSourceRuleGroupIdentifier,
|
||||||
|
DataSourceRulesSourceIdentifier,
|
||||||
|
GrafanaRuleGroupIdentifier,
|
||||||
|
} from 'app/types/unified-alerting';
|
||||||
|
import {
|
||||||
|
GrafanaPromRuleDTO,
|
||||||
|
GrafanaPromRuleGroupDTO,
|
||||||
|
PromRuleDTO,
|
||||||
|
PromRuleGroupDTO,
|
||||||
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { prometheusApi } from '../../api/prometheusApi';
|
|
||||||
import { RulesFilter } from '../../search/rulesSearchParser';
|
import { RulesFilter } from '../../search/rulesSearchParser';
|
||||||
import { labelsMatchMatchers } from '../../utils/alertmanager';
|
import { labelsMatchMatchers } from '../../utils/alertmanager';
|
||||||
import { Annotation } from '../../utils/constants';
|
import { Annotation } from '../../utils/constants';
|
||||||
import { getDatasourceAPIUid, getExternalRulesSources } from '../../utils/datasource';
|
import { GrafanaRulesSource, getDatasourceAPIUid, getExternalRulesSources } from '../../utils/datasource';
|
||||||
import { parseMatcher } from '../../utils/matchers';
|
import { parseMatcher } from '../../utils/matchers';
|
||||||
import { isAlertingRule } from '../../utils/rules';
|
import { isAlertingRule } from '../../utils/rules';
|
||||||
|
|
||||||
export interface RuleWithOrigin {
|
import { useGrafanaGroupsGenerator, usePrometheusGroupsGenerator } from './prometheusGroupsGenerator';
|
||||||
rule: PromRuleDTO;
|
|
||||||
groupIdentifier: DataSourceRuleGroupIdentifier;
|
export type RuleWithOrigin = PromRuleWithOrigin | GrafanaRuleWithOrigin;
|
||||||
|
|
||||||
|
export interface GrafanaRuleWithOrigin {
|
||||||
|
rule: GrafanaPromRuleDTO;
|
||||||
|
groupIdentifier: GrafanaRuleGroupIdentifier;
|
||||||
|
/**
|
||||||
|
* The name of the namespace that contains the rule group
|
||||||
|
* groupIdentifier contains the uid of the namespace, but not the user-friendly display name
|
||||||
|
*/
|
||||||
|
namespaceName: string;
|
||||||
|
origin: 'grafana';
|
||||||
}
|
}
|
||||||
|
|
||||||
const { useLazyGroupsQuery } = prometheusApi;
|
export interface PromRuleWithOrigin {
|
||||||
|
rule: PromRuleDTO;
|
||||||
|
groupIdentifier: DataSourceRuleGroupIdentifier;
|
||||||
|
origin: 'datasource';
|
||||||
|
}
|
||||||
|
|
||||||
export function useFilteredRulesIteratorProvider() {
|
export function useFilteredRulesIteratorProvider() {
|
||||||
const [fetchGroups] = useLazyGroupsQuery();
|
|
||||||
const allExternalRulesSources = getExternalRulesSources();
|
const allExternalRulesSources = getExternalRulesSources();
|
||||||
|
|
||||||
/**
|
const prometheusGroupsGenerator = usePrometheusGroupsGenerator();
|
||||||
* This async generator will continue to yield rule groups and will keep fetching backend pages as long as the consumer
|
const grafanaGroupsGenerator = useGrafanaGroupsGenerator();
|
||||||
* is iterating.
|
|
||||||
*/
|
|
||||||
const fetchRuleSourceGroups = useCallback(
|
|
||||||
async function* (ruleSource: ExternalRulesSourceIdentifier, maxGroups: number) {
|
|
||||||
const response = await fetchGroups({ ruleSource: { uid: ruleSource.uid }, groupLimit: maxGroups });
|
|
||||||
|
|
||||||
if (!response.isSuccess) {
|
const getFilteredRulesIterator = (filterState: RulesFilter, groupLimit: number): AsyncIterableX<RuleWithOrigin> => {
|
||||||
return;
|
const normalizedFilterState = normalizeFilterState(filterState);
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data?.data) {
|
|
||||||
yield* response.data.data.groups.map((group) => [ruleSource, group] as const);
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastToken: string | undefined = undefined;
|
|
||||||
if (response.data?.data?.groupNextToken) {
|
|
||||||
lastToken = response.data.data.groupNextToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (lastToken) {
|
|
||||||
const response = await fetchGroups({
|
|
||||||
ruleSource: { uid: ruleSource.uid },
|
|
||||||
groupNextToken: lastToken,
|
|
||||||
groupLimit: maxGroups,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.isSuccess) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data?.data) {
|
|
||||||
yield* response.data.data.groups.map((group) => [ruleSource, group] as const);
|
|
||||||
}
|
|
||||||
|
|
||||||
lastToken = response.data?.data?.groupNextToken;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fetchGroups]
|
|
||||||
);
|
|
||||||
|
|
||||||
const getFilteredRulesIterator = (filterState: RulesFilter, groupLimit: number) => {
|
|
||||||
const ruleSourcesToFetchFrom = filterState.dataSourceNames.length
|
const ruleSourcesToFetchFrom = filterState.dataSourceNames.length
|
||||||
? filterState.dataSourceNames.map((ds) => ({ name: ds, uid: getDatasourceAPIUid(ds) }))
|
? filterState.dataSourceNames.map<DataSourceRulesSourceIdentifier>((ds) => ({
|
||||||
|
name: ds,
|
||||||
|
uid: getDatasourceAPIUid(ds),
|
||||||
|
ruleSourceType: 'datasource',
|
||||||
|
}))
|
||||||
: allExternalRulesSources;
|
: allExternalRulesSources;
|
||||||
|
|
||||||
// This split into the first one and the rest is only for compatibility with the merge function from ix
|
const grafanaIterator = from(grafanaGroupsGenerator(groupLimit)).pipe(
|
||||||
const [source, ...iterables] = ruleSourcesToFetchFrom.map((ds) => fetchRuleSourceGroups(ds, groupLimit));
|
filter((group) => groupFilter(group, normalizedFilterState)),
|
||||||
|
flatMap((group) => group.rules.map((rule) => [group, rule] as const)),
|
||||||
|
filter(([_, rule]) => ruleFilter(rule, normalizedFilterState)),
|
||||||
|
map(([group, rule]) => mapGrafanaRuleToRuleWithOrigin(group, rule))
|
||||||
|
);
|
||||||
|
|
||||||
return merge(source, ...iterables).pipe(
|
const [source, ...iterables] = ruleSourcesToFetchFrom.map((ds) => {
|
||||||
filter(([_, group]) => groupFilter(group, filterState)),
|
return from(prometheusGroupsGenerator(ds, groupLimit)).pipe(map((group) => [ds, group] as const));
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataSourcesIterator = merge(source, ...iterables).pipe(
|
||||||
|
filter(([_, group]) => groupFilter(group, normalizedFilterState)),
|
||||||
flatMap(([rulesSource, group]) => group.rules.map((rule) => [rulesSource, group, rule] as const)),
|
flatMap(([rulesSource, group]) => group.rules.map((rule) => [rulesSource, group, rule] as const)),
|
||||||
filter(([_, __, rule]) => ruleFilter(rule, filterState)),
|
filter(([_, __, rule]) => ruleFilter(rule, filterState)),
|
||||||
map(([rulesSource, group, rule]) => mapRuleToRuleWithOrigin(rulesSource, group, rule))
|
map(([rulesSource, group, rule]) => mapRuleToRuleWithOrigin(rulesSource, group, rule))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return merge(grafanaIterator, dataSourcesIterator);
|
||||||
};
|
};
|
||||||
|
|
||||||
return { getFilteredRulesIterator };
|
return { getFilteredRulesIterator };
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRuleToRuleWithOrigin(
|
function mapRuleToRuleWithOrigin(
|
||||||
rulesSource: ExternalRulesSourceIdentifier,
|
rulesSource: DataSourceRulesSourceIdentifier,
|
||||||
group: PromRuleGroupDTO,
|
group: PromRuleGroupDTO,
|
||||||
rule: PromRuleDTO
|
rule: PromRuleDTO
|
||||||
): RuleWithOrigin {
|
): PromRuleWithOrigin {
|
||||||
return {
|
return {
|
||||||
rule,
|
rule,
|
||||||
groupIdentifier: {
|
groupIdentifier: {
|
||||||
@@ -100,6 +98,24 @@ function mapRuleToRuleWithOrigin(
|
|||||||
groupName: group.name,
|
groupName: group.name,
|
||||||
groupOrigin: 'datasource',
|
groupOrigin: 'datasource',
|
||||||
},
|
},
|
||||||
|
origin: 'datasource',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapGrafanaRuleToRuleWithOrigin(
|
||||||
|
group: GrafanaPromRuleGroupDTO,
|
||||||
|
rule: GrafanaPromRuleDTO
|
||||||
|
): GrafanaRuleWithOrigin {
|
||||||
|
return {
|
||||||
|
rule,
|
||||||
|
groupIdentifier: {
|
||||||
|
rulesSource: GrafanaRulesSource,
|
||||||
|
namespace: { uid: group.folderUid },
|
||||||
|
groupName: group.name,
|
||||||
|
groupOrigin: 'grafana',
|
||||||
|
},
|
||||||
|
namespaceName: group.file,
|
||||||
|
origin: 'grafana',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,11 +127,11 @@ function groupFilter(group: PromRuleGroupDTO, filterState: RulesFilter): boolean
|
|||||||
const { name, file } = group;
|
const { name, file } = group;
|
||||||
|
|
||||||
// TODO Add fuzzy filtering or not
|
// TODO Add fuzzy filtering or not
|
||||||
if (filterState.namespace && !file.includes(filterState.namespace)) {
|
if (filterState.namespace && !file.toLowerCase().includes(filterState.namespace)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterState.groupName && !name.includes(filterState.groupName)) {
|
if (filterState.groupName && !name.toLowerCase().includes(filterState.groupName)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,11 +141,13 @@ function groupFilter(group: PromRuleGroupDTO, filterState: RulesFilter): boolean
|
|||||||
function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter) {
|
function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter) {
|
||||||
const { name, labels = {}, health, type } = rule;
|
const { name, labels = {}, health, type } = rule;
|
||||||
|
|
||||||
if (filterState.freeFormWords.length > 0 && !filterState.freeFormWords.some((word) => name.includes(word))) {
|
const nameLower = name.toLowerCase();
|
||||||
|
|
||||||
|
if (filterState.freeFormWords.length > 0 && !filterState.freeFormWords.some((word) => nameLower.includes(word))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterState.ruleName && !name.includes(filterState.ruleName)) {
|
if (filterState.ruleName && !nameLower.includes(filterState.ruleName)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +183,19 @@ function ruleFilter(rule: PromRuleDTO, filterState: RulesFilter) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lowercase free form words, rule name, group name and namespace
|
||||||
|
*/
|
||||||
|
function normalizeFilterState(filterState: RulesFilter): RulesFilter {
|
||||||
|
return {
|
||||||
|
...filterState,
|
||||||
|
freeFormWords: filterState.freeFormWords.map((word) => word.toLowerCase()),
|
||||||
|
ruleName: filterState.ruleName?.toLowerCase(),
|
||||||
|
groupName: filterState.groupName?.toLowerCase(),
|
||||||
|
namespace: filterState.namespace?.toLowerCase(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function looseParseMatcher(matcherQuery: string): Matcher | undefined {
|
function looseParseMatcher(matcherQuery: string): Matcher | undefined {
|
||||||
try {
|
try {
|
||||||
return parseMatcher(matcherQuery);
|
return parseMatcher(matcherQuery);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
|
import { isLoading, useAsync } from '../../hooks/useAsync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides pagination functionality for rule groups with lazy loading.
|
||||||
|
* Instead of loading all groups at once, it uses a generator to fetch them in batches as needed,
|
||||||
|
* which helps with performance when dealing with large numbers of rules.
|
||||||
|
*
|
||||||
|
* @param groupsGenerator - An async generator that yields rule groups in batches
|
||||||
|
* @param pageSize - Number of groups to display per page
|
||||||
|
* @returns Pagination state and controls for navigating through rule groups
|
||||||
|
*/
|
||||||
|
export function usePaginatedPrometheusGroups<TGroup extends PromRuleGroupDTO>(
|
||||||
|
groupsGenerator: AsyncGenerator<TGroup, void, unknown>,
|
||||||
|
pageSize: number
|
||||||
|
) {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [groups, setGroups] = useState<TGroup[]>([]);
|
||||||
|
const [lastPage, setLastPage] = useState<number | undefined>(undefined);
|
||||||
|
|
||||||
|
const [{ execute: fetchMoreGroups }, groupsRequestState] = useAsync(async (groupsCount: number) => {
|
||||||
|
let done = false;
|
||||||
|
const currentGroups: TGroup[] = [];
|
||||||
|
|
||||||
|
while (currentGroups.length < groupsCount) {
|
||||||
|
const generatorResult = await groupsGenerator.next();
|
||||||
|
if (generatorResult.done) {
|
||||||
|
done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const group = generatorResult.value;
|
||||||
|
currentGroups.push(group);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (done) {
|
||||||
|
const groupsTotal = groups.length + currentGroups.length;
|
||||||
|
setLastPage(Math.ceil(groupsTotal / pageSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
setGroups((groups) => [...groups, ...currentGroups]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// lastPage could be computed from groups.length and pageSize
|
||||||
|
const fetchInProgress = isLoading(groupsRequestState);
|
||||||
|
const canMoveForward = !fetchInProgress && (!lastPage || currentPage < lastPage);
|
||||||
|
// When going backward we already have the groups loaded, so no need to check if fetchInProgress
|
||||||
|
const canMoveBackward = currentPage > 1;
|
||||||
|
|
||||||
|
const nextPage = useCallback(async () => {
|
||||||
|
if (canMoveForward) {
|
||||||
|
setCurrentPage((page) => page + 1);
|
||||||
|
}
|
||||||
|
}, [canMoveForward]);
|
||||||
|
|
||||||
|
const previousPage = useCallback(async () => {
|
||||||
|
if (canMoveBackward) {
|
||||||
|
setCurrentPage((page) => page - 1);
|
||||||
|
}
|
||||||
|
}, [canMoveBackward]);
|
||||||
|
|
||||||
|
// groups.length - pageSize to have one more page loaded to prevent flickering with loading state
|
||||||
|
// lastPage === undefined because 0 is falsy but a value which should stop fetching (e.g for broken data sources)
|
||||||
|
const shouldFetchNextPage = groups.length - pageSize < pageSize * currentPage && lastPage === undefined;
|
||||||
|
|
||||||
|
if (shouldFetchNextPage && !fetchInProgress) {
|
||||||
|
fetchMoreGroups(pageSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupsPage = useMemo(() => {
|
||||||
|
return groups.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||||
|
}, [groups, currentPage, pageSize]);
|
||||||
|
|
||||||
|
return { isLoading: fetchInProgress, page: groupsPage, nextPage, previousPage, canMoveForward, canMoveBackward };
|
||||||
|
}
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
||||||
import { usePrevious } from 'react-use';
|
|
||||||
|
|
||||||
import { PromRuleGroupDTO } from 'app/types/unified-alerting-dto';
|
|
||||||
|
|
||||||
import { groupRulesByFileName } from '../../api/prometheus';
|
|
||||||
import { prometheusApi } from '../../api/prometheusApi';
|
|
||||||
import { isLoading, useAsync } from '../../hooks/useAsync';
|
|
||||||
import { getDatasourceAPIUid } from '../../utils/datasource';
|
|
||||||
|
|
||||||
const { useLazyGroupsQuery } = prometheusApi;
|
|
||||||
|
|
||||||
export function usePaginatedPrometheusRuleNamespaces(ruleSourceName: string, pageSize: number) {
|
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
|
||||||
const [groups, setGroups] = useState<PromRuleGroupDTO[]>([]);
|
|
||||||
const [lastPage, setLastPage] = useState<number | undefined>(undefined);
|
|
||||||
|
|
||||||
const { groupsGenerator } = usePrometheusGroupsGenerator(ruleSourceName, pageSize);
|
|
||||||
|
|
||||||
const [{ execute: fetchMoreGroups }, groupsRequestState] = useAsync(async (groupsCount: number) => {
|
|
||||||
let done = false;
|
|
||||||
const currentGroups: PromRuleGroupDTO[] = [];
|
|
||||||
|
|
||||||
while (currentGroups.length < groupsCount) {
|
|
||||||
const group = await groupsGenerator.next();
|
|
||||||
if (group.done) {
|
|
||||||
done = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentGroups.push(group.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (done) {
|
|
||||||
const groupsTotal = groups.length + currentGroups.length;
|
|
||||||
setLastPage(Math.ceil(groupsTotal / pageSize));
|
|
||||||
}
|
|
||||||
|
|
||||||
setGroups((groups) => [...groups, ...currentGroups]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const fetchInProgress = isLoading(groupsRequestState);
|
|
||||||
const canMoveForward = !fetchInProgress && (!lastPage || currentPage < lastPage);
|
|
||||||
const canMoveBackward = currentPage > 1 && !fetchInProgress;
|
|
||||||
|
|
||||||
const nextPage = useCallback(async () => {
|
|
||||||
if (canMoveForward) {
|
|
||||||
setCurrentPage((page) => page + 1);
|
|
||||||
}
|
|
||||||
}, [canMoveForward]);
|
|
||||||
|
|
||||||
const previousPage = useCallback(async () => {
|
|
||||||
if (canMoveBackward) {
|
|
||||||
setCurrentPage((page) => page - 1);
|
|
||||||
}
|
|
||||||
}, [canMoveBackward]);
|
|
||||||
|
|
||||||
// groups.length - pageSize to have one more page loaded to prevent flickering with loading state
|
|
||||||
// lastPage === undefined because 0 is falsy but a value which should stop fetching (e.g for broken data sources)
|
|
||||||
const shouldFetchNextPage = groups.length - pageSize < pageSize * currentPage && lastPage === undefined;
|
|
||||||
|
|
||||||
if (shouldFetchNextPage && !fetchInProgress) {
|
|
||||||
fetchMoreGroups(pageSize);
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageNamespaces = useMemo(() => {
|
|
||||||
const pageGroups = groups.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
|
||||||
// groupRulesByFileName mutates the array and RTKQ query freezes the response data
|
|
||||||
return groupRulesByFileName(structuredClone(pageGroups), ruleSourceName);
|
|
||||||
}, [groups, ruleSourceName, currentPage, pageSize]);
|
|
||||||
|
|
||||||
return { isLoading: fetchInProgress, page: pageNamespaces, nextPage, previousPage, canMoveForward, canMoveBackward };
|
|
||||||
}
|
|
||||||
|
|
||||||
function usePrometheusGroupsGenerator(ruleSourceName: string, pageSize: number) {
|
|
||||||
const [fetchGroups, { isLoading }] = useLazyGroupsQuery();
|
|
||||||
|
|
||||||
const prevRuleSourceName = usePrevious(ruleSourceName);
|
|
||||||
// Generator lazily provides groups one by one only when needed
|
|
||||||
// This might look a bit complex but it allows us to have one API for paginated and non-paginated Prometheus data sources
|
|
||||||
// For unpaginated data sources we just fetch everything in one go
|
|
||||||
// For paginated we fetch the next page when needed
|
|
||||||
const getGroups = useCallback(
|
|
||||||
async function* (ruleSourceName: string, maxGroups: number) {
|
|
||||||
const ruleSourceUid = getDatasourceAPIUid(ruleSourceName);
|
|
||||||
|
|
||||||
const response = await fetchGroups({
|
|
||||||
ruleSource: { uid: ruleSourceUid },
|
|
||||||
groupLimit: maxGroups,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.isSuccess) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data?.data) {
|
|
||||||
yield* response.data.data.groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
let lastToken: string | undefined = undefined;
|
|
||||||
if (response.data?.data?.groupNextToken) {
|
|
||||||
lastToken = response.data.data.groupNextToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
while (lastToken) {
|
|
||||||
const response = await fetchGroups({
|
|
||||||
ruleSource: { uid: ruleSourceUid },
|
|
||||||
groupNextToken: lastToken,
|
|
||||||
groupLimit: maxGroups,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.isSuccess) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.data?.data) {
|
|
||||||
yield* response.data.data.groups;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastToken = response.data?.data?.groupNextToken;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fetchGroups]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [groupsGenerator, setGroupsGenerator] = useState<AsyncGenerator<PromRuleGroupDTO, void, unknown>>(
|
|
||||||
getGroups(ruleSourceName, pageSize)
|
|
||||||
);
|
|
||||||
|
|
||||||
const resetGenerator = useCallback(() => {
|
|
||||||
setGroupsGenerator(getGroups(ruleSourceName, pageSize));
|
|
||||||
}, [ruleSourceName, getGroups, pageSize]);
|
|
||||||
|
|
||||||
if (prevRuleSourceName && prevRuleSourceName !== ruleSourceName) {
|
|
||||||
resetGenerator();
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const currentGenerator = groupsGenerator;
|
|
||||||
return () => {
|
|
||||||
currentGenerator.return();
|
|
||||||
};
|
|
||||||
}, [groupsGenerator]);
|
|
||||||
|
|
||||||
return { groupsGenerator, isLoading };
|
|
||||||
}
|
|
||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
} from 'app/plugins/datasource/alertmanager/types';
|
} from 'app/plugins/datasource/alertmanager/types';
|
||||||
import { AccessControlAction } from 'app/types';
|
import { AccessControlAction } from 'app/types';
|
||||||
import {
|
import {
|
||||||
ExternalRulesSourceIdentifier,
|
DataSourceRulesSourceIdentifier as DataSourceRulesSourceIdentifier,
|
||||||
|
GrafanaRulesSourceIdentifier,
|
||||||
GrafanaRulesSourceSymbol,
|
GrafanaRulesSourceSymbol,
|
||||||
RulesSource,
|
RulesSource,
|
||||||
RulesSourceUid,
|
RulesSourceUid,
|
||||||
@@ -28,6 +29,12 @@ import { getAllDataSources } from './config';
|
|||||||
export const GRAFANA_RULES_SOURCE_NAME = 'grafana';
|
export const GRAFANA_RULES_SOURCE_NAME = 'grafana';
|
||||||
export const GRAFANA_DATASOURCE_NAME = '-- Grafana --';
|
export const GRAFANA_DATASOURCE_NAME = '-- Grafana --';
|
||||||
|
|
||||||
|
export const GrafanaRulesSource: GrafanaRulesSourceIdentifier = {
|
||||||
|
uid: GrafanaRulesSourceSymbol,
|
||||||
|
name: GRAFANA_RULES_SOURCE_NAME,
|
||||||
|
ruleSourceType: 'grafana',
|
||||||
|
};
|
||||||
|
|
||||||
export enum DataSourceType {
|
export enum DataSourceType {
|
||||||
Alertmanager = 'alertmanager',
|
Alertmanager = 'alertmanager',
|
||||||
Loki = 'loki',
|
Loki = 'loki',
|
||||||
@@ -214,10 +221,11 @@ export function getAllRulesSourceNames(): string[] {
|
|||||||
return availableRulesSources;
|
return availableRulesSources;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getExternalRulesSources(): ExternalRulesSourceIdentifier[] {
|
export function getExternalRulesSources(): DataSourceRulesSourceIdentifier[] {
|
||||||
return getRulesDataSources().map((ds) => ({
|
return getRulesDataSources().map((ds) => ({
|
||||||
name: ds.name,
|
name: ds.name,
|
||||||
uid: ds.uid,
|
uid: ds.uid,
|
||||||
|
ruleSourceType: 'datasource',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { isGrafanaRulerRule } from './rules';
|
|||||||
function fromCombinedRule(rule: CombinedRule): RuleGroupIdentifierV2 {
|
function fromCombinedRule(rule: CombinedRule): RuleGroupIdentifierV2 {
|
||||||
if (isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulesSource(rule.namespace.rulesSource)) {
|
if (isGrafanaRulerRule(rule.rulerRule) && isGrafanaRulesSource(rule.namespace.rulesSource)) {
|
||||||
return {
|
return {
|
||||||
rulesSource: { uid: GrafanaRulesSourceSymbol, name: GRAFANA_RULES_SOURCE_NAME },
|
rulesSource: { uid: GrafanaRulesSourceSymbol, name: GRAFANA_RULES_SOURCE_NAME, ruleSourceType: 'grafana' },
|
||||||
namespace: { uid: rule.rulerRule.grafana_alert.namespace_uid },
|
namespace: { uid: rule.rulerRule.grafana_alert.namespace_uid },
|
||||||
groupName: rule.group.name,
|
groupName: rule.group.name,
|
||||||
groupOrigin: 'grafana',
|
groupOrigin: 'grafana',
|
||||||
@@ -16,7 +16,7 @@ function fromCombinedRule(rule: CombinedRule): RuleGroupIdentifierV2 {
|
|||||||
const rulesSourceName = getRulesSourceName(rule.namespace.rulesSource);
|
const rulesSourceName = getRulesSourceName(rule.namespace.rulesSource);
|
||||||
const rulesSourceUid = getDatasourceAPIUid(rulesSourceName);
|
const rulesSourceUid = getDatasourceAPIUid(rulesSourceName);
|
||||||
return {
|
return {
|
||||||
rulesSource: { uid: rulesSourceUid, name: rulesSourceName },
|
rulesSource: { uid: rulesSourceUid, name: rulesSourceName, ruleSourceType: 'datasource' },
|
||||||
namespace: { name: rule.namespace.name },
|
namespace: { name: rule.namespace.name },
|
||||||
groupName: rule.group.name,
|
groupName: rule.group.name,
|
||||||
groupOrigin: 'datasource',
|
groupOrigin: 'datasource',
|
||||||
|
|||||||
@@ -149,16 +149,31 @@ export interface PromRecordingRuleDTO extends PromRuleDTOBase {
|
|||||||
|
|
||||||
export type PromRuleDTO = PromAlertingRuleDTO | PromRecordingRuleDTO;
|
export type PromRuleDTO = PromAlertingRuleDTO | PromRecordingRuleDTO;
|
||||||
|
|
||||||
export interface PromRuleGroupDTO {
|
export interface PromRuleGroupDTO<TRule = PromRuleDTO> {
|
||||||
name: string;
|
name: string;
|
||||||
file: string;
|
file: string;
|
||||||
rules: PromRuleDTO[];
|
rules: TRule[];
|
||||||
interval: number;
|
interval: number;
|
||||||
|
|
||||||
evaluationTime?: number; // these 2 are not in older prometheus payloads
|
evaluationTime?: number; // these 2 are not in older prometheus payloads
|
||||||
lastEvaluation?: string;
|
lastEvaluation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GrafanaPromAlertingRuleDTO extends PromAlertingRuleDTO {
|
||||||
|
uid: string;
|
||||||
|
folderUid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrafanaPromRecordingRuleDTO extends PromRecordingRuleDTO {
|
||||||
|
uid: string;
|
||||||
|
folderUid: string;
|
||||||
|
}
|
||||||
|
export type GrafanaPromRuleDTO = GrafanaPromAlertingRuleDTO | GrafanaPromRecordingRuleDTO;
|
||||||
|
|
||||||
|
export interface GrafanaPromRuleGroupDTO extends PromRuleGroupDTO<GrafanaPromRuleDTO> {
|
||||||
|
folderUid: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface PromResponse<T> {
|
export interface PromResponse<T> {
|
||||||
status: 'success' | 'error' | ''; // mocks return empty string
|
status: 'success' | 'error' | ''; // mocks return empty string
|
||||||
data: T;
|
data: T;
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { AlertState, DataSourceInstanceSettings } from '@grafana/data';
|
import { AlertState, DataSourceInstanceSettings } from '@grafana/data';
|
||||||
import { PromOptions } from '@grafana/prometheus';
|
import { PromOptions } from '@grafana/prometheus';
|
||||||
import { GRAFANA_RULES_SOURCE_NAME } from 'app/features/alerting/unified/utils/datasource';
|
|
||||||
import { LokiOptions } from 'app/plugins/datasource/loki/types';
|
import { LokiOptions } from 'app/plugins/datasource/loki/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -71,6 +70,18 @@ export interface RecordingRule extends RuleBase {
|
|||||||
|
|
||||||
export type Rule = AlertingRule | RecordingRule;
|
export type Rule = AlertingRule | RecordingRule;
|
||||||
|
|
||||||
|
export interface GrafanaAlertingRule extends AlertingRule {
|
||||||
|
uid: string;
|
||||||
|
folderUid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GrafanaRecordingRule extends RecordingRule {
|
||||||
|
uid: string;
|
||||||
|
folderUid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GrafanaRule = GrafanaAlertingRule | GrafanaRecordingRule;
|
||||||
|
|
||||||
export type BaseRuleGroup = { name: string };
|
export type BaseRuleGroup = { name: string };
|
||||||
|
|
||||||
type TotalsWithoutAlerting = Exclude<AlertInstanceTotalState, AlertInstanceTotalState.Alerting>;
|
type TotalsWithoutAlerting = Exclude<AlertInstanceTotalState, AlertInstanceTotalState.Alerting>;
|
||||||
@@ -85,6 +96,18 @@ export interface RuleGroup {
|
|||||||
totals?: Partial<Record<TotalsWithoutAlerting | FiringTotal, number>>;
|
totals?: Partial<Record<TotalsWithoutAlerting | FiringTotal, number>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DataSourceRuleGroup {
|
||||||
|
id: DataSourceRuleGroupIdentifier;
|
||||||
|
interval: number;
|
||||||
|
rules: Rule[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DataSourceRuleNamespace {
|
||||||
|
rulesSource: DataSourceRulesSourceIdentifier;
|
||||||
|
id: DataSourceNamespaceIdentifier;
|
||||||
|
groups: DataSourceRuleGroup[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface RuleNamespace {
|
export interface RuleNamespace {
|
||||||
dataSourceName: string;
|
dataSourceName: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -154,16 +177,20 @@ export interface RuleWithLocation<T = RulerRuleDTO> {
|
|||||||
export const GrafanaRulesSourceSymbol = Symbol('grafana');
|
export const GrafanaRulesSourceSymbol = Symbol('grafana');
|
||||||
export type RulesSourceUid = string | typeof GrafanaRulesSourceSymbol;
|
export type RulesSourceUid = string | typeof GrafanaRulesSourceSymbol;
|
||||||
|
|
||||||
export interface ExternalRulesSourceIdentifier {
|
export interface DataSourceRulesSourceIdentifier {
|
||||||
uid: string;
|
uid: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
// discriminator
|
||||||
|
ruleSourceType: 'datasource';
|
||||||
}
|
}
|
||||||
export interface GrafanaRulesSourceIdentifier {
|
export interface GrafanaRulesSourceIdentifier {
|
||||||
uid: typeof GrafanaRulesSourceSymbol;
|
uid: typeof GrafanaRulesSourceSymbol;
|
||||||
name: typeof GRAFANA_RULES_SOURCE_NAME;
|
name: 'grafana';
|
||||||
|
// discriminator
|
||||||
|
ruleSourceType: 'grafana';
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RulesSourceIdentifier = ExternalRulesSourceIdentifier | GrafanaRulesSourceIdentifier;
|
export type RulesSourceIdentifier = DataSourceRulesSourceIdentifier | GrafanaRulesSourceIdentifier;
|
||||||
|
|
||||||
/** @deprecated use RuleGroupIdentifierV2 instead */
|
/** @deprecated use RuleGroupIdentifierV2 instead */
|
||||||
export interface RuleGroupIdentifier {
|
export interface RuleGroupIdentifier {
|
||||||
@@ -189,7 +216,7 @@ export interface GrafanaRuleGroupIdentifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DataSourceRuleGroupIdentifier {
|
export interface DataSourceRuleGroupIdentifier {
|
||||||
rulesSource: ExternalRulesSourceIdentifier;
|
rulesSource: DataSourceRulesSourceIdentifier;
|
||||||
groupName: string;
|
groupName: string;
|
||||||
namespace: DataSourceNamespaceIdentifier;
|
namespace: DataSourceNamespaceIdentifier;
|
||||||
groupOrigin: 'datasource';
|
groupOrigin: 'datasource';
|
||||||
|
|||||||
@@ -299,6 +299,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"group-actions": {
|
||||||
|
"actions-trigger": "Rule group actions",
|
||||||
|
"delete": "Delete",
|
||||||
|
"edit": "Edit",
|
||||||
|
"export": "Export",
|
||||||
|
"reorder": "Re-order rules"
|
||||||
|
},
|
||||||
"list-view": {
|
"list-view": {
|
||||||
"empty": {
|
"empty": {
|
||||||
"new-alert-rule": "New alert rule",
|
"new-alert-rule": "New alert rule",
|
||||||
@@ -484,11 +491,22 @@
|
|||||||
},
|
},
|
||||||
"rule-list": {
|
"rule-list": {
|
||||||
"configure-datasource": "Configure",
|
"configure-datasource": "Configure",
|
||||||
|
"ds-error-boundary": {
|
||||||
|
"description": "Check the data source configuration. Does the data source support Prometheus API?",
|
||||||
|
"title": "Unable to load rules from this data source"
|
||||||
|
},
|
||||||
"filter-view": {
|
"filter-view": {
|
||||||
"no-more-results": "No more results – showing {{numberOfRules}} rules",
|
"no-more-results": "No more results – showing {{numberOfRules}} rules",
|
||||||
"no-rules-found": "No alert or recording rules matched your current set of filters."
|
"no-rules-found": "No alert or recording rules matched your current set of filters."
|
||||||
},
|
},
|
||||||
"new-alert-rule": "New alert rule"
|
"new-alert-rule": "New alert rule",
|
||||||
|
"pagination": {
|
||||||
|
"next-page": "next page",
|
||||||
|
"previous-page": "previous page"
|
||||||
|
},
|
||||||
|
"return-button": {
|
||||||
|
"title": "Alert rules"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rule-state": {
|
"rule-state": {
|
||||||
"creating": "Creating",
|
"creating": "Creating",
|
||||||
|
|||||||
@@ -299,6 +299,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"group-actions": {
|
||||||
|
"actions-trigger": "Ŗūľę ģřőūp äčŧįőʼnş",
|
||||||
|
"delete": "Đęľęŧę",
|
||||||
|
"edit": "Ēđįŧ",
|
||||||
|
"export": "Ēχpőřŧ",
|
||||||
|
"reorder": "Ŗę-őřđęř řūľęş"
|
||||||
|
},
|
||||||
"list-view": {
|
"list-view": {
|
||||||
"empty": {
|
"empty": {
|
||||||
"new-alert-rule": "Ńęŵ äľęřŧ řūľę",
|
"new-alert-rule": "Ńęŵ äľęřŧ řūľę",
|
||||||
@@ -484,11 +491,22 @@
|
|||||||
},
|
},
|
||||||
"rule-list": {
|
"rule-list": {
|
||||||
"configure-datasource": "Cőʼnƒįģūřę",
|
"configure-datasource": "Cőʼnƒįģūřę",
|
||||||
|
"ds-error-boundary": {
|
||||||
|
"description": "Cĥęčĸ ŧĥę đäŧä şőūřčę čőʼnƒįģūřäŧįőʼn. Đőęş ŧĥę đäŧä şőūřčę şūppőřŧ Přőmęŧĥęūş ÅPĨ?",
|
||||||
|
"title": "Ůʼnäþľę ŧő ľőäđ řūľęş ƒřőm ŧĥįş đäŧä şőūřčę"
|
||||||
|
},
|
||||||
"filter-view": {
|
"filter-view": {
|
||||||
"no-more-results": "Ńő mőřę řęşūľŧş – şĥőŵįʼnģ {{numberOfRules}} řūľęş",
|
"no-more-results": "Ńő mőřę řęşūľŧş – şĥőŵįʼnģ {{numberOfRules}} řūľęş",
|
||||||
"no-rules-found": "Ńő äľęřŧ őř řęčőřđįʼnģ řūľęş mäŧčĥęđ yőūř čūřřęʼnŧ şęŧ őƒ ƒįľŧęřş."
|
"no-rules-found": "Ńő äľęřŧ őř řęčőřđįʼnģ řūľęş mäŧčĥęđ yőūř čūřřęʼnŧ şęŧ őƒ ƒįľŧęřş."
|
||||||
},
|
},
|
||||||
"new-alert-rule": "Ńęŵ äľęřŧ řūľę"
|
"new-alert-rule": "Ńęŵ äľęřŧ řūľę",
|
||||||
|
"pagination": {
|
||||||
|
"next-page": "ʼnęχŧ päģę",
|
||||||
|
"previous-page": "přęvįőūş päģę"
|
||||||
|
},
|
||||||
|
"return-button": {
|
||||||
|
"title": "Åľęřŧ řūľęş"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"rule-state": {
|
"rule-state": {
|
||||||
"creating": "Cřęäŧįʼnģ",
|
"creating": "Cřęäŧįʼnģ",
|
||||||
|
|||||||
Reference in New Issue
Block a user