Alerting: Backend state filtering for history UI (#109647)

This commit is contained in:
Alexander Akhmetov
2025-09-03 17:47:03 +02:00
committed by GitHub
parent 9dfd250049
commit 8a7c1f595a
17 changed files with 425 additions and 204 deletions
@@ -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),
@@ -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&current=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())
})
}
}
+2
View File
@@ -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
@@ -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
}
@@ -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)
}
})
}
@@ -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<string, string> }
{
ruleUid?: string;
from?: number;
to?: number;
limit?: number;
labels?: Record<string, string>;
previous?: string;
current?: string;
}
>({
query: ({ ruleUid, from, to, limit = 100, labels }) => {
query: ({ ruleUid, from, to, limit = 100, labels, previous, current }) => {
const params: Record<string, string | number | undefined> = {
ruleUID: ruleUid,
from,
to,
limit,
previous,
current,
};
if (labels) {
@@ -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
@@ -67,10 +67,16 @@ class HistoryAPIDatasource extends RuntimeDataSource<HistoryAPIQuery> {
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<HistoryAPIQuery> {
* @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<string, string>) => {
export const getHistory = (
from: number,
to: number,
labels?: Record<string, string>,
current?: string,
previous?: string
) => {
return dispatch(
stateHistoryApi.endpoints.getRuleHistory.initiate(
{
@@ -98,6 +110,8 @@ export const getHistory = (from: number, to: number, labels?: Record<string, str
to: to,
limit: LIMIT_EVENTS,
labels: labels,
current: current,
previous: previous,
},
{
forceRefetch: Boolean(getTimeSrv().getAutoRefreshInteval().interval), // force refetch in case we are using the refresh option
@@ -68,6 +68,9 @@ export const HistoryEventsList = ({
const labelFilters = parseBackendLabelFilters(valueInLabelFilter.toString());
const stateTo = valueInStateToFilter.toString();
const stateFrom = valueInStateFromFilter.toString();
const {
data: stateHistory,
isLoading,
@@ -78,12 +81,12 @@ export const HistoryEventsList = ({
to: to,
limit: LIMIT_EVENTS,
labels: labelFilters,
current: stateTo !== 'all' ? stateTo : undefined,
previous: stateFrom !== 'all' ? stateFrom : undefined,
});
const { historyRecords: historyRecordsNotSorted } = useRuleHistoryRecords(stateHistory, {
labels: valueInLabelFilter.toString(),
stateFrom: valueInStateFromFilter.toString(),
stateTo: valueInStateToFilter.toString(),
});
const historyRecords = historyRecordsNotSorted.sort((a, b) => b.timestamp - a.timestamp);
@@ -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
@@ -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,
},
]
`;
@@ -0,0 +1,7 @@
export const StateFilterValues = {
all: 'all',
firing: 'Alerting',
normal: 'Normal',
pending: 'Pending',
recovering: 'Recovering',
} as const;
@@ -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: [] };
@@ -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();
});
});
@@ -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<string, st
}
interface HistoryFilters {
stateTo: string;
stateFrom: string;
labels: string;
}
const emptyFilters: HistoryFilters = {
stateTo: 'all',
stateFrom: 'all',
labels: '',
};
@@ -60,30 +56,17 @@ const emptyFilters: HistoryFilters = {
* This allows us to be able to filter by labels and states in the groupDataFramesByTime function.
*/
export function historyResultToDataFrame({ data }: DataFrameJSON, filters = emptyFilters): DataFrame[] {
const { stateTo, stateFrom } = filters;
// Extract timestamps and lines from the response
const [tsValues = [], lines = []] = data?.values ?? [];
const timestamps = isNumbers(tsValues) ? tsValues : [];
// Filter log records by state and create a list of log records with the timestamp and line
const logRecords = timestamps.reduce<LogRecord[]>((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;
}, []);
@@ -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,
},
};
};
@@ -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<typeof getHistoryResponse>,
previous?: GrafanaAlertState,
current?: GrafanaAlertState
) => {
if (!previous && !current) {
return data;
}
const stateMap: Record<string, string> = {
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);
});
};