Commit 12dcba5d by Peter Holmberg Committed by GitHub

AlertingNG: Test definition (#30886)

* 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 <papagian@users.noreply.github.com>
Co-authored-by: Sofia Papagiannaki <sofia@grafana.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
parent b9f6bd78
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(<AlertRuleList {...props} />);
const wrapper = shallow(<AlertRuleListUnconnected {...props} />);
return {
wrapper,
instance: wrapper.instance() as AlertRuleList,
instance: wrapper.instance() as AlertRuleListUnconnected,
};
};
......
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<AlertRule | AlertDefinition>;
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<Props, any> {
const mapDispatchToProps = {
updateLocation,
getAlertRulesAsync,
setSearchQuery,
togglePauseAlertRule,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
interface OwnProps {}
export type Props = OwnProps & ConnectedProps<typeof connector>;
export class AlertRuleListUnconnected extends PureComponent<Props, any> {
stateFilters = [
{ label: 'All', value: 'all' },
{ label: 'OK', value: 'ok' },
......@@ -118,9 +131,11 @@ export class AlertRuleList extends PureComponent<Props, any> {
</div>
</div>
<div className="page-action-bar__spacer" />
<LinkButton variant="primary" href="alerting/new">
Add NG Alert
</LinkButton>
{config.featureToggles.ngalert && (
<LinkButton variant="primary" href="alerting/new">
Add NG Alert
</LinkButton>
)}
<Button variant="secondary" onClick={this.onOpenHowTo}>
How to add an alert
</Button>
......@@ -153,20 +168,4 @@ export class AlertRuleList extends PureComponent<Props, any> {
}
}
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));
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';
interface OwnProps {
saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition;
}
function mapStateToProps(state: StoreState) {
const pageId = getRouteParamsId(state.location);
interface ConnectedProps {
uiState: AlertDefinitionUiState;
queryRunner: PanelQueryRunner;
queryOptions: QueryGroupOptions;
alertDefinition: AlertDefinition;
pageId: string;
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) ?? '',
};
}
interface DispatchProps {
updateAlertDefinitionUiState: typeof updateAlertDefinitionUiState;
updateAlertDefinitionOption: typeof updateAlertDefinitionOption;
getAlertDefinition: typeof getAlertDefinition;
updateAlertDefinition: typeof updateAlertDefinition;
createAlertDefinition: typeof createAlertDefinition;
const mapDispatchToProps = {
updateAlertDefinitionUiState,
updateAlertDefinitionOption,
evaluateAlertDefinition,
updateAlertDefinition,
createAlertDefinition,
getAlertDefinition,
evaluateNotSavedAlertDefinition,
onRunQueries,
cleanUpDefinitionState,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
interface OwnProps {
saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition;
}
type Props = OwnProps & ConnectedProps & DispatchProps;
type Props = OwnProps & ConnectedProps<typeof connector>;
class NextGenAlertingPage extends PureComponent<Props> {
class NextGenAlertingPageUnconnected extends PureComponent<Props> {
componentDidMount() {
const { getAlertDefinition, pageId } = this.props;
......@@ -52,8 +66,13 @@ class NextGenAlertingPage extends PureComponent<Props> {
}
}
onChangeAlertOption = (event: FormEvent<HTMLFormElement>) => {
this.props.updateAlertDefinitionOption({ [event.currentTarget.name]: event.currentTarget.value });
componentWillUnmount() {
this.props.cleanUpDefinitionState();
}
onChangeAlertOption = (event: FormEvent<HTMLElement>) => {
const formEvent = event as FormEvent<HTMLFormElement>;
this.props.updateAlertDefinitionOption({ [formEvent.currentTarget.name]: formEvent.currentTarget.value });
};
onChangeInterval = (interval: SelectableValue<number>) => {
......@@ -80,7 +99,14 @@ class NextGenAlertingPage extends PureComponent<Props> {
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<Props> {
}
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 (
<div className={styles.wrapper}>
......@@ -108,7 +144,14 @@ class NextGenAlertingPage extends PureComponent<Props> {
<div className={styles.splitPanesWrapper}>
<SplitPaneWrapper
leftPaneComponents={[
<AlertingQueryPreview key="queryPreview" queryRunner={queryRunner} />,
<AlertingQueryPreview
key="queryPreview"
queryRunner={queryRunner!} // if the queryRunner is undefined here somethings very wrong so it's ok to throw an unhandled error
getInstances={getInstances}
queries={queryOptions.queries}
onTest={this.onTest}
onRunQueries={onRunQueries}
/>,
<AlertingQueryEditor key="queryEditor" />,
]}
uiState={uiState}
......@@ -129,29 +172,7 @@ class NextGenAlertingPage extends PureComponent<Props> {
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (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<DispatchProps, OwnProps> = {
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`
......
......@@ -12,7 +12,7 @@ const intervalOptions: Array<SelectableValue<number>> = [
interface Props {
alertDefinition: AlertDefinition;
onChange: (event: FormEvent) => void;
onChange: (event: FormEvent<HTMLElement>) => void;
onIntervalChange: (interval: SelectableValue<number>) => void;
onConditionChange: (refId: SelectableValue<string>) => void;
queryOptions: QueryGroupOptions;
......
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';
interface OwnProps {}
interface ConnectedProps {
queryOptions: QueryGroupOptions;
queryRunner: PanelQueryRunner;
}
interface DispatchProps {
queryOptionsChange: typeof queryOptionsChange;
onRunQueries: typeof onRunQueries;
function mapStateToProps(state: StoreState) {
return {
queryOptions: state.alertDefinition.getQueryOptions(),
queryRunner: state.alertDefinition.queryRunner,
};
}
type Props = ConnectedProps & DispatchProps & OwnProps;
const mapDispatchToProps = {
queryOptionsChange,
onRunQueries,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export class AlertingQueryEditor extends PureComponent<Props> {
interface OwnProps {}
type Props = OwnProps & ConnectedProps<typeof connector>;
class AlertingQueryEditorUnconnected extends PureComponent<Props> {
onQueryOptionsChange = (queryOptions: QueryGroupOptions) => {
this.props.queryOptionsChange(queryOptions);
};
......@@ -51,7 +56,7 @@ export class AlertingQueryEditor extends PureComponent<Props> {
/>
</div>
<QueryGroup
queryRunner={queryRunner}
queryRunner={queryRunner!} // if the queryRunner is undefined here somethings very wrong so it's ok to throw an unhandled error
options={queryOptions}
onRunQueries={this.onRunQueries}
onOptionsChange={this.onQueryOptionsChange}
......@@ -62,19 +67,7 @@ export class AlertingQueryEditor extends PureComponent<Props> {
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => {
return {
queryOptions: state.alertDefinition.queryOptions,
queryRunner: state.alertDefinition.queryRunner,
};
};
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
queryOptionsChange,
onRunQueries,
};
export default connect(mapStateToProps, mapDispatchToProps)(AlertingQueryEditor);
export const AlertingQueryEditor = connector(AlertingQueryEditorUnconnected);
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
......
......@@ -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<Props> = ({ queryRunner }) => {
export const AlertingQueryPreview: FC<Props> = ({ getInstances, onRunQueries, onTest, queryRunner, queries }) => {
const [activeTab, setActiveTab] = useState<string>(Tabs.Query);
const styles = useStyles(getStyles);
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []);
const data = useObservable(observable);
const data = useObservable<PanelData>(observable);
const instances = getInstances();
return (
<div className={styles.wrapper}>
......@@ -49,16 +54,33 @@ export const AlertingQueryPreview: FC<Props> = ({ queryRunner }) => {
<h4 className={styles.noQueriesHeader}>There was an error :(</h4>
<div>{data.error?.data?.error}</div>
</div>
) : data && data.series.length > 0 ? (
) : queries && queries.length > 0 ? (
<AutoSizer style={{ width: '100%', height: '100%' }}>
{({ width, height }) => {
switch (activeTab) {
case Tabs.Instances:
return <PreviewInstancesTab isTested={false} data={data} styles={styles} />;
return (
<PreviewInstancesTab
isTested={instances.length > 0}
instances={instances}
styles={styles}
width={width}
height={height}
onTest={onTest}
/>
);
case Tabs.Query:
default:
return <PreviewQueryTab data={data} width={width} height={height} />;
if (data) {
return <PreviewQueryTab data={data} width={width} height={height} />;
}
return (
<div className={styles.noQueries}>
<h4 className={styles.noQueriesHeader}>Run queries to view data.</h4>
<Button onClick={onRunQueries}>Run queries</Button>
</div>
);
}
}}
</AutoSizer>
......
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<Props> = ({ data, isTested, styles }) => {
export const PreviewInstancesTab: FC<Props> = ({ instances, isTested, onTest, height, styles, width }) => {
if (!isTested) {
return (
<div className={styles.noQueries}>
<h4 className={styles.noQueriesHeader}>You haven’t tested your alert yet.</h4>
<div>In order to see your instances, you need to test your alert first.</div>
<Button>Test alert now</Button>
<Button onClick={onTest}>Test alert now</Button>
</div>
);
}
return <div>Instances</div>;
return <Table data={instances[0]} height={height} width={width} />;
};
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<Props> = ({ data, height, width }) => {
const [currentSeries, setSeries] = useState<number>(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<Array<SelectableValue<number>>>(() => {
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 <Table data={toDataFrame([])} height={height} width={width} />;
}
if (data.series.length > 1) {
return (
<div className={styles.wrapper}>
......
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,
notificationChannelLoaded,
setAlertDefinition,
setAlertDefinitions,
setInstanceData,
setNotificationChannels,
setQueryOptions,
setUiState,
ALERT_DEFINITION_UI_STATE_STORAGE_KEY,
updateAlertDefinitionOptions,
setQueryOptions,
setAlertDefinitions,
setAlertDefinition,
} 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<void> {
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<void> {
};
}
export function evaluateAlertDefinition(): ThunkResult<void> {
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<void> {
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<void> {
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,
};
}
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,
});
}
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,
},
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,
},
};
});
}
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<AlertDefinitionDTO>) => {
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<Partial<AlertDefinition>>) => {
return { ...state, alertDefinition: { ...state.alertDefinition, ...action.payload } };
state.alertDefinition = { ...state.alertDefinition, ...action.payload };
},
setUiState: (state: AlertDefinitionState, action: PayloadAction<AlertDefinitionUiState>) => {
return { ...state, uiState: { ...state.uiState, ...action.payload } };
state.uiState = { ...state.uiState, ...action.payload };
},
setQueryOptions: (state: AlertDefinitionState, action: PayloadAction<QueryGroupOptions>) => {
return {
...state,
queryOptions: action.payload,
};
state.getQueryOptions = () => action.payload;
},
setAlertDefinitions: (state: AlertDefinitionState, action: PayloadAction<AlertDefinition[]>) => {
return { ...state, alertDefinitions: action.payload };
state.alertDefinitions = action.payload;
},
setInstanceData: (state: AlertDefinitionState, action: PayloadAction<DataFrame[]>) => {
state.getInstances = () => action.payload;
},
cleanUpState: (state: AlertDefinitionState, action: PayloadAction<undefined>) => {
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;
......
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 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment