diff --git a/pkg/services/ngalert/api/api_ruler_history.go b/pkg/services/ngalert/api/api_ruler_history.go index 00e77d28363..8e32b776274 100644 --- a/pkg/services/ngalert/api/api_ruler_history.go +++ b/pkg/services/ngalert/api/api_ruler_history.go @@ -2,6 +2,7 @@ package api import ( "context" + "fmt" "net/http" "strings" "time" @@ -10,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/ngalert/eval" "github.com/grafana/grafana/pkg/services/ngalert/models" ) @@ -32,6 +34,22 @@ func (srv *HistorySrv) RouteQueryStateHistory(c *contextmodel.ReqContext) respon dashUID := c.Query("dashboardUID") panelID := c.QueryInt64("panelID") + previous := c.Query("previous") + if previous != "" { + _, err := eval.ParseStateString(previous) + if err != nil { + return ErrResp(http.StatusBadRequest, fmt.Errorf("invalid previous state filter: %w", err), "") + } + } + + current := c.Query("current") + if current != "" { + _, err := eval.ParseStateString(current) + if err != nil { + return ErrResp(http.StatusBadRequest, fmt.Errorf("invalid current state filter: %w", err), "") + } + } + labels := make(map[string]string) for k, v := range c.Req.URL.Query() { if strings.HasPrefix(k, labelQueryPrefix) { @@ -44,6 +62,8 @@ func (srv *HistorySrv) RouteQueryStateHistory(c *contextmodel.ReqContext) respon OrgID: c.GetOrgID(), DashboardUID: dashUID, PanelID: panelID, + Previous: previous, + Current: current, SignedInUser: c.SignedInUser, From: time.Unix(from, 0), To: time.Unix(to, 0), diff --git a/pkg/services/ngalert/api/api_ruler_history_test.go b/pkg/services/ngalert/api/api_ruler_history_test.go new file mode 100644 index 00000000000..cba4d9f7b3c --- /dev/null +++ b/pkg/services/ngalert/api/api_ruler_history_test.go @@ -0,0 +1,55 @@ +package api + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/infra/log" + contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/web" +) + +type mockHistorian struct{} + +func (m *mockHistorian) Query(ctx context.Context, query models.HistoryQuery) (*data.Frame, error) { + return &data.Frame{Name: "history"}, nil +} + +func TestRouteQueryStateHistory(t *testing.T) { + testCases := []struct { + name string + queryParams string + expectedCode int + }{ + {"valid states", "previous=Normal¤t=Alerting", http.StatusOK}, + {"invalid previous", "previous=InvalidState", http.StatusBadRequest}, + {"invalid current", "current=InvalidState", http.StatusBadRequest}, + } + + srv := &HistorySrv{ + logger: log.NewNopLogger(), + hist: &mockHistorian{}, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/?"+tt.queryParams, nil) + + c := &contextmodel.ReqContext{ + Context: &web.Context{Req: req}, + SignedInUser: &user.SignedInUser{OrgID: 1}, + } + + resp := srv.RouteQueryStateHistory(c) + + assert.Equal(t, tt.expectedCode, resp.Status()) + }) + } +} diff --git a/pkg/services/ngalert/models/history.go b/pkg/services/ngalert/models/history.go index 1a3dd1b5b29..e75b76603df 100644 --- a/pkg/services/ngalert/models/history.go +++ b/pkg/services/ngalert/models/history.go @@ -13,6 +13,8 @@ type HistoryQuery struct { DashboardUID string PanelID int64 Labels map[string]string + Previous string + Current string From time.Time To time.Time Limit int diff --git a/pkg/services/ngalert/state/historian/loki.go b/pkg/services/ngalert/state/historian/loki.go index c8e9b356552..4ea2ce2c845 100644 --- a/pkg/services/ngalert/state/historian/loki.go +++ b/pkg/services/ngalert/state/historian/loki.go @@ -465,6 +465,20 @@ func buildQueryTail(query models.HistoryQuery) (string, error) { b.WriteString(" | panelID=") b.WriteString(strconv.FormatInt(query.PanelID, 10)) } + if query.Previous != "" { + b.WriteString(" | previous=~") + _, err := fmt.Fprintf(&b, "%q", "^"+regexp.QuoteMeta(query.Previous)+".*") + if err != nil { + return "", err + } + } + if query.Current != "" { + b.WriteString(" | current=~") + _, err := fmt.Fprintf(&b, "%q", "^"+regexp.QuoteMeta(query.Current)+".*") + if err != nil { + return "", err + } + } requiredSize := 0 labelKeys := make([]string, 0, len(query.Labels)) @@ -491,6 +505,8 @@ func queryHasLogFilters(query models.HistoryQuery) bool { return query.RuleUID != "" || query.DashboardUID != "" || query.PanelID != 0 || + query.Previous != "" || + query.Current != "" || len(query.Labels) > 0 } diff --git a/pkg/services/ngalert/state/historian/loki_test.go b/pkg/services/ngalert/state/historian/loki_test.go index e206fac1df4..08487fb27e7 100644 --- a/pkg/services/ngalert/state/historian/loki_test.go +++ b/pkg/services/ngalert/state/historian/loki_test.go @@ -206,13 +206,13 @@ func TestRemoteLokiBackend(t *testing.T) { } func TestBuildLogQuery(t *testing.T) { - maxQuerySize := 110 cases := []struct { - name string - query models.HistoryQuery - folderUIDs []string - exp []string - expErr error + name string + query models.HistoryQuery + folderUIDs []string + maxQuerySize int + exp []string + expErr error }{ { name: "default includes state history label and orgID label", @@ -328,11 +328,54 @@ func TestBuildLogQuery(t *testing.T) { folderUIDs: []string{"folder-1", "folder-2", "folder-" + strings.Repeat("!", 14)}, expErr: ErrLokiQueryTooLong, }, + { + name: "filters by previous state", + query: models.HistoryQuery{ + OrgID: 123, + Previous: "Normal", + }, + exp: []string{`{orgID="123",from="state-history"} | json | previous=~"^Normal.*"`}, + }, + { + name: "filters by current state", + query: models.HistoryQuery{ + OrgID: 123, + Current: "Alerting", + }, + exp: []string{`{orgID="123",from="state-history"} | json | current=~"^Alerting.*"`}, + }, + { + name: "filters by both previous and current state", + query: models.HistoryQuery{ + OrgID: 123, + Previous: "Normal", + Current: "Alerting", + }, + exp: []string{`{orgID="123",from="state-history"} | json | previous=~"^Normal.*" | current=~"^Alerting.*"`}, + }, + { + name: "combines state filters with other filters", + query: models.HistoryQuery{ + OrgID: 123, + RuleUID: "rule-uid", + Previous: "Pending", + Current: "Alerting", + Labels: map[string]string{ + "instance": "localhost:9090", + }, + }, + maxQuerySize: 200, + exp: []string{`{orgID="123",from="state-history"} | json | ruleUID="rule-uid" | previous=~"^Pending.*" | current=~"^Alerting.*" | labels_instance="localhost:9090"`}, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - res, err := BuildLogQuery(tc.query, tc.folderUIDs, maxQuerySize) + querySize := tc.maxQuerySize + if querySize == 0 { + querySize = 110 // default size + } + res, err := BuildLogQuery(tc.query, tc.folderUIDs, querySize) if tc.expErr != nil { require.ErrorIs(t, err, tc.expErr) return @@ -340,7 +383,7 @@ func TestBuildLogQuery(t *testing.T) { require.NoError(t, err) assert.EqualValues(t, tc.exp, res) for i, q := range res { - assert.LessOrEqualf(t, len(q), maxQuerySize, "query at index %d exceeded max query size. Query: %s", i, q) + assert.LessOrEqualf(t, len(q), querySize, "query at index %d exceeded max query size. Query: %s", i, q) } }) } diff --git a/public/app/features/alerting/unified/api/stateHistoryApi.ts b/public/app/features/alerting/unified/api/stateHistoryApi.ts index bb4c85c58a9..29dfa9e515a 100644 --- a/public/app/features/alerting/unified/api/stateHistoryApi.ts +++ b/public/app/features/alerting/unified/api/stateHistoryApi.ts @@ -6,14 +6,24 @@ export const stateHistoryApi = alertingApi.injectEndpoints({ endpoints: (build) => ({ getRuleHistory: build.query< DataFrameJSON, - { ruleUid?: string; from?: number; to?: number; limit?: number; labels?: Record } + { + ruleUid?: string; + from?: number; + to?: number; + limit?: number; + labels?: Record; + previous?: string; + current?: string; + } >({ - query: ({ ruleUid, from, to, limit = 100, labels }) => { + query: ({ ruleUid, from, to, limit = 100, labels, previous, current }) => { const params: Record = { ruleUID: ruleUid, from, to, limit, + previous, + current, }; if (labels) { diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx index 9f8c8e2eb25..298073a7699 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralAlertHistoryScene.tsx @@ -43,6 +43,7 @@ import { LogMessages, logInfo } from '../../../Analytics'; import { alertStateHistoryDatasource, useRegisterHistoryRuntimeDataSource } from './CentralHistoryRuntimeDataSource'; import { HistoryEventsListObject } from './EventListSceneObject'; +import { StateFilterValues } from './constants'; export const LABELS_FILTER = 'LABELS_FILTER'; export const STATE_FILTER_TO = 'STATE_FILTER_TO'; @@ -57,14 +58,6 @@ export const STATE_FILTER_FROM = 'STATE_FILTER_FROM'; * Both share time range and filter variable from the parent scene. */ -export const StateFilterValues = { - all: 'all', - firing: 'Alerting', - normal: 'Normal', - pending: 'Pending', - recovering: 'Recovering', -} as const; - export const CentralAlertHistoryScene = () => { //track the loading of the central alert state history diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts b/public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts index f6b76f5e00a..e0d14a8e0b4 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts +++ b/public/app/features/alerting/unified/components/rules/central-state-history/CentralHistoryRuntimeDataSource.ts @@ -67,10 +67,16 @@ class HistoryAPIDatasource extends RuntimeDataSource { const labelFilters = parseBackendLabelFilters(labels); - const historyResult = await getHistory(from, to, labelFilters); + const historyResult = await getHistory( + from, + to, + labelFilters, + stateTo !== 'all' ? stateTo : undefined, + stateFrom !== 'all' ? stateFrom : undefined + ); return { - data: historyResultToDataFrame(historyResult, { stateTo, stateFrom, labels }), + data: historyResultToDataFrame(historyResult, { labels }), }; } @@ -90,7 +96,13 @@ class HistoryAPIDatasource extends RuntimeDataSource { * @param labels optional label filters for backend filtering * @returns the history events filtered by time and labels */ -export const getHistory = (from: number, to: number, labels?: Record) => { +export const getHistory = ( + from: number, + to: number, + labels?: Record, + current?: string, + previous?: string +) => { return dispatch( stateHistoryApi.endpoints.getRuleHistory.initiate( { @@ -98,6 +110,8 @@ export const getHistory = (from: number, to: number, labels?: Record b.timestamp - a.timestamp); diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/HistoryEventsList.test.tsx b/public/app/features/alerting/unified/components/rules/central-state-history/HistoryEventsList.test.tsx index d538fc8b0c2..39553ae4b50 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/HistoryEventsList.test.tsx +++ b/public/app/features/alerting/unified/components/rules/central-state-history/HistoryEventsList.test.tsx @@ -6,9 +6,9 @@ import { getDefaultTimeRange } from '@grafana/data'; import { setupMswServer } from '../../../mockApi'; import { captureRequests } from '../../../mocks/server/events'; -import { StateFilterValues } from './CentralAlertHistoryScene'; import { getHistory } from './CentralHistoryRuntimeDataSource'; import { HistoryEventsList } from './EventListSceneObject'; +import { StateFilterValues } from './constants'; setupMswServer(); // msw server is setup to intercept the history api call and return the mocked data by default diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/__snapshots__/utils.test.ts.snap b/public/app/features/alerting/unified/components/rules/central-state-history/__snapshots__/utils.test.ts.snap index bd51492acd2..691557c4e10 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/__snapshots__/utils.test.ts.snap +++ b/public/app/features/alerting/unified/components/rules/central-state-history/__snapshots__/utils.test.ts.snap @@ -53,7 +53,11 @@ exports[`historyResultToDataFrame should decode and filter example1 1`] = ` "name": "time", "type": "time", "values": [ + 1727189670000, 1727189680000, + 1727189700000, + 1727189690000, + 1727189710000, ], }, { @@ -61,11 +65,15 @@ exports[`historyResultToDataFrame should decode and filter example1 1`] = ` "name": "value", "type": "number", "values": [ - 1, + 11, + 2, + 2, + 8, + 9, ], }, ], - "length": 1, + "length": 5, }, ] `; @@ -85,6 +93,10 @@ exports[`historyResultToDataFrame should decode and filter example2 1`] = ` "type": "time", "values": [ 1727189670000, + 1727189680000, + 1727189700000, + 1727189690000, + 1727189710000, ], }, { @@ -92,11 +104,15 @@ exports[`historyResultToDataFrame should decode and filter example2 1`] = ` "name": "value", "type": "number", "values": [ + 11, + 2, + 2, 8, + 9, ], }, ], - "length": 1, + "length": 5, }, ] `; diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/constants.ts b/public/app/features/alerting/unified/components/rules/central-state-history/constants.ts new file mode 100644 index 00000000000..dcf0ad3f6c9 --- /dev/null +++ b/public/app/features/alerting/unified/components/rules/central-state-history/constants.ts @@ -0,0 +1,7 @@ +export const StateFilterValues = { + all: 'all', + firing: 'Alerting', + normal: 'Normal', + pending: 'Pending', + recovering: 'Recovering', +} as const; diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/useRuleHistoryRecords.ts b/public/app/features/alerting/unified/components/rules/central-state-history/useRuleHistoryRecords.ts index ee8c739d3e5..b56b2a81f24 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/useRuleHistoryRecords.ts +++ b/public/app/features/alerting/unified/components/rules/central-state-history/useRuleHistoryRecords.ts @@ -8,14 +8,22 @@ import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers'; import { LogRecord } from '../state-history/common'; import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords'; -import { StateFilterValues } from './CentralAlertHistoryScene'; +import { StateFilterValues } from './constants'; -const emptyFilters = { +type StateFilter = (typeof StateFilterValues)[keyof typeof StateFilterValues]; + +const emptyFilters: HistoryRecordFilters = { labels: '', - stateFrom: 'all', - stateTo: 'all', + stateFrom: StateFilterValues.all, + stateTo: StateFilterValues.all, }; +interface HistoryRecordFilters { + labels: string; + stateFrom?: StateFilter; + stateTo?: StateFilter; +} + /** * This hook filters the history records based on the label, stateTo and stateFrom filters. * @param filterInLabel @@ -24,12 +32,12 @@ const emptyFilters = { * @param stateHistory the original history records * @returns the filtered history records */ -export function useRuleHistoryRecords(stateHistory?: DataFrameJSON, filters = emptyFilters) { +export function useRuleHistoryRecords(stateHistory?: DataFrameJSON, filters: HistoryRecordFilters = emptyFilters) { return useMemo(() => ruleHistoryToRecords(stateHistory, filters), [filters, stateHistory]); } -export function ruleHistoryToRecords(stateHistory?: DataFrameJSON, filters = emptyFilters) { - const { labels, stateFrom, stateTo } = filters; +export function ruleHistoryToRecords(stateHistory?: DataFrameJSON, filters: HistoryRecordFilters = emptyFilters) { + const { labels, stateFrom = StateFilterValues.all, stateTo = StateFilterValues.all } = filters; if (!stateHistory?.data) { return { historyRecords: [] }; diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/utils.test.ts b/public/app/features/alerting/unified/components/rules/central-state-history/utils.test.ts index dacb6ba2e49..579e81620ce 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/utils.test.ts +++ b/public/app/features/alerting/unified/components/rules/central-state-history/utils.test.ts @@ -9,15 +9,11 @@ describe('historyResultToDataFrame', () => { it('should decode and filter example1', () => { expect( historyResultToDataFrame(fixtureData, { - stateFrom: 'Pending', - stateTo: 'Alerting', labels: "alertname: 'XSS attack vector'", }) ).toMatchSnapshot(); }); it('should decode and filter example2', () => { - expect( - historyResultToDataFrame(fixtureData, { stateFrom: 'Normal', stateTo: 'NoData', labels: 'region: EMEA' }) - ).toMatchSnapshot(); + expect(historyResultToDataFrame(fixtureData, { labels: 'region: EMEA' })).toMatchSnapshot(); }); }); diff --git a/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts b/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts index aaa7ea4830e..968253301a5 100644 --- a/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts +++ b/public/app/features/alerting/unified/components/rules/central-state-history/utils.ts @@ -12,14 +12,14 @@ import { getDisplayProcessor, } from '@grafana/data'; import { fieldIndexComparer } from '@grafana/data/internal'; -import { mapStateWithReasonToBaseState } from 'app/types/unified-alerting-dto'; import { labelsMatchMatchers } from '../../../utils/alertmanager'; import { parsePromQLStyleMatcherLooseSafe } from '../../../utils/matchers'; import { LogRecord } from '../state-history/common'; import { isLine, isNumbers } from '../state-history/useRuleHistoryRecords'; -import { LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO, StateFilterValues } from './CentralAlertHistoryScene'; +import { LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO } from './CentralAlertHistoryScene'; +import { StateFilterValues } from './constants'; const GROUPING_INTERVAL = 10 * 1000; // 10 seconds const QUERY_PARAM_PREFIX = 'var-'; // Prefix used by Grafana to sync variables in the URL @@ -42,14 +42,10 @@ export function parseBackendLabelFilters(labelFilter: string): Record((acc, timestamp: number, index: number) => { const line = lines[index]; if (!isLine(line)) { return acc; } - // we have to filter out by state at that point , because we are going to group by timestamp and these states are going to be lost - const baseStateTo = mapStateWithReasonToBaseState(line.current); - const baseStateFrom = mapStateWithReasonToBaseState(line.previous); - const stateToMatch = stateTo !== StateFilterValues.all ? stateTo === baseStateTo : true; - const stateFromMatch = stateFrom !== StateFilterValues.all ? stateFrom === baseStateFrom : true; - - // filter by state - if (stateToMatch && stateFromMatch) { - acc.push({ timestamp, line }); - } - + acc.push({ timestamp, line }); return acc; }, []); diff --git a/public/app/features/alerting/unified/mocks/grafanaRulerApi.ts b/public/app/features/alerting/unified/mocks/grafanaRulerApi.ts index 18c690e7678..54c205183ab 100644 --- a/public/app/features/alerting/unified/mocks/grafanaRulerApi.ts +++ b/public/app/features/alerting/unified/mocks/grafanaRulerApi.ts @@ -162,154 +162,158 @@ export const time_plus_10 = time_0 + 10 * 1000; export const time_plus_30 = time_0 + 30 * 1000; // returns 4 transitions. times is an array of 4 timestamps. -export const getHistoryResponse = (times: number[]) => ({ - schema: { - fields: [ - { - name: 'time', - type: FieldType.time, - labels: {}, +export const getHistoryResponse = (times: number[]) => { + const timeValues = [...times]; + const lineValues = [ + { + schemaVersion: 1, + previous: 'Pending', + current: 'Alerting', + value: { + A: 1, + B: 1, + C: 1, }, - { - name: 'line', - type: FieldType.other, - labels: {}, + condition: 'C', + dashboardUID: '', + panelID: 0, + fingerprint: '141da2d491f61029', + ruleTitle: 'alert1', + ruleID: 7, + ruleUID: 'adnpo0g62bg1sb', + labels: { + alertname: 'alert1', + grafana_folder: 'FOLDER A', + handler: '/alerting/*', }, - { - name: 'labels', - type: FieldType.other, - labels: {}, + }, + { + schemaVersion: 1, + previous: 'Alerting', + current: 'Normal', + value: { + A: 1, + B: 1, + C: 1, }, - ], - }, - data: { - values: [ - [...times], - [ - { - schemaVersion: 1, - previous: 'Pending', - current: 'Alerting', - value: { - A: 1, - B: 1, - C: 1, - }, - condition: 'C', - dashboardUID: '', - panelID: 0, - fingerprint: '141da2d491f61029', - ruleTitle: 'alert1', - ruleID: 7, - ruleUID: 'adnpo0g62bg1sb', - labels: { - alertname: 'alert1', - grafana_folder: 'FOLDER A', - handler: '/alerting/*', - }, - }, - { - schemaVersion: 1, - previous: 'Alerting', - current: 'Normal', - value: { - A: 1, - B: 1, - C: 1, - }, - condition: 'C', - dashboardUID: '', - panelID: 0, - fingerprint: '141da2d491f61030', - ruleTitle: 'alert2', - ruleID: 3, - ruleUID: 'adna1xso80hdsd', - labels: { - alertname: 'alert2', - grafana_folder: 'FOLDER A', - handler: '/alerting/*', - }, - }, - { - schemaVersion: 1, - previous: 'Normal', - current: 'Pending', - value: { - A: 1, - B: 1, - C: 1, - }, - condition: 'C', - dashboardUID: '', - panelID: 0, + condition: 'C', + dashboardUID: '', + panelID: 0, + fingerprint: '141da2d491f61030', + ruleTitle: 'alert2', + ruleID: 3, + ruleUID: 'adna1xso80hdsd', + labels: { + alertname: 'alert2', + grafana_folder: 'FOLDER A', + handler: '/alerting/*', + }, + }, + { + schemaVersion: 1, + previous: 'Normal', + current: 'Pending', + value: { + A: 1, + B: 1, + C: 1, + }, + condition: 'C', + dashboardUID: '', + panelID: 0, - fingerprint: '141da2d491f61031', - ruleTitle: 'alert1', - ruleID: 7, - ruleUID: 'adnpo0g62bg1sb', - labels: { - alertname: 'alert1', - grafana_folder: 'FOLDER A', - handler: '/alerting/*', - }, + fingerprint: '141da2d491f61031', + ruleTitle: 'alert1', + ruleID: 7, + ruleUID: 'adnpo0g62bg1sb', + labels: { + alertname: 'alert1', + grafana_folder: 'FOLDER A', + handler: '/alerting/*', + }, + }, + { + schemaVersion: 1, + previous: 'Pending', + current: 'Alerting', + value: { + A: 1, + B: 1, + C: 1, + }, + condition: 'C', + dashboardUID: '', + panelID: 0, + fingerprint: '5d438530c73fc657', + ruleTitle: 'alert2', + ruleID: 3, + ruleUID: 'adna1xso80hdsd', + labels: { + alertname: 'alert2', + grafana_folder: 'FOLDER A', + handler: '/alerting/*', + }, + }, + ]; + const labelsValues = [ + { + folderUID: 'edlvwh5881z40e', + from: 'state-history', + group: 'GROUP111', + level: 'info', + orgID: '1', + service_name: 'unknown_service', + }, + { + folderUID: 'edlvwh5881z40e', + from: 'state-history', + group: 'GROUP111', + level: 'info', + orgID: '1', + service_name: 'unknown_service', + }, + { + folderUID: 'edlvwh5881z40e', + from: 'state-history', + group: 'GROUP111', + level: 'info', + orgID: '1', + service_name: 'unknown_service', + }, + { + folderUID: 'edlvwh5881z40e', + from: 'state-history', + group: 'GROUP111', + level: 'info', + orgID: '1', + service_name: 'unknown_service', + }, + ]; + + const values: [number[], typeof lineValues, typeof labelsValues] = [timeValues, lineValues, labelsValues]; + + return { + schema: { + fields: [ + { + name: 'time', + type: FieldType.time, + labels: {}, }, { - schemaVersion: 1, - previous: 'Pending', - current: 'Alerting', - value: { - A: 1, - B: 1, - C: 1, - }, - condition: 'C', - dashboardUID: '', - panelID: 0, - fingerprint: '5d438530c73fc657', - ruleTitle: 'alert2', - ruleID: 3, - ruleUID: 'adna1xso80hdsd', - labels: { - alertname: 'alert2', - grafana_folder: 'FOLDER A', - handler: '/alerting/*', - }, + name: 'line', + type: FieldType.other, + labels: {}, + }, + { + name: 'labels', + type: FieldType.other, + labels: {}, }, ], - [ - { - folderUID: 'edlvwh5881z40e', - from: 'state-history', - group: 'GROUP111', - level: 'info', - orgID: '1', - service_name: 'unknown_service', - }, - { - folderUID: 'edlvwh5881z40e', - from: 'state-history', - group: 'GROUP111', - level: 'info', - orgID: '1', - service_name: 'unknown_service', - }, - { - folderUID: 'edlvwh5881z40e', - from: 'state-history', - group: 'GROUP111', - level: 'info', - orgID: '1', - service_name: 'unknown_service', - }, - { - folderUID: 'edlvwh5881z40e', - from: 'state-history', - group: 'GROUP111', - level: 'info', - orgID: '1', - service_name: 'unknown_service', - }, - ], - ], - }, -}); + }, + data: { + values, + }, + }; +}; diff --git a/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts b/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts index 43dd6761400..b0f58408306 100644 --- a/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts +++ b/public/app/features/alerting/unified/mocks/server/handlers/grafanaRuler.ts @@ -4,11 +4,13 @@ import { HttpResponse, delay, http } from 'msw'; export const MOCK_GRAFANA_ALERT_RULE_TITLE = 'Test alert'; import { + GrafanaAlertState, GrafanaRuleDefinition, PromRulesResponse, RulerGrafanaRuleDTO, RulerRuleGroupDTO, RulerRulesConfigDTO, + isGrafanaAlertState, } from '../../../../../../types/unified-alerting-dto'; import { GrafanaGroupUpdatedResponse } from '../../../api/alertRuleModel'; import { getHistoryResponse, grafanaRulerRule, rulerTestDb, time_0, time_plus_30 } from '../../grafanaRulerApi'; @@ -204,9 +206,58 @@ export const rulerRuleVersionHistoryHandler = () => { }); }; +const filterHistoryByState = ( + data: ReturnType, + previous?: GrafanaAlertState, + current?: GrafanaAlertState +) => { + if (!previous && !current) { + return data; + } + const stateMap: Record = { + firing: 'Alerting', + normal: 'Normal', + pending: 'Pending', + }; + + const [timeValues, lineValues, labelsValues] = data.data.values; + + const filteredRecords: typeof lineValues = []; + const filteredTimes: typeof timeValues = []; + const filteredLabels: typeof labelsValues = []; + + lineValues.forEach((record, index: number) => { + const matchesPrevious = !previous || record.previous === (stateMap[previous] || previous); + const matchesCurrent = !current || record.current === (stateMap[current] || current); + + if (matchesPrevious && matchesCurrent) { + filteredRecords.push(record); + filteredTimes.push(timeValues[index]); + filteredLabels.push(labelsValues[index]); + } + }); + + return { + ...data, + data: { + values: [filteredTimes, filteredRecords, filteredLabels], + }, + }; +}; + export const historyHandler = () => { - return http.get('/api/v1/rules/history', () => { - return HttpResponse.json(getHistoryResponse([time_0, time_0, time_plus_30, time_plus_30])); + return http.get('/api/v1/rules/history', ({ request }) => { + const url = new URL(request.url); + const previousParam = url.searchParams.get('previous'); + const currentParam = url.searchParams.get('current'); + + const previous = previousParam && isGrafanaAlertState(previousParam) ? previousParam : undefined; + const current = currentParam && isGrafanaAlertState(currentParam) ? currentParam : undefined; + + const fullData = getHistoryResponse([time_0, time_0, time_plus_30, time_plus_30]); + const filteredData = filterHistoryByState(fullData, previous, current); + + return HttpResponse.json(filteredData); }); };