From 12dcba5d0b2de236ff43867574b4711a7646cf3c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 15 Feb 2021 13:56:59 +0100 Subject: [PATCH] AlertingNG: Test definition (#30886) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * break out new and edit * changed model to match new model in backend * AlertingNG: API modifications (#30683) * Fix API consistency * Change eval alert definition to POST request * Fix eval endpoint to accept custom now parameter * Change JSON input property for create/update endpoints * model adjustments * set mixed datasource, fix put url * update snapshots * run test response through converters * remove edit and add landing page * remove snapshot tests ans snapshots * wrap linkbutton in array * different approaches to massage data * get instead of post * use function to return instances data * hook up test button in view * test endpoint for not saved definitions * function that return query options * Chore: fixes strict error * hide ng alert button * typings * fix setAlertDef error * better message when you have queries but no data * NGAlert: Refactoring that handles cleaning up state (#31087) * Chore: some refactorings of state * Chore: reduces strict null errors Co-authored-by: Sofia Papagiannaki Co-authored-by: Sofia Papagiannaki Co-authored-by: Hugo Häggmark Co-authored-by: Hugo Häggmark --- .../features/alerting/AlertRuleList.test.tsx | 7 +- .../app/features/alerting/AlertRuleList.tsx | 65 ++++---- .../features/alerting/NextGenAlertingPage.tsx | 129 +++++++++------ .../components/AlertDefinitionOptions.tsx | 2 +- .../components/AlertingQueryEditor.tsx | 47 +++--- .../components/AlertingQueryPreview.tsx | 36 +++- .../components/PreviewInstancesTab.tsx | 15 +- .../alerting/components/PreviewQueryTab.tsx | 19 ++- public/app/features/alerting/state/actions.ts | 155 +++++++++++++----- .../app/features/alerting/state/reducers.ts | 67 +++++--- public/app/types/alerting.ts | 7 +- 11 files changed, 341 insertions(+), 208 deletions(-) diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index 8461c67afea..a081712effc 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { AlertRuleList, Props } from './AlertRuleList'; +import { AlertRuleListUnconnected, Props } from './AlertRuleList'; import { AlertRule } from '../../types'; import appEvents from '../../core/app_events'; import { NavModel } from '@grafana/data'; @@ -24,15 +24,16 @@ const setup = (propOverrides?: object) => { stateFilter: '', search: '', isLoading: false, + ngAlertDefinitions: [], }; Object.assign(props, propOverrides); - const wrapper = shallow(); + const wrapper = shallow(); return { wrapper, - instance: wrapper.instance() as AlertRuleList, + instance: wrapper.instance() as AlertRuleListUnconnected, }; }; diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 2e003de4fc5..1fc5ba02a3f 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; -import { connect } from 'react-redux'; +import { connect, ConnectedProps } from 'react-redux'; import Page from 'app/core/components/Page/Page'; import AlertRuleItem from './AlertRuleItem'; import appEvents from 'app/core/app_events'; @@ -10,24 +10,37 @@ import { AlertDefinition, AlertRule, CoreEvents, StoreState } from 'app/types'; import { getAlertRulesAsync, togglePauseAlertRule } from './state/actions'; import { getAlertRuleItems, getSearchQuery } from './state/selectors'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; -import { NavModel, SelectableValue } from '@grafana/data'; +import { SelectableValue } from '@grafana/data'; +import { config } from '@grafana/runtime'; import { setSearchQuery } from './state/reducers'; import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui'; import { AlertDefinitionItem } from './components/AlertDefinitionItem'; -export interface Props { - navModel: NavModel; - alertRules: Array; - updateLocation: typeof updateLocation; - getAlertRulesAsync: typeof getAlertRulesAsync; - setSearchQuery: typeof setSearchQuery; - togglePauseAlertRule: typeof togglePauseAlertRule; - stateFilter: string; - search: string; - isLoading: boolean; +function mapStateToProps(state: StoreState) { + return { + navModel: getNavModel(state.navIndex, 'alert-list'), + alertRules: getAlertRuleItems(state), + stateFilter: state.location.query.state, + search: getSearchQuery(state.alertRules), + isLoading: state.alertRules.isLoading, + ngAlertDefinitions: state.alertDefinition.alertDefinitions, + }; } -export class AlertRuleList extends PureComponent { +const mapDispatchToProps = { + updateLocation, + getAlertRulesAsync, + setSearchQuery, + togglePauseAlertRule, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +interface OwnProps {} + +export type Props = OwnProps & ConnectedProps; + +export class AlertRuleListUnconnected extends PureComponent { stateFilters = [ { label: 'All', value: 'all' }, { label: 'OK', value: 'ok' }, @@ -118,9 +131,11 @@ export class AlertRuleList extends PureComponent {
- - Add NG Alert - + {config.featureToggles.ngalert && ( + + Add NG Alert + + )} @@ -153,20 +168,4 @@ export class AlertRuleList extends PureComponent { } } -const mapStateToProps = (state: StoreState) => ({ - navModel: getNavModel(state.navIndex, 'alert-list'), - alertRules: getAlertRuleItems(state), - stateFilter: state.location.query.state, - search: getSearchQuery(state.alertRules), - isLoading: state.alertRules.isLoading, - ngAlertDefinitions: state.alertDefinition.alertDefinitions, -}); - -const mapDispatchToProps = { - updateLocation, - getAlertRulesAsync, - setSearchQuery, - togglePauseAlertRule, -}; - -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(AlertRuleList)); +export default hot(module)(connector(AlertRuleListUnconnected)); diff --git a/public/app/features/alerting/NextGenAlertingPage.tsx b/public/app/features/alerting/NextGenAlertingPage.tsx index c872d1cdeef..c53d0e57397 100644 --- a/public/app/features/alerting/NextGenAlertingPage.tsx +++ b/public/app/features/alerting/NextGenAlertingPage.tsx @@ -1,49 +1,63 @@ import React, { FormEvent, PureComponent } from 'react'; import { hot } from 'react-hot-loader'; -import { MapDispatchToProps, MapStateToProps } from 'react-redux'; +import { connect, ConnectedProps } from 'react-redux'; import { css } from 'emotion'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { PageToolbar, stylesFactory, ToolbarButton } from '@grafana/ui'; + import { config } from 'app/core/config'; import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; -import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp'; -import AlertingQueryEditor from './components/AlertingQueryEditor'; +import { AlertingQueryEditor } from './components/AlertingQueryEditor'; import { AlertDefinitionOptions } from './components/AlertDefinitionOptions'; import { AlertingQueryPreview } from './components/AlertingQueryPreview'; import { - updateAlertDefinitionOption, + cleanUpDefinitionState, createAlertDefinition, - updateAlertDefinitionUiState, - updateAlertDefinition, + evaluateAlertDefinition, + evaluateNotSavedAlertDefinition, getAlertDefinition, + onRunQueries, + updateAlertDefinition, + updateAlertDefinitionOption, + updateAlertDefinitionUiState, } from './state/actions'; import { getRouteParamsId } from 'app/core/selectors/location'; -import { AlertDefinition, AlertDefinitionUiState, QueryGroupOptions, StoreState } from '../../types'; -import { PanelQueryRunner } from '../query/state/PanelQueryRunner'; +import { StoreState } from 'app/types'; + +function mapStateToProps(state: StoreState) { + const pageId = getRouteParamsId(state.location); + + return { + uiState: state.alertDefinition.uiState, + getQueryOptions: state.alertDefinition.getQueryOptions, + queryRunner: state.alertDefinition.queryRunner, + getInstances: state.alertDefinition.getInstances, + alertDefinition: state.alertDefinition.alertDefinition, + pageId: (pageId as string) ?? '', + }; +} + +const mapDispatchToProps = { + updateAlertDefinitionUiState, + updateAlertDefinitionOption, + evaluateAlertDefinition, + updateAlertDefinition, + createAlertDefinition, + getAlertDefinition, + evaluateNotSavedAlertDefinition, + onRunQueries, + cleanUpDefinitionState, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); interface OwnProps { saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition; } -interface ConnectedProps { - uiState: AlertDefinitionUiState; - queryRunner: PanelQueryRunner; - queryOptions: QueryGroupOptions; - alertDefinition: AlertDefinition; - pageId: string; -} +type Props = OwnProps & ConnectedProps; -interface DispatchProps { - updateAlertDefinitionUiState: typeof updateAlertDefinitionUiState; - updateAlertDefinitionOption: typeof updateAlertDefinitionOption; - getAlertDefinition: typeof getAlertDefinition; - updateAlertDefinition: typeof updateAlertDefinition; - createAlertDefinition: typeof createAlertDefinition; -} - -type Props = OwnProps & ConnectedProps & DispatchProps; - -class NextGenAlertingPage extends PureComponent { +class NextGenAlertingPageUnconnected extends PureComponent { componentDidMount() { const { getAlertDefinition, pageId } = this.props; @@ -52,8 +66,13 @@ class NextGenAlertingPage extends PureComponent { } } - onChangeAlertOption = (event: FormEvent) => { - this.props.updateAlertDefinitionOption({ [event.currentTarget.name]: event.currentTarget.value }); + componentWillUnmount() { + this.props.cleanUpDefinitionState(); + } + + onChangeAlertOption = (event: FormEvent) => { + const formEvent = event as FormEvent; + this.props.updateAlertDefinitionOption({ [formEvent.currentTarget.name]: formEvent.currentTarget.value }); }; onChangeInterval = (interval: SelectableValue) => { @@ -80,7 +99,14 @@ class NextGenAlertingPage extends PureComponent { onDiscard = () => {}; - onTest = () => {}; + onTest = () => { + const { alertDefinition, evaluateAlertDefinition, evaluateNotSavedAlertDefinition } = this.props; + if (alertDefinition.uid) { + evaluateAlertDefinition(); + } else { + evaluateNotSavedAlertDefinition(); + } + }; renderToolbarActions() { return [ @@ -97,8 +123,18 @@ class NextGenAlertingPage extends PureComponent { } render() { - const { alertDefinition, uiState, updateAlertDefinitionUiState, queryRunner, queryOptions } = this.props; + const { + alertDefinition, + getInstances, + uiState, + updateAlertDefinitionUiState, + queryRunner, + getQueryOptions, + onRunQueries, + } = this.props; + const styles = getStyles(config.theme); + const queryOptions = getQueryOptions(); return (
@@ -108,7 +144,14 @@ class NextGenAlertingPage extends PureComponent {
, + , , ]} uiState={uiState} @@ -129,29 +172,7 @@ class NextGenAlertingPage extends PureComponent { } } -const mapStateToProps: MapStateToProps = (state) => { - const pageId = getRouteParamsId(state.location); - - return { - uiState: state.alertDefinition.uiState, - queryOptions: state.alertDefinition.queryOptions, - queryRunner: state.alertDefinition.queryRunner, - alertDefinition: state.alertDefinition.alertDefinition, - pageId: (pageId as string) ?? '', - }; -}; - -const mapDispatchToProps: MapDispatchToProps = { - updateAlertDefinitionUiState, - updateAlertDefinitionOption, - updateAlertDefinition, - createAlertDefinition, - getAlertDefinition, -}; - -export default hot(module)( - connectWithCleanUp(mapStateToProps, mapDispatchToProps, (state) => state.alertDefinition)(NextGenAlertingPage) -); +export default hot(module)(connector(NextGenAlertingPageUnconnected)); const getStyles = stylesFactory((theme: GrafanaTheme) => ({ wrapper: css` diff --git a/public/app/features/alerting/components/AlertDefinitionOptions.tsx b/public/app/features/alerting/components/AlertDefinitionOptions.tsx index c929960a5ab..05a47d86c5f 100644 --- a/public/app/features/alerting/components/AlertDefinitionOptions.tsx +++ b/public/app/features/alerting/components/AlertDefinitionOptions.tsx @@ -12,7 +12,7 @@ const intervalOptions: Array> = [ interface Props { alertDefinition: AlertDefinition; - onChange: (event: FormEvent) => void; + onChange: (event: FormEvent) => void; onIntervalChange: (interval: SelectableValue) => void; onConditionChange: (refId: SelectableValue) => void; queryOptions: QueryGroupOptions; diff --git a/public/app/features/alerting/components/AlertingQueryEditor.tsx b/public/app/features/alerting/components/AlertingQueryEditor.tsx index d2eb103d914..0251ac4f942 100644 --- a/public/app/features/alerting/components/AlertingQueryEditor.tsx +++ b/public/app/features/alerting/components/AlertingQueryEditor.tsx @@ -1,28 +1,33 @@ import React, { PureComponent } from 'react'; -import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; +import { connect, ConnectedProps } from 'react-redux'; import { css } from 'emotion'; import { GrafanaTheme } from '@grafana/data'; import { RefreshPicker, stylesFactory } from '@grafana/ui'; + import { config } from 'app/core/config'; import { QueryGroup } from '../../query/components/QueryGroup'; -import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { onRunQueries, queryOptionsChange } from '../state/actions'; import { QueryGroupOptions, StoreState } from 'app/types'; +function mapStateToProps(state: StoreState) { + return { + queryOptions: state.alertDefinition.getQueryOptions(), + queryRunner: state.alertDefinition.queryRunner, + }; +} + +const mapDispatchToProps = { + queryOptionsChange, + onRunQueries, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + interface OwnProps {} -interface ConnectedProps { - queryOptions: QueryGroupOptions; - queryRunner: PanelQueryRunner; -} -interface DispatchProps { - queryOptionsChange: typeof queryOptionsChange; - onRunQueries: typeof onRunQueries; -} +type Props = OwnProps & ConnectedProps; -type Props = ConnectedProps & DispatchProps & OwnProps; - -export class AlertingQueryEditor extends PureComponent { +class AlertingQueryEditorUnconnected extends PureComponent { onQueryOptionsChange = (queryOptions: QueryGroupOptions) => { this.props.queryOptionsChange(queryOptions); }; @@ -51,7 +56,7 @@ export class AlertingQueryEditor extends PureComponent { />
{ } } -const mapStateToProps: MapStateToProps = (state) => { - return { - queryOptions: state.alertDefinition.queryOptions, - queryRunner: state.alertDefinition.queryRunner, - }; -}; - -const mapDispatchToProps: MapDispatchToProps = { - queryOptionsChange, - onRunQueries, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(AlertingQueryEditor); +export const AlertingQueryEditor = connector(AlertingQueryEditorUnconnected); const getStyles = stylesFactory((theme: GrafanaTheme) => { return { diff --git a/public/app/features/alerting/components/AlertingQueryPreview.tsx b/public/app/features/alerting/components/AlertingQueryPreview.tsx index b6d4887641d..bc947d88978 100644 --- a/public/app/features/alerting/components/AlertingQueryPreview.tsx +++ b/public/app/features/alerting/components/AlertingQueryPreview.tsx @@ -2,8 +2,8 @@ import React, { FC, useMemo, useState } from 'react'; import { useObservable } from 'react-use'; import { css } from 'emotion'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { GrafanaTheme } from '@grafana/data'; -import { TabsBar, TabContent, Tab, useStyles, Icon } from '@grafana/ui'; +import { DataFrame, DataQuery, GrafanaTheme, PanelData } from '@grafana/data'; +import { Button, Icon, Tab, TabContent, TabsBar, useStyles } from '@grafana/ui'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { PreviewQueryTab } from './PreviewQueryTab'; import { PreviewInstancesTab } from './PreviewInstancesTab'; @@ -20,14 +20,19 @@ const tabs = [ interface Props { queryRunner: PanelQueryRunner; + getInstances: () => DataFrame[]; + queries: DataQuery[]; + onTest: () => void; + onRunQueries: () => void; } -export const AlertingQueryPreview: FC = ({ queryRunner }) => { +export const AlertingQueryPreview: FC = ({ getInstances, onRunQueries, onTest, queryRunner, queries }) => { const [activeTab, setActiveTab] = useState(Tabs.Query); const styles = useStyles(getStyles); const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []); - const data = useObservable(observable); + const data = useObservable(observable); + const instances = getInstances(); return (
@@ -49,16 +54,33 @@ export const AlertingQueryPreview: FC = ({ queryRunner }) => {

There was an error :(

{data.error?.data?.error}
- ) : data && data.series.length > 0 ? ( + ) : queries && queries.length > 0 ? ( {({ width, height }) => { switch (activeTab) { case Tabs.Instances: - return ; + return ( + 0} + instances={instances} + styles={styles} + width={width} + height={height} + onTest={onTest} + /> + ); case Tabs.Query: default: - return ; + if (data) { + return ; + } + return ( +
+

Run queries to view data.

+ +
+ ); } }}
diff --git a/public/app/features/alerting/components/PreviewInstancesTab.tsx b/public/app/features/alerting/components/PreviewInstancesTab.tsx index 59bce004eee..438c1e8d3d6 100644 --- a/public/app/features/alerting/components/PreviewInstancesTab.tsx +++ b/public/app/features/alerting/components/PreviewInstancesTab.tsx @@ -1,23 +1,26 @@ import React, { FC } from 'react'; -import { PanelData } from '@grafana/data'; -import { Button } from '@grafana/ui'; +import { DataFrame } from '@grafana/data'; +import { Button, Table } from '@grafana/ui'; import { PreviewStyles } from './AlertingQueryPreview'; interface Props { - data: PanelData; + instances: DataFrame[]; isTested: boolean; styles: PreviewStyles; + width: number; + height: number; + onTest: () => void; } -export const PreviewInstancesTab: FC = ({ data, isTested, styles }) => { +export const PreviewInstancesTab: FC = ({ instances, isTested, onTest, height, styles, width }) => { if (!isTested) { return (

You haven’t tested your alert yet.

In order to see your instances, you need to test your alert first.
- +
); } - return
Instances
; + return ; }; diff --git a/public/app/features/alerting/components/PreviewQueryTab.tsx b/public/app/features/alerting/components/PreviewQueryTab.tsx index 7de0f0eec02..e9d3e4dad45 100644 --- a/public/app/features/alerting/components/PreviewQueryTab.tsx +++ b/public/app/features/alerting/components/PreviewQueryTab.tsx @@ -1,10 +1,10 @@ import React, { FC, useMemo, useState } from 'react'; -import { getFrameDisplayName, GrafanaTheme, PanelData } from '@grafana/data'; +import { getFrameDisplayName, GrafanaTheme, PanelData, SelectableValue, toDataFrame } from '@grafana/data'; import { Select, stylesFactory, Table, useTheme } from '@grafana/ui'; import { css } from 'emotion'; interface Props { - data: PanelData; + data?: PanelData; width: number; height: number; } @@ -13,14 +13,21 @@ export const PreviewQueryTab: FC = ({ data, height, width }) => { const [currentSeries, setSeries] = useState(0); const theme = useTheme(); const styles = getStyles(theme, height); - const series = useMemo( - () => data.series.map((frame, index) => ({ value: index, label: getFrameDisplayName(frame) })), - [data.series] - ); + const series = useMemo>>(() => { + if (data?.series) { + return data.series.map((frame, index) => ({ value: index, label: getFrameDisplayName(frame) })); + } + + return []; + }, [data]); // Select padding const padding = 16; + if (!data?.series?.length) { + return
; + } + if (data.series.length > 1) { return (
diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index cc94c6005e6..44770313c35 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -1,29 +1,38 @@ -import { AppEvents, dateMath } from '@grafana/data'; +import { + AppEvents, + applyFieldOverrides, + arrowTableToDataFrame, + base64StringToArrowTable, + DataSourceApi, + dateMath, +} from '@grafana/data'; import { config, getBackendSrv, getDataSourceSrv } from '@grafana/runtime'; import { appEvents } from 'app/core/core'; import { updateLocation } from 'app/core/actions'; import store from 'app/core/store'; import { - notificationChannelLoaded, + ALERT_DEFINITION_UI_STATE_STORAGE_KEY, + cleanUpState, loadAlertRules, loadedAlertRules, - setNotificationChannels, - setUiState, - ALERT_DEFINITION_UI_STATE_STORAGE_KEY, - updateAlertDefinitionOptions, - setQueryOptions, - setAlertDefinitions, + notificationChannelLoaded, setAlertDefinition, + setAlertDefinitions, + setInstanceData, + setNotificationChannels, + setQueryOptions, + setUiState, + updateAlertDefinitionOptions, } from './reducers'; import { AlertDefinition, + AlertDefinitionState, AlertDefinitionUiState, AlertRuleDTO, NotifierDTO, - ThunkResult, - QueryGroupOptions, QueryGroupDataSource, - AlertDefinitionState, + QueryGroupOptions, + ThunkResult, } from 'app/types'; import { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource'; import { ExpressionQuery } from '../../expressions/types'; @@ -161,10 +170,12 @@ export function queryOptionsChange(queryOptions: QueryGroupOptions): ThunkResult export function onRunQueries(): ThunkResult { return (dispatch, getStore) => { - const { queryRunner, queryOptions } = getStore().alertDefinition; + const { queryRunner, getQueryOptions } = getStore().alertDefinition; const timeRange = { from: 'now-1h', to: 'now' }; + const queryOptions = getQueryOptions(); - queryRunner.run({ + queryRunner!.run({ + // if the queryRunner is undefined here somethings very wrong so it's ok to throw an unhandled error timezone: 'browser', timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange }, maxDataPoints: queryOptions.maxDataPoints ?? 100, @@ -175,41 +186,99 @@ export function onRunQueries(): ThunkResult { }; } +export function evaluateAlertDefinition(): ThunkResult { + return async (dispatch, getStore) => { + const { alertDefinition } = getStore().alertDefinition; + + const response: { instances: string[] } = await getBackendSrv().get( + `/api/alert-definitions/eval/${alertDefinition.uid}` + ); + + const handledResponse = handleBase64Response(response.instances); + + dispatch(setInstanceData(handledResponse)); + appEvents.emit(AppEvents.alertSuccess, ['Alert definition tested successfully']); + }; +} + +export function evaluateNotSavedAlertDefinition(): ThunkResult { + return async (dispatch, getStore) => { + const { alertDefinition, getQueryOptions } = getStore().alertDefinition; + const defaultDataSource = await getDataSourceSrv().get(null); + + const response: { instances: string[] } = await getBackendSrv().post('/api/alert-definitions/eval', { + condition: alertDefinition.condition, + data: buildDataQueryModel(getQueryOptions(), defaultDataSource), + }); + + const handledResponse = handleBase64Response(response.instances); + dispatch(setInstanceData(handledResponse)); + appEvents.emit(AppEvents.alertSuccess, ['Alert definition tested successfully']); + }; +} + +export function cleanUpDefinitionState(): ThunkResult { + return (dispatch) => { + dispatch(cleanUpState(undefined)); + }; +} + async function buildAlertDefinition(state: AlertDefinitionState) { - const queryOptions = state.queryOptions; + const queryOptions = state.getQueryOptions(); const currentAlertDefinition = state.alertDefinition; const defaultDataSource = await getDataSourceSrv().get(null); return { ...currentAlertDefinition, - data: queryOptions.queries.map((query) => { - let dataSource: QueryGroupDataSource; - const isExpression = query.datasource === ExpressionDatasourceID; - - if (isExpression) { - dataSource = { name: ExpressionDatasourceID, uid: ExpressionDatasourceID }; - } else { - const dataSourceSetting = getDataSourceSrv().getInstanceSettings(query.datasource); - - dataSource = { - name: dataSourceSetting?.name ?? defaultDataSource.name, - uid: dataSourceSetting?.uid ?? defaultDataSource.uid, - }; - } - - return { - model: { - ...query, - type: isExpression ? (query as ExpressionQuery).type : query.queryType, - datasource: dataSource.name, - datasourceUid: dataSource.uid, - }, - refId: query.refId, - relativeTimeRange: { - From: 500, - To: 0, - }, - }; - }), + data: buildDataQueryModel(queryOptions, defaultDataSource), }; } + +function handleBase64Response(frames: string[]) { + const dataFrames = frames.map((instance) => { + const table = base64StringToArrowTable(instance); + return arrowTableToDataFrame(table); + }); + + return applyFieldOverrides({ + data: dataFrames, + fieldConfig: { + defaults: {}, + overrides: [], + }, + replaceVariables: (value: any) => value, + theme: config.theme, + }); +} + +function buildDataQueryModel(queryOptions: QueryGroupOptions, defaultDataSource: DataSourceApi) { + return queryOptions.queries.map((query) => { + let dataSource: QueryGroupDataSource; + const isExpression = query.datasource === ExpressionDatasourceID; + + if (isExpression) { + dataSource = { name: ExpressionDatasourceID, uid: ExpressionDatasourceID }; + } else { + const dataSourceSetting = getDataSourceSrv().getInstanceSettings(query.datasource); + + dataSource = { + name: dataSourceSetting?.name ?? defaultDataSource.name, + uid: dataSourceSetting?.uid ?? defaultDataSource.uid, + }; + } + + return { + model: { + ...query, + type: isExpression ? (query as ExpressionQuery).type : query.queryType, + datasource: dataSource.name, + datasourceUid: dataSource.uid, + }, + refId: query.refId, + relativeTimeRange: { + From: 500, + To: 0, + }, + }; + }); +} diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index 79407f55ce9..36ca8e90324 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -1,5 +1,5 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { ApplyFieldOverrideOptions, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data'; +import { ApplyFieldOverrideOptions, DataFrame, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data'; import alertDef from './alertDef'; import { AlertDefinition, @@ -62,11 +62,14 @@ export const initialAlertDefinitionState: AlertDefinitionState = { data: [], intervalSeconds: 60, }, - queryOptions: { maxDataPoints: 100, dataSource: { name: '-- Mixed --' }, queries: [] }, queryRunner: new PanelQueryRunner(dataConfig), uiState: { ...store.getObject(ALERT_DEFINITION_UI_STATE_STORAGE_KEY, DEFAULT_ALERT_DEFINITION_UI_STATE) }, data: [], alertDefinitions: [] as AlertDefinition[], + /* These are functions as they are mutated later on and redux toolkit will Object.freeze state so + * we need to store these using functions instead */ + getInstances: () => [] as DataFrame[], + getQueryOptions: () => ({ maxDataPoints: 100, dataSource: { name: '-- Mixed --' }, queries: [] }), }; function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule { @@ -160,37 +163,49 @@ const alertDefinitionSlice = createSlice({ initialState: initialAlertDefinitionState, reducers: { setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction) => { - return { - ...state, - alertDefinition: { - title: action.payload.title, - id: action.payload.id, - uid: action.payload.uid, - condition: action.payload.condition, - intervalSeconds: action.payload.intervalSeconds, - data: action.payload.data, - description: '', - }, - queryOptions: { - ...state.queryOptions, - queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })), - }, - }; + const currentOptions = state.getQueryOptions(); + + state.alertDefinition.title = action.payload.title; + state.alertDefinition.id = action.payload.id; + state.alertDefinition.uid = action.payload.uid; + state.alertDefinition.condition = action.payload.condition; + state.alertDefinition.intervalSeconds = action.payload.intervalSeconds; + state.alertDefinition.data = action.payload.data; + state.alertDefinition.description = action.payload.description; + state.getQueryOptions = () => ({ + ...currentOptions, + queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })), + }); }, updateAlertDefinitionOptions: (state: AlertDefinitionState, action: PayloadAction>) => { - return { ...state, alertDefinition: { ...state.alertDefinition, ...action.payload } }; + state.alertDefinition = { ...state.alertDefinition, ...action.payload }; }, setUiState: (state: AlertDefinitionState, action: PayloadAction) => { - return { ...state, uiState: { ...state.uiState, ...action.payload } }; + state.uiState = { ...state.uiState, ...action.payload }; }, setQueryOptions: (state: AlertDefinitionState, action: PayloadAction) => { - return { - ...state, - queryOptions: action.payload, - }; + state.getQueryOptions = () => action.payload; }, setAlertDefinitions: (state: AlertDefinitionState, action: PayloadAction) => { - return { ...state, alertDefinitions: action.payload }; + state.alertDefinitions = action.payload; + }, + setInstanceData: (state: AlertDefinitionState, action: PayloadAction) => { + state.getInstances = () => action.payload; + }, + cleanUpState: (state: AlertDefinitionState, action: PayloadAction) => { + if (state.queryRunner) { + state.queryRunner.destroy(); + state.queryRunner = undefined; + delete state.queryRunner; + state.queryRunner = new PanelQueryRunner(dataConfig); + } + + state.alertDefinitions = initialAlertDefinitionState.alertDefinitions; + state.alertDefinition = initialAlertDefinitionState.alertDefinition; + state.data = initialAlertDefinitionState.data; + state.getInstances = initialAlertDefinitionState.getInstances; + state.getQueryOptions = initialAlertDefinitionState.getQueryOptions; + state.uiState = initialAlertDefinitionState.uiState; }, }, }); @@ -209,6 +224,8 @@ export const { setQueryOptions, setAlertDefinitions, setAlertDefinition, + setInstanceData, + cleanUpState, } = alertDefinitionSlice.actions; export const alertRulesReducer = alertRulesSlice.reducer; diff --git a/public/app/types/alerting.ts b/public/app/types/alerting.ts index 71c04156f03..f34490f2f2f 100644 --- a/public/app/types/alerting.ts +++ b/public/app/types/alerting.ts @@ -1,4 +1,4 @@ -import { DataQuery, PanelData, SelectableValue, TimeRange } from '@grafana/data'; +import { DataFrame, DataQuery, PanelData, SelectableValue, TimeRange } from '@grafana/data'; import { PanelQueryRunner } from '../features/query/state/PanelQueryRunner'; import { QueryGroupOptions } from './query'; import { ExpressionQuery } from '../features/expressions/types'; @@ -140,10 +140,11 @@ export interface AlertNotification { export interface AlertDefinitionState { uiState: AlertDefinitionUiState; alertDefinition: AlertDefinition; - queryOptions: QueryGroupOptions; - queryRunner: PanelQueryRunner; + queryRunner?: PanelQueryRunner; data: PanelData[]; alertDefinitions: AlertDefinition[]; + getInstances: () => DataFrame[]; + getQueryOptions: () => QueryGroupOptions; } export interface AlertDefinition {