From dd9638cadeb98d826fa7dd2f6120706eb3b771a2 Mon Sep 17 00:00:00 2001 From: Sonia Aguilar <33540275+soniaAguilarPeiron@users.noreply.github.com> Date: Tue, 10 Dec 2024 13:09:42 +0100 Subject: [PATCH] Alerting: Improve performance ash page (#97619) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- .../CentralAlertHistoryScene.tsx | 234 +++++++++--------- .../CentralHistoryRuntimeDataSource.ts | 26 +- .../EventListSceneObject.tsx | 59 +++-- .../__snapshots__/utils.test.ts.snap | 33 ++- .../rules/central-state-history/utils.test.ts | 15 +- .../rules/central-state-history/utils.ts | 15 +- 6 files changed, 221 insertions(+), 161 deletions(-) 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 35a90efe365..2b4693d3a38 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 @@ -1,14 +1,16 @@ import { css } from '@emotion/css'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { GrafanaTheme2, VariableHide } from '@grafana/data'; import { CustomVariable, EmbeddedScene, PanelBuilders, + SceneComponentProps, SceneControlsSpacer, SceneFlexItem, SceneFlexLayout, + SceneObjectBase, SceneQueryRunner, SceneReactObject, SceneRefreshPicker, @@ -16,7 +18,9 @@ import { SceneTimeRange, SceneVariableSet, TextBoxVariable, + VariableDependencyConfig, VariableValueSelectors, + sceneGraph, useUrlSync, } from '@grafana/scenes'; import { GraphDrawStyle, VisibilityMode } from '@grafana/schema/dist/esm/index'; @@ -36,14 +40,13 @@ import { import { Trans } from 'app/core/internationalization'; import { LogMessages, logInfo } from '../../../Analytics'; -import { DataSourceInformation } from '../../../home/Insights'; import { alertStateHistoryDatasource, useRegisterHistoryRuntimeDataSource } from './CentralHistoryRuntimeDataSource'; import { HistoryEventsListObject } from './EventListSceneObject'; -export const LABELS_FILTER = 'labelsFilter'; -export const STATE_FILTER_TO = 'stateFilterTo'; -export const STATE_FILTER_FROM = 'stateFilterFrom'; +export const LABELS_FILTER = 'LABELS_FILTER'; +export const STATE_FILTER_TO = 'STATE_FILTER_TO'; +export const STATE_FILTER_FROM = 'STATE_FILTER_FROM'; /** * * This scene shows the history of the alert state changes. @@ -67,74 +70,72 @@ export const CentralAlertHistoryScene = () => { 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. - const scene = new EmbeddedScene({ - controls: [ - new SceneReactObject({ - component: LabelFilter, - }), - new SceneReactObject({ - component: FilterInfo, - }), - new VariableValueSelectors({}), - new SceneReactObject({ - component: ClearFilterButton, - props: { - labelsFilterVariable, - transitionsToFilterVariable, - transitionsFromFilterVariable, - }, - }), - 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(alertStateHistoryDatasource), + const scene = useMemo(() => { + // 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}`, + }); + + return new EmbeddedScene({ + controls: [ + new SceneReactObject({ + component: LabelFilter, }), - new SceneFlexItem({ - body: new HistoryEventsListObject(), + new SceneReactObject({ + 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 const isUrlSyncInitialized = useUrlSync(scene); @@ -147,22 +148,11 @@ export const CentralAlertHistoryScene = () => { /** * 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. - * @param alertStateHistoryDataSource the datasource information for the runtime datasource */ -function getEventsSceneObject(alertStateHistoryDataSource: DataSourceInformation) { - return new EmbeddedScene({ - controls: [], - body: new SceneFlexLayout({ - direction: 'column', - children: [ - new SceneFlexItem({ - ySizing: 'content', - body: new SceneFlexLayout({ - children: [getEventsScenesFlexItem(alertStateHistoryDataSource)], - }), - }), - ], - }), +function getEventsSceneObject() { + return new SceneFlexLayout({ + direction: 'column', + children: [getEventsScenesFlexItem()], }); } @@ -171,15 +161,15 @@ function getEventsSceneObject(alertStateHistoryDataSource: DataSourceInformation * @param datasource the datasource information for the runtime datasource * @returns the SceneQueryRunner */ -function getSceneQuery(datasource: DataSourceInformation) { +function getQueryRunnerForAlertHistoryDataSource() { const query = new SceneQueryRunner({ - datasource: datasource, + datasource: alertStateHistoryDatasource, queries: [ { refId: 'A', - expr: '', - queryType: 'range', - step: '10s', + labels: '${LABELS_FILTER}', + stateFrom: '${STATE_FILTER_FROM}', + 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. * 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({ minHeight: 300, body: PanelBuilders.timeseries() @@ -197,7 +187,7 @@ export function getEventsScenesFlexItem(datasource: DataSourceInformation) { .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.' ) - .setData(getSceneQuery(datasource)) + .setData(getQueryRunnerForAlertHistoryDataSource()) .setColor({ mode: 'continuous-BlPu' }) .setCustomFieldConfig('fillOpacity', 100) .setCustomFieldConfig('drawStyle', GraphDrawStyle.Bars) @@ -213,47 +203,49 @@ export function getEventsScenesFlexItem(datasource: DataSourceInformation) { .setCustomFieldConfig('scaleDistribution', { type: ScaleDistribution.Linear }) .setOption('legend', { showLegend: false, displayMode: LegendDisplayMode.Hidden }) .setOption('tooltip', { mode: TooltipDisplayMode.Single }) - .setNoValue('No events found') .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({ - labelsFilterVariable, - transitionsToFilterVariable, - transitionsFromFilterVariable, -}: { - labelsFilterVariable: TextBoxVariable; - transitionsToFilterVariable: CustomVariable; - transitionsFromFilterVariable: CustomVariable; -}) { - // get the current values of the filters - const valueInLabelsFilter = labelsFilterVariable.getValue(); - //todo: use parsePromQLStyleMatcherLooseSafe to validate the label filter and check the lenghtof the result - const valueInTransitionsFilter = transitionsToFilterVariable.getValue(); - const valueInTransitionsFromFilter = transitionsFromFilterVariable.getValue(); +export class ClearFilterButtonScenesObject extends SceneObjectBase { + public static Component = ClearFilterButtonObjectRenderer; + + protected _variableDependency = new VariableDependencyConfig(this, { + variableNames: [LABELS_FILTER, STATE_FILTER_FROM, STATE_FILTER_TO], + }); +} + +export function ClearFilterButtonObjectRenderer({ model }: SceneComponentProps) { + // This make sure the component is re-rendered when the variables change + model.useState(); + + const labelsFilter = sceneGraph.interpolate(model, '${LABELS_FILTER}'); + const stateTo = sceneGraph.interpolate(model, '${STATE_FILTER_TO}'); + const stateFrom = sceneGraph.interpolate(model, '${STATE_FILTER_FROM}'); + // if no filter is active, return null - if ( - !valueInLabelsFilter && - valueInTransitionsFilter === StateFilterValues.all && - valueInTransitionsFromFilter === StateFilterValues.all - ) { + if (!labelsFilter && stateTo === StateFilterValues.all && stateFrom === StateFilterValues.all) { return null; } + const onClearFilter = () => { - labelsFilterVariable.setValue(''); - transitionsToFilterVariable.changeValueTo(StateFilterValues.all); - transitionsFromFilterVariable.changeValueTo(StateFilterValues.all); + const labelsFiltersVariable = sceneGraph.lookupVariable(LABELS_FILTER, model); + if (labelsFiltersVariable instanceof TextBoxVariable) { + 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 (