mirror of
https://github.com/grafana/grafana.git
synced 2025-01-11 00:22:06 -06:00
Alerting: Improve performance ash page (#97619)
* first commit adding usememo and refactoring scenes objects to keep an state for variables values * Fix scenes with the panel reacting to variables changes * move body to the model * address some pr feedback * Refactoring central alert history scene (#97658) Refactoring * fix test and some wrong imports * update comments * add eslint-disable-next-line * remove unnecessary SceneFlexLayout and SceneFlexItem wrapper * address pr feedback * update tests for labels filtering --------- Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
448d87157c
commit
dd9638cade
@ -1,14 +1,16 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { useEffect } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, VariableHide } from '@grafana/data';
|
import { GrafanaTheme2, VariableHide } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
CustomVariable,
|
CustomVariable,
|
||||||
EmbeddedScene,
|
EmbeddedScene,
|
||||||
PanelBuilders,
|
PanelBuilders,
|
||||||
|
SceneComponentProps,
|
||||||
SceneControlsSpacer,
|
SceneControlsSpacer,
|
||||||
SceneFlexItem,
|
SceneFlexItem,
|
||||||
SceneFlexLayout,
|
SceneFlexLayout,
|
||||||
|
SceneObjectBase,
|
||||||
SceneQueryRunner,
|
SceneQueryRunner,
|
||||||
SceneReactObject,
|
SceneReactObject,
|
||||||
SceneRefreshPicker,
|
SceneRefreshPicker,
|
||||||
@ -16,7 +18,9 @@ import {
|
|||||||
SceneTimeRange,
|
SceneTimeRange,
|
||||||
SceneVariableSet,
|
SceneVariableSet,
|
||||||
TextBoxVariable,
|
TextBoxVariable,
|
||||||
|
VariableDependencyConfig,
|
||||||
VariableValueSelectors,
|
VariableValueSelectors,
|
||||||
|
sceneGraph,
|
||||||
useUrlSync,
|
useUrlSync,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { GraphDrawStyle, VisibilityMode } from '@grafana/schema/dist/esm/index';
|
import { GraphDrawStyle, VisibilityMode } from '@grafana/schema/dist/esm/index';
|
||||||
@ -36,14 +40,13 @@ import {
|
|||||||
import { Trans } from 'app/core/internationalization';
|
import { Trans } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { LogMessages, logInfo } from '../../../Analytics';
|
import { LogMessages, logInfo } from '../../../Analytics';
|
||||||
import { DataSourceInformation } from '../../../home/Insights';
|
|
||||||
|
|
||||||
import { alertStateHistoryDatasource, useRegisterHistoryRuntimeDataSource } from './CentralHistoryRuntimeDataSource';
|
import { alertStateHistoryDatasource, useRegisterHistoryRuntimeDataSource } from './CentralHistoryRuntimeDataSource';
|
||||||
import { HistoryEventsListObject } from './EventListSceneObject';
|
import { HistoryEventsListObject } from './EventListSceneObject';
|
||||||
|
|
||||||
export const LABELS_FILTER = 'labelsFilter';
|
export const LABELS_FILTER = 'LABELS_FILTER';
|
||||||
export const STATE_FILTER_TO = 'stateFilterTo';
|
export const STATE_FILTER_TO = 'STATE_FILTER_TO';
|
||||||
export const STATE_FILTER_FROM = 'stateFilterFrom';
|
export const STATE_FILTER_FROM = 'STATE_FILTER_FROM';
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* This scene shows the history of the alert state changes.
|
* This scene shows the history of the alert state changes.
|
||||||
@ -67,74 +70,72 @@ export const CentralAlertHistoryScene = () => {
|
|||||||
logInfo(LogMessages.loadedCentralAlertStateHistory);
|
logInfo(LogMessages.loadedCentralAlertStateHistory);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// create the variables for the filters
|
|
||||||
// textbox variable for filtering by labels
|
|
||||||
const labelsFilterVariable = new TextBoxVariable({
|
|
||||||
name: LABELS_FILTER,
|
|
||||||
label: 'Labels: ',
|
|
||||||
});
|
|
||||||
//custom variable for filtering by the current state
|
|
||||||
const transitionsToFilterVariable = new CustomVariable({
|
|
||||||
name: STATE_FILTER_TO,
|
|
||||||
value: StateFilterValues.all,
|
|
||||||
label: 'End state:',
|
|
||||||
hide: VariableHide.dontHide,
|
|
||||||
query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending}`,
|
|
||||||
});
|
|
||||||
//custom variable for filtering by the previous state
|
|
||||||
const transitionsFromFilterVariable = new CustomVariable({
|
|
||||||
name: STATE_FILTER_FROM,
|
|
||||||
value: StateFilterValues.all,
|
|
||||||
label: 'Start state:',
|
|
||||||
hide: VariableHide.dontHide,
|
|
||||||
query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
useRegisterHistoryRuntimeDataSource(); // register the runtime datasource for the history api.
|
useRegisterHistoryRuntimeDataSource(); // register the runtime datasource for the history api.
|
||||||
|
|
||||||
const scene = new EmbeddedScene({
|
const scene = useMemo(() => {
|
||||||
controls: [
|
// create the variables for the filters
|
||||||
new SceneReactObject({
|
// textbox variable for filtering by labels
|
||||||
component: LabelFilter,
|
const labelsFilterVariable = new TextBoxVariable({
|
||||||
}),
|
name: LABELS_FILTER,
|
||||||
new SceneReactObject({
|
label: 'Labels: ',
|
||||||
component: FilterInfo,
|
});
|
||||||
}),
|
|
||||||
new VariableValueSelectors({}),
|
//custom variable for filtering by the current state
|
||||||
new SceneReactObject({
|
const transitionsToFilterVariable = new CustomVariable({
|
||||||
component: ClearFilterButton,
|
name: STATE_FILTER_TO,
|
||||||
props: {
|
value: StateFilterValues.all,
|
||||||
labelsFilterVariable,
|
label: 'End state:',
|
||||||
transitionsToFilterVariable,
|
hide: VariableHide.dontHide,
|
||||||
transitionsFromFilterVariable,
|
query: `All : ${StateFilterValues.all}, To Firing : ${StateFilterValues.firing},To Normal : ${StateFilterValues.normal},To Pending : ${StateFilterValues.pending}`,
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
new SceneControlsSpacer(),
|
//custom variable for filtering by the previous state
|
||||||
new SceneTimePicker({}),
|
const transitionsFromFilterVariable = new CustomVariable({
|
||||||
new SceneRefreshPicker({}),
|
name: STATE_FILTER_FROM,
|
||||||
],
|
value: StateFilterValues.all,
|
||||||
// use default time range as from 1 hour ago to now, as the limit of the history api is 5000 events,
|
label: 'Start state:',
|
||||||
// and using a wider time range might lead to showing gaps in the events list and the chart.
|
hide: VariableHide.dontHide,
|
||||||
$timeRange: new SceneTimeRange({
|
query: `All : ${StateFilterValues.all}, From Firing : ${StateFilterValues.firing},From Normal : ${StateFilterValues.normal},From Pending : ${StateFilterValues.pending}`,
|
||||||
from: 'now-1h',
|
});
|
||||||
to: 'now',
|
|
||||||
}),
|
return new EmbeddedScene({
|
||||||
$variables: new SceneVariableSet({
|
controls: [
|
||||||
variables: [labelsFilterVariable, transitionsFromFilterVariable, transitionsToFilterVariable],
|
new SceneReactObject({
|
||||||
}),
|
component: LabelFilter,
|
||||||
body: new SceneFlexLayout({
|
|
||||||
direction: 'column',
|
|
||||||
children: [
|
|
||||||
new SceneFlexItem({
|
|
||||||
ySizing: 'content',
|
|
||||||
body: getEventsSceneObject(alertStateHistoryDatasource),
|
|
||||||
}),
|
}),
|
||||||
new SceneFlexItem({
|
new SceneReactObject({
|
||||||
body: new HistoryEventsListObject(),
|
component: FilterInfo,
|
||||||
}),
|
}),
|
||||||
|
new VariableValueSelectors({}),
|
||||||
|
new ClearFilterButtonScenesObject({}),
|
||||||
|
new SceneControlsSpacer(),
|
||||||
|
new SceneTimePicker({}),
|
||||||
|
new SceneRefreshPicker({}),
|
||||||
],
|
],
|
||||||
}),
|
// use default time range as from 1 hour ago to now, as the limit of the history api is 5000 events,
|
||||||
});
|
// and using a wider time range might lead to showing gaps in the events list and the chart.
|
||||||
|
$timeRange: new SceneTimeRange({
|
||||||
|
from: 'now-1h',
|
||||||
|
to: 'now',
|
||||||
|
}),
|
||||||
|
$variables: new SceneVariableSet({
|
||||||
|
variables: [labelsFilterVariable, transitionsFromFilterVariable, transitionsToFilterVariable],
|
||||||
|
}),
|
||||||
|
body: new SceneFlexLayout({
|
||||||
|
direction: 'column',
|
||||||
|
children: [
|
||||||
|
new SceneFlexItem({
|
||||||
|
ySizing: 'content',
|
||||||
|
body: getEventsSceneObject(),
|
||||||
|
}),
|
||||||
|
new SceneFlexItem({
|
||||||
|
body: new HistoryEventsListObject({}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
// we need to call this to sync the url with the scene state
|
// we need to call this to sync the url with the scene state
|
||||||
const isUrlSyncInitialized = useUrlSync(scene);
|
const isUrlSyncInitialized = useUrlSync(scene);
|
||||||
|
|
||||||
@ -147,22 +148,11 @@ export const CentralAlertHistoryScene = () => {
|
|||||||
/**
|
/**
|
||||||
* Creates a SceneFlexItem with a timeseries panel that shows the events.
|
* Creates a SceneFlexItem with a timeseries panel that shows the events.
|
||||||
* The query uses a runtime datasource that fetches the events from the history api.
|
* The query uses a runtime datasource that fetches the events from the history api.
|
||||||
* @param alertStateHistoryDataSource the datasource information for the runtime datasource
|
|
||||||
*/
|
*/
|
||||||
function getEventsSceneObject(alertStateHistoryDataSource: DataSourceInformation) {
|
function getEventsSceneObject() {
|
||||||
return new EmbeddedScene({
|
return new SceneFlexLayout({
|
||||||
controls: [],
|
direction: 'column',
|
||||||
body: new SceneFlexLayout({
|
children: [getEventsScenesFlexItem()],
|
||||||
direction: 'column',
|
|
||||||
children: [
|
|
||||||
new SceneFlexItem({
|
|
||||||
ySizing: 'content',
|
|
||||||
body: new SceneFlexLayout({
|
|
||||||
children: [getEventsScenesFlexItem(alertStateHistoryDataSource)],
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -171,15 +161,15 @@ function getEventsSceneObject(alertStateHistoryDataSource: DataSourceInformation
|
|||||||
* @param datasource the datasource information for the runtime datasource
|
* @param datasource the datasource information for the runtime datasource
|
||||||
* @returns the SceneQueryRunner
|
* @returns the SceneQueryRunner
|
||||||
*/
|
*/
|
||||||
function getSceneQuery(datasource: DataSourceInformation) {
|
function getQueryRunnerForAlertHistoryDataSource() {
|
||||||
const query = new SceneQueryRunner({
|
const query = new SceneQueryRunner({
|
||||||
datasource: datasource,
|
datasource: alertStateHistoryDatasource,
|
||||||
queries: [
|
queries: [
|
||||||
{
|
{
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
expr: '',
|
labels: '${LABELS_FILTER}',
|
||||||
queryType: 'range',
|
stateFrom: '${STATE_FILTER_FROM}',
|
||||||
step: '10s',
|
stateTo: '${STATE_FILTER_TO}',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@ -189,7 +179,7 @@ function getSceneQuery(datasource: DataSourceInformation) {
|
|||||||
* This function creates a SceneFlexItem with a timeseries panel that shows the events.
|
* This function creates a SceneFlexItem with a timeseries panel that shows the events.
|
||||||
* The query uses a runtime datasource that fetches the events from the history api.
|
* The query uses a runtime datasource that fetches the events from the history api.
|
||||||
*/
|
*/
|
||||||
export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
|
export function getEventsScenesFlexItem() {
|
||||||
return new SceneFlexItem({
|
return new SceneFlexItem({
|
||||||
minHeight: 300,
|
minHeight: 300,
|
||||||
body: PanelBuilders.timeseries()
|
body: PanelBuilders.timeseries()
|
||||||
@ -197,7 +187,7 @@ export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
|
|||||||
.setDescription(
|
.setDescription(
|
||||||
'Each alert event represents an alert instance that changed its state at a particular point in time. The history of the data is displayed over a period of time.'
|
'Each alert event represents an alert instance that changed its state at a particular point in time. The history of the data is displayed over a period of time.'
|
||||||
)
|
)
|
||||||
.setData(getSceneQuery(datasource))
|
.setData(getQueryRunnerForAlertHistoryDataSource())
|
||||||
.setColor({ mode: 'continuous-BlPu' })
|
.setColor({ mode: 'continuous-BlPu' })
|
||||||
.setCustomFieldConfig('fillOpacity', 100)
|
.setCustomFieldConfig('fillOpacity', 100)
|
||||||
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars)
|
.setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars)
|
||||||
@ -213,47 +203,49 @@ export function getEventsScenesFlexItem(datasource: DataSourceInformation) {
|
|||||||
.setCustomFieldConfig('scaleDistribution', { type: ScaleDistribution.Linear })
|
.setCustomFieldConfig('scaleDistribution', { type: ScaleDistribution.Linear })
|
||||||
.setOption('legend', { showLegend: false, displayMode: LegendDisplayMode.Hidden })
|
.setOption('legend', { showLegend: false, displayMode: LegendDisplayMode.Hidden })
|
||||||
.setOption('tooltip', { mode: TooltipDisplayMode.Single })
|
.setOption('tooltip', { mode: TooltipDisplayMode.Single })
|
||||||
|
|
||||||
.setNoValue('No events found')
|
.setNoValue('No events found')
|
||||||
.build(),
|
.build(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
* This component shows a button to clear the filters.
|
|
||||||
* It is shown when the filters are active.
|
|
||||||
* props:
|
|
||||||
* labelsFilterVariable: the textbox variable for filtering by labels
|
|
||||||
* transitionsToFilterVariable: the custom variable for filtering by the current state
|
|
||||||
* transitionsFromFilterVariable: the custom variable for filtering by the previous state
|
|
||||||
*/
|
|
||||||
|
|
||||||
function ClearFilterButton({
|
export class ClearFilterButtonScenesObject extends SceneObjectBase {
|
||||||
labelsFilterVariable,
|
public static Component = ClearFilterButtonObjectRenderer;
|
||||||
transitionsToFilterVariable,
|
|
||||||
transitionsFromFilterVariable,
|
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||||
}: {
|
variableNames: [LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO],
|
||||||
labelsFilterVariable: TextBoxVariable;
|
});
|
||||||
transitionsToFilterVariable: CustomVariable;
|
}
|
||||||
transitionsFromFilterVariable: CustomVariable;
|
|
||||||
}) {
|
export function ClearFilterButtonObjectRenderer({ model }: SceneComponentProps<ClearFilterButtonScenesObject>) {
|
||||||
// get the current values of the filters
|
// This make sure the component is re-rendered when the variables change
|
||||||
const valueInLabelsFilter = labelsFilterVariable.getValue();
|
model.useState();
|
||||||
//todo: use parsePromQLStyleMatcherLooseSafe to validate the label filter and check the lenghtof the result
|
|
||||||
const valueInTransitionsFilter = transitionsToFilterVariable.getValue();
|
const labelsFilter = sceneGraph.interpolate(model, '${LABELS_FILTER}');
|
||||||
const valueInTransitionsFromFilter = transitionsFromFilterVariable.getValue();
|
const stateTo = sceneGraph.interpolate(model, '${STATE_FILTER_TO}');
|
||||||
|
const stateFrom = sceneGraph.interpolate(model, '${STATE_FILTER_FROM}');
|
||||||
|
|
||||||
// if no filter is active, return null
|
// if no filter is active, return null
|
||||||
if (
|
if (!labelsFilter && stateTo === StateFilterValues.all && stateFrom === StateFilterValues.all) {
|
||||||
!valueInLabelsFilter &&
|
|
||||||
valueInTransitionsFilter === StateFilterValues.all &&
|
|
||||||
valueInTransitionsFromFilter === StateFilterValues.all
|
|
||||||
) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClearFilter = () => {
|
const onClearFilter = () => {
|
||||||
labelsFilterVariable.setValue('');
|
const labelsFiltersVariable = sceneGraph.lookupVariable(LABELS_FILTER, model);
|
||||||
transitionsToFilterVariable.changeValueTo(StateFilterValues.all);
|
if (labelsFiltersVariable instanceof TextBoxVariable) {
|
||||||
transitionsFromFilterVariable.changeValueTo(StateFilterValues.all);
|
labelsFiltersVariable.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateToFilterVariable = sceneGraph.lookupVariable(STATE_FILTER_TO, model);
|
||||||
|
if (stateToFilterVariable instanceof CustomVariable) {
|
||||||
|
stateToFilterVariable.changeValueTo(StateFilterValues.all);
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateFromFilterVariable = sceneGraph.lookupVariable(STATE_FILTER_FROM, model);
|
||||||
|
if (stateFromFilterVariable instanceof CustomVariable) {
|
||||||
|
stateFromFilterVariable.changeValueTo(StateFilterValues.all);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip content="Clear filter">
|
<Tooltip content="Clear filter">
|
||||||
<Button variant={'secondary'} icon="times" onClick={onClearFilter}>
|
<Button variant={'secondary'} icon="times" onClick={onClearFilter}>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
import { DataQuery, DataQueryRequest, DataQueryResponse, TestDataSourceResponse } from '@grafana/data';
|
import { DataQuery, DataQueryRequest, DataQueryResponse, TestDataSourceResponse } from '@grafana/data';
|
||||||
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
import { RuntimeDataSource, sceneUtils } from '@grafana/scenes';
|
import { RuntimeDataSource, sceneUtils } from '@grafana/scenes';
|
||||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { dispatch } from 'app/store/store';
|
import { dispatch } from 'app/store/store';
|
||||||
@ -9,7 +10,7 @@ import { stateHistoryApi } from '../../../api/stateHistoryApi';
|
|||||||
import { DataSourceInformation } from '../../../home/Insights';
|
import { DataSourceInformation } from '../../../home/Insights';
|
||||||
|
|
||||||
import { LIMIT_EVENTS } from './EventListSceneObject';
|
import { LIMIT_EVENTS } from './EventListSceneObject';
|
||||||
import { getStateFilterFromInQueryParams, getStateFilterToInQueryParams, historyResultToDataFrame } from './utils';
|
import { historyResultToDataFrame } from './utils';
|
||||||
|
|
||||||
const historyDataSourceUid = '__history_api_ds_uid__';
|
const historyDataSourceUid = '__history_api_ds_uid__';
|
||||||
const historyDataSourcePluginId = '__history_api_ds_pluginId__';
|
const historyDataSourcePluginId = '__history_api_ds_pluginId__';
|
||||||
@ -31,6 +32,12 @@ export function useRegisterHistoryRuntimeDataSource() {
|
|||||||
}, [ds]);
|
}, [ds]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface HistoryAPIQuery extends DataQuery {
|
||||||
|
labels?: string;
|
||||||
|
stateFrom?: string;
|
||||||
|
stateTo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class is a runtime datasource that fetches the events from the history api.
|
* This class is a runtime datasource that fetches the events from the history api.
|
||||||
* The events are grouped by alert instance and then converted to a DataFrame list.
|
* The events are grouped by alert instance and then converted to a DataFrame list.
|
||||||
@ -38,23 +45,28 @@ export function useRegisterHistoryRuntimeDataSource() {
|
|||||||
* This allows us to filter the events by labels.
|
* This allows us to filter the events by labels.
|
||||||
* The result is a timeseries panel that shows the events for the selected time range and filtered by labels.
|
* The result is a timeseries panel that shows the events for the selected time range and filtered by labels.
|
||||||
*/
|
*/
|
||||||
class HistoryAPIDatasource extends RuntimeDataSource {
|
class HistoryAPIDatasource extends RuntimeDataSource<HistoryAPIQuery> {
|
||||||
constructor(pluginId: string, uid: string) {
|
constructor(pluginId: string, uid: string) {
|
||||||
super(uid, pluginId);
|
super(uid, pluginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async query(request: DataQueryRequest<DataQuery>): Promise<DataQueryResponse> {
|
async query(request: DataQueryRequest<HistoryAPIQuery>): Promise<DataQueryResponse> {
|
||||||
const from = request.range.from.unix();
|
const from = request.range.from.unix();
|
||||||
const to = request.range.to.unix();
|
const to = request.range.to.unix();
|
||||||
|
// get the query from the request
|
||||||
|
const query = request.targets[0]!;
|
||||||
|
|
||||||
// Get the labels and states filters from the URL
|
const templateSrv = getTemplateSrv();
|
||||||
const stateTo = getStateFilterToInQueryParams();
|
|
||||||
const stateFrom = getStateFilterFromInQueryParams();
|
// we get the labels, stateTo and stateFrom from the query variables
|
||||||
|
const labels = templateSrv.replace(query.labels ?? '', request.scopedVars);
|
||||||
|
const stateTo = templateSrv.replace(query.stateTo ?? '', request.scopedVars);
|
||||||
|
const stateFrom = templateSrv.replace(query.stateFrom ?? '', request.scopedVars);
|
||||||
|
|
||||||
const historyResult = await getHistory(from, to);
|
const historyResult = await getHistory(from, to);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: historyResultToDataFrame(historyResult, { stateTo, stateFrom }),
|
data: historyResultToDataFrame(historyResult, { stateTo, stateFrom, labels }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
SceneComponentProps,
|
SceneComponentProps,
|
||||||
SceneObjectBase,
|
SceneObjectBase,
|
||||||
TextBoxVariable,
|
TextBoxVariable,
|
||||||
|
VariableDependencyConfig,
|
||||||
VariableValue,
|
VariableValue,
|
||||||
sceneGraph,
|
sceneGraph,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
@ -495,48 +496,54 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
|
|
||||||
export class HistoryEventsListObject extends SceneObjectBase {
|
export class HistoryEventsListObject extends SceneObjectBase {
|
||||||
public static Component = HistoryEventsListObjectRenderer;
|
public static Component = HistoryEventsListObjectRenderer;
|
||||||
public constructor() {
|
|
||||||
super({});
|
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||||
}
|
variableNames: [LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FilterType = 'label' | 'stateFrom' | 'stateTo';
|
export type FilterType = 'label' | 'stateFrom' | 'stateTo';
|
||||||
|
|
||||||
export function HistoryEventsListObjectRenderer({ model }: SceneComponentProps<HistoryEventsListObject>) {
|
export function HistoryEventsListObjectRenderer({ model }: SceneComponentProps<HistoryEventsListObject>) {
|
||||||
const { value: timeRange } = sceneGraph.getTimeRange(model).useState(); // get time range from scene graph
|
// This make sure the component is re-rendered when the variables change
|
||||||
// eslint-disable-next-line
|
model.useState();
|
||||||
const labelsFiltersVariable = sceneGraph.lookupVariable(LABELS_FILTER, model)! as TextBoxVariable;
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const stateToFilterVariable = sceneGraph.lookupVariable(STATE_FILTER_TO, model)! as CustomVariable;
|
|
||||||
// eslint-disable-next-line
|
|
||||||
const stateFromFilterVariable = sceneGraph.lookupVariable(STATE_FILTER_FROM, model)! as CustomVariable;
|
|
||||||
|
|
||||||
const valueInfilterTextBox: VariableValue = labelsFiltersVariable.getValue();
|
const { value: timeRange } = sceneGraph.getTimeRange(model).useState(); // get time range from scene graph
|
||||||
const valueInStateToFilter = stateToFilterVariable.getValue();
|
|
||||||
const valueInStateFromFilter = stateFromFilterVariable.getValue();
|
const labelsFiltersVariable = sceneGraph.lookupVariable(LABELS_FILTER, model);
|
||||||
|
const stateToFilterVariable = sceneGraph.lookupVariable(STATE_FILTER_TO, model);
|
||||||
|
const stateFromFilterVariable = sceneGraph.lookupVariable(STATE_FILTER_FROM, model);
|
||||||
|
|
||||||
const addFilter = (key: string, value: string, type: FilterType) => {
|
const addFilter = (key: string, value: string, type: FilterType) => {
|
||||||
const newFilterToAdd = `${key}=${value}`;
|
const newFilterToAdd = `${key}=${value}`;
|
||||||
trackUseCentralHistoryFilterByClicking({ type, key, value });
|
trackUseCentralHistoryFilterByClicking({ type, key, value });
|
||||||
if (type === 'stateTo') {
|
if (type === 'stateTo' && stateToFilterVariable instanceof CustomVariable) {
|
||||||
stateToFilterVariable.changeValueTo(value);
|
stateToFilterVariable.changeValueTo(value);
|
||||||
}
|
}
|
||||||
if (type === 'stateFrom') {
|
if (type === 'stateFrom' && stateFromFilterVariable instanceof CustomVariable) {
|
||||||
stateFromFilterVariable.changeValueTo(value);
|
stateFromFilterVariable.changeValueTo(value);
|
||||||
}
|
}
|
||||||
const finalFilter = combineMatcherStrings(valueInfilterTextBox.toString(), newFilterToAdd);
|
if (type === 'label' && labelsFiltersVariable instanceof TextBoxVariable) {
|
||||||
if (type === 'label') {
|
const finalFilter = combineMatcherStrings(labelsFiltersVariable.state.value.toString(), newFilterToAdd);
|
||||||
labelsFiltersVariable.setValue(finalFilter);
|
labelsFiltersVariable.setValue(finalFilter);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (
|
||||||
<HistoryEventsList
|
stateToFilterVariable instanceof CustomVariable &&
|
||||||
timeRange={timeRange}
|
stateFromFilterVariable instanceof CustomVariable &&
|
||||||
valueInLabelFilter={valueInfilterTextBox}
|
labelsFiltersVariable instanceof TextBoxVariable
|
||||||
addFilter={addFilter}
|
) {
|
||||||
valueInStateToFilter={valueInStateToFilter}
|
return (
|
||||||
valueInStateFromFilter={valueInStateFromFilter}
|
<HistoryEventsList
|
||||||
/>
|
timeRange={timeRange}
|
||||||
);
|
valueInLabelFilter={labelsFiltersVariable.state.value}
|
||||||
|
addFilter={addFilter}
|
||||||
|
valueInStateToFilter={stateToFilterVariable.state.value}
|
||||||
|
valueInStateFromFilter={stateFromFilterVariable.state.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ exports[`historyResultToDataFrame should decode 1`] = `
|
|||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`historyResultToDataFrame should decode and filter 1`] = `
|
exports[`historyResultToDataFrame should decode and filter example1 1`] = `
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -69,3 +69,34 @@ exports[`historyResultToDataFrame should decode and filter 1`] = `
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`historyResultToDataFrame should decode and filter example2 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"custom": {
|
||||||
|
"fillOpacity": 100,
|
||||||
|
},
|
||||||
|
"displayName": "Time",
|
||||||
|
},
|
||||||
|
"name": "time",
|
||||||
|
"type": "time",
|
||||||
|
"values": [
|
||||||
|
1727189670000,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"config": {},
|
||||||
|
"name": "value",
|
||||||
|
"type": "number",
|
||||||
|
"values": [
|
||||||
|
8,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"length": 1,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
|
@ -6,7 +6,18 @@ describe('historyResultToDataFrame', () => {
|
|||||||
expect(historyResultToDataFrame(fixtureData)).toMatchSnapshot();
|
expect(historyResultToDataFrame(fixtureData)).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should decode and filter', () => {
|
it('should decode and filter example1', () => {
|
||||||
expect(historyResultToDataFrame(fixtureData, { stateFrom: 'Pending', stateTo: 'Alerting' })).toMatchSnapshot();
|
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,9 +24,16 @@ import { LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO, StateFilterValues }
|
|||||||
const GROUPING_INTERVAL = 10 * 1000; // 10 seconds
|
const GROUPING_INTERVAL = 10 * 1000; // 10 seconds
|
||||||
const QUERY_PARAM_PREFIX = 'var-'; // Prefix used by Grafana to sync variables in the URL
|
const QUERY_PARAM_PREFIX = 'var-'; // Prefix used by Grafana to sync variables in the URL
|
||||||
|
|
||||||
const emptyFilters = {
|
interface HistoryFilters {
|
||||||
|
stateTo: string;
|
||||||
|
stateFrom: string;
|
||||||
|
labels: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyFilters: HistoryFilters = {
|
||||||
stateTo: 'all',
|
stateTo: 'all',
|
||||||
stateFrom: 'all',
|
stateFrom: 'all',
|
||||||
|
labels: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -75,7 +82,7 @@ export function historyResultToDataFrame({ data }: DataFrameJSON, filters = empt
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Group DataFrames by time and filter by labels
|
// Group DataFrames by time and filter by labels
|
||||||
return groupDataFramesByTimeAndFilterByLabels(dataFrames);
|
return groupDataFramesByTimeAndFilterByLabels(dataFrames, filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scenes sync variables in the URL adding a prefix to the variable name.
|
// Scenes sync variables in the URL adding a prefix to the variable name.
|
||||||
@ -98,9 +105,9 @@ export function getStateFilterFromInQueryParams() {
|
|||||||
* This function groups the data frames by time and filters them by labels.
|
* This function groups the data frames by time and filters them by labels.
|
||||||
* The interval is set to 10 seconds.
|
* The interval is set to 10 seconds.
|
||||||
* */
|
* */
|
||||||
function groupDataFramesByTimeAndFilterByLabels(dataFrames: DataFrame[]): DataFrame[] {
|
export function groupDataFramesByTimeAndFilterByLabels(dataFrames: DataFrame[], filters: HistoryFilters): DataFrame[] {
|
||||||
// Filter data frames by labels. This is used to filter out the data frames that do not match the query.
|
// Filter data frames by labels. This is used to filter out the data frames that do not match the query.
|
||||||
const labelsFilterValue = getLabelsFilterInQueryParams();
|
const labelsFilterValue = filters.labels;
|
||||||
const dataframesFiltered = dataFrames.filter((frame) => {
|
const dataframesFiltered = dataFrames.filter((frame) => {
|
||||||
const labels = JSON.parse(frame.name ?? ''); // in name we store the labels stringified
|
const labels = JSON.parse(frame.name ?? ''); // in name we store the labels stringified
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user