Alerting: Backend state filtering for history UI (#109647)
This commit is contained in:
@@ -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¤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())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
+1
-8
@@ -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
|
||||
|
||||
|
||||
+17
-3
@@ -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
|
||||
|
||||
+5
-2
@@ -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);
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+19
-3
@@ -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,
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
export const StateFilterValues = {
|
||||
all: 'all',
|
||||
firing: 'Alerting',
|
||||
normal: 'Normal',
|
||||
pending: 'Pending',
|
||||
recovering: 'Recovering',
|
||||
} as const;
|
||||
+15
-7
@@ -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: [] };
|
||||
|
||||
+1
-5
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+3
-20
@@ -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);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user