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 React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { AlertRuleList, Props } from './AlertRuleList'; import { AlertRuleListUnconnected, Props } from './AlertRuleList';
import { AlertRule } from '../../types'; import { AlertRule } from '../../types';
import appEvents from '../../core/app_events'; import appEvents from '../../core/app_events';
import { NavModel } from '@grafana/data'; import { NavModel } from '@grafana/data';
...@@ -24,15 +24,16 @@ const setup = (propOverrides?: object) => { ...@@ -24,15 +24,16 @@ const setup = (propOverrides?: object) => {
stateFilter: '', stateFilter: '',
search: '', search: '',
isLoading: false, isLoading: false,
ngAlertDefinitions: [],
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
const wrapper = shallow(<AlertRuleList {...props} />); const wrapper = shallow(<AlertRuleListUnconnected {...props} />);
return { return {
wrapper, wrapper,
instance: wrapper.instance() as AlertRuleList, instance: wrapper.instance() as AlertRuleListUnconnected,
}; };
}; };
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader'; 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 Page from 'app/core/components/Page/Page';
import AlertRuleItem from './AlertRuleItem'; import AlertRuleItem from './AlertRuleItem';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
...@@ -10,24 +10,37 @@ import { AlertDefinition, AlertRule, CoreEvents, StoreState } from 'app/types'; ...@@ -10,24 +10,37 @@ import { AlertDefinition, AlertRule, CoreEvents, StoreState } from 'app/types';
import { getAlertRulesAsync, togglePauseAlertRule } from './state/actions'; import { getAlertRulesAsync, togglePauseAlertRule } from './state/actions';
import { getAlertRuleItems, getSearchQuery } from './state/selectors'; import { getAlertRuleItems, getSearchQuery } from './state/selectors';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; 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 { setSearchQuery } from './state/reducers';
import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui'; import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui';
import { AlertDefinitionItem } from './components/AlertDefinitionItem'; import { AlertDefinitionItem } from './components/AlertDefinitionItem';
export interface Props { function mapStateToProps(state: StoreState) {
navModel: NavModel; return {
alertRules: Array<AlertRule | AlertDefinition>; navModel: getNavModel(state.navIndex, 'alert-list'),
updateLocation: typeof updateLocation; alertRules: getAlertRuleItems(state),
getAlertRulesAsync: typeof getAlertRulesAsync; stateFilter: state.location.query.state,
setSearchQuery: typeof setSearchQuery; search: getSearchQuery(state.alertRules),
togglePauseAlertRule: typeof togglePauseAlertRule; isLoading: state.alertRules.isLoading,
stateFilter: string; ngAlertDefinitions: state.alertDefinition.alertDefinitions,
search: string; };
isLoading: boolean;
} }
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 = [ stateFilters = [
{ label: 'All', value: 'all' }, { label: 'All', value: 'all' },
{ label: 'OK', value: 'ok' }, { label: 'OK', value: 'ok' },
...@@ -118,9 +131,11 @@ export class AlertRuleList extends PureComponent<Props, any> { ...@@ -118,9 +131,11 @@ export class AlertRuleList extends PureComponent<Props, any> {
</div> </div>
</div> </div>
<div className="page-action-bar__spacer" /> <div className="page-action-bar__spacer" />
{config.featureToggles.ngalert && (
<LinkButton variant="primary" href="alerting/new"> <LinkButton variant="primary" href="alerting/new">
Add NG Alert Add NG Alert
</LinkButton> </LinkButton>
)}
<Button variant="secondary" onClick={this.onOpenHowTo}> <Button variant="secondary" onClick={this.onOpenHowTo}>
How to add an alert How to add an alert
</Button> </Button>
...@@ -153,20 +168,4 @@ export class AlertRuleList extends PureComponent<Props, any> { ...@@ -153,20 +168,4 @@ export class AlertRuleList extends PureComponent<Props, any> {
} }
} }
const mapStateToProps = (state: StoreState) => ({ export default hot(module)(connector(AlertRuleListUnconnected));
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));
import React, { FormEvent, PureComponent } from 'react'; import React, { FormEvent, PureComponent } from 'react';
import { hot } from 'react-hot-loader'; import { hot } from 'react-hot-loader';
import { MapDispatchToProps, MapStateToProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { css } from 'emotion'; import { css } from 'emotion';
import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { PageToolbar, stylesFactory, ToolbarButton } from '@grafana/ui'; import { PageToolbar, stylesFactory, ToolbarButton } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper'; 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 { AlertDefinitionOptions } from './components/AlertDefinitionOptions';
import { AlertingQueryPreview } from './components/AlertingQueryPreview'; import { AlertingQueryPreview } from './components/AlertingQueryPreview';
import { import {
updateAlertDefinitionOption, cleanUpDefinitionState,
createAlertDefinition, createAlertDefinition,
updateAlertDefinitionUiState, evaluateAlertDefinition,
updateAlertDefinition, evaluateNotSavedAlertDefinition,
getAlertDefinition, getAlertDefinition,
onRunQueries,
updateAlertDefinition,
updateAlertDefinitionOption,
updateAlertDefinitionUiState,
} from './state/actions'; } from './state/actions';
import { getRouteParamsId } from 'app/core/selectors/location'; import { getRouteParamsId } from 'app/core/selectors/location';
import { AlertDefinition, AlertDefinitionUiState, QueryGroupOptions, StoreState } from '../../types'; import { StoreState } from 'app/types';
import { PanelQueryRunner } from '../query/state/PanelQueryRunner';
interface OwnProps { function mapStateToProps(state: StoreState) {
saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition; const pageId = getRouteParamsId(state.location);
}
interface ConnectedProps { return {
uiState: AlertDefinitionUiState; uiState: state.alertDefinition.uiState,
queryRunner: PanelQueryRunner; getQueryOptions: state.alertDefinition.getQueryOptions,
queryOptions: QueryGroupOptions; queryRunner: state.alertDefinition.queryRunner,
alertDefinition: AlertDefinition; getInstances: state.alertDefinition.getInstances,
pageId: string; alertDefinition: state.alertDefinition.alertDefinition,
pageId: (pageId as string) ?? '',
};
} }
interface DispatchProps { const mapDispatchToProps = {
updateAlertDefinitionUiState: typeof updateAlertDefinitionUiState; updateAlertDefinitionUiState,
updateAlertDefinitionOption: typeof updateAlertDefinitionOption; updateAlertDefinitionOption,
getAlertDefinition: typeof getAlertDefinition; evaluateAlertDefinition,
updateAlertDefinition: typeof updateAlertDefinition; updateAlertDefinition,
createAlertDefinition: typeof createAlertDefinition; 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() { componentDidMount() {
const { getAlertDefinition, pageId } = this.props; const { getAlertDefinition, pageId } = this.props;
...@@ -52,8 +66,13 @@ class NextGenAlertingPage extends PureComponent<Props> { ...@@ -52,8 +66,13 @@ class NextGenAlertingPage extends PureComponent<Props> {
} }
} }
onChangeAlertOption = (event: FormEvent<HTMLFormElement>) => { componentWillUnmount() {
this.props.updateAlertDefinitionOption({ [event.currentTarget.name]: event.currentTarget.value }); 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>) => { onChangeInterval = (interval: SelectableValue<number>) => {
...@@ -80,7 +99,14 @@ class NextGenAlertingPage extends PureComponent<Props> { ...@@ -80,7 +99,14 @@ class NextGenAlertingPage extends PureComponent<Props> {
onDiscard = () => {}; onDiscard = () => {};
onTest = () => {}; onTest = () => {
const { alertDefinition, evaluateAlertDefinition, evaluateNotSavedAlertDefinition } = this.props;
if (alertDefinition.uid) {
evaluateAlertDefinition();
} else {
evaluateNotSavedAlertDefinition();
}
};
renderToolbarActions() { renderToolbarActions() {
return [ return [
...@@ -97,8 +123,18 @@ class NextGenAlertingPage extends PureComponent<Props> { ...@@ -97,8 +123,18 @@ class NextGenAlertingPage extends PureComponent<Props> {
} }
render() { 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 styles = getStyles(config.theme);
const queryOptions = getQueryOptions();
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
...@@ -108,7 +144,14 @@ class NextGenAlertingPage extends PureComponent<Props> { ...@@ -108,7 +144,14 @@ class NextGenAlertingPage extends PureComponent<Props> {
<div className={styles.splitPanesWrapper}> <div className={styles.splitPanesWrapper}>
<SplitPaneWrapper <SplitPaneWrapper
leftPaneComponents={[ 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" />, <AlertingQueryEditor key="queryEditor" />,
]} ]}
uiState={uiState} uiState={uiState}
...@@ -129,29 +172,7 @@ class NextGenAlertingPage extends PureComponent<Props> { ...@@ -129,29 +172,7 @@ class NextGenAlertingPage extends PureComponent<Props> {
} }
} }
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => { export default hot(module)(connector(NextGenAlertingPageUnconnected));
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)
);
const getStyles = stylesFactory((theme: GrafanaTheme) => ({ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
wrapper: css` wrapper: css`
......
...@@ -12,7 +12,7 @@ const intervalOptions: Array<SelectableValue<number>> = [ ...@@ -12,7 +12,7 @@ const intervalOptions: Array<SelectableValue<number>> = [
interface Props { interface Props {
alertDefinition: AlertDefinition; alertDefinition: AlertDefinition;
onChange: (event: FormEvent) => void; onChange: (event: FormEvent<HTMLElement>) => void;
onIntervalChange: (interval: SelectableValue<number>) => void; onIntervalChange: (interval: SelectableValue<number>) => void;
onConditionChange: (refId: SelectableValue<string>) => void; onConditionChange: (refId: SelectableValue<string>) => void;
queryOptions: QueryGroupOptions; queryOptions: QueryGroupOptions;
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import { css } from 'emotion'; import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data'; import { GrafanaTheme } from '@grafana/data';
import { RefreshPicker, stylesFactory } from '@grafana/ui'; import { RefreshPicker, stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { QueryGroup } from '../../query/components/QueryGroup'; import { QueryGroup } from '../../query/components/QueryGroup';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
import { onRunQueries, queryOptionsChange } from '../state/actions'; import { onRunQueries, queryOptionsChange } from '../state/actions';
import { QueryGroupOptions, StoreState } from 'app/types'; import { QueryGroupOptions, StoreState } from 'app/types';
interface OwnProps {} function mapStateToProps(state: StoreState) {
return {
interface ConnectedProps { queryOptions: state.alertDefinition.getQueryOptions(),
queryOptions: QueryGroupOptions; queryRunner: state.alertDefinition.queryRunner,
queryRunner: PanelQueryRunner; };
}
interface DispatchProps {
queryOptionsChange: typeof queryOptionsChange;
onRunQueries: typeof onRunQueries;
} }
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) => { onQueryOptionsChange = (queryOptions: QueryGroupOptions) => {
this.props.queryOptionsChange(queryOptions); this.props.queryOptionsChange(queryOptions);
}; };
...@@ -51,7 +56,7 @@ export class AlertingQueryEditor extends PureComponent<Props> { ...@@ -51,7 +56,7 @@ export class AlertingQueryEditor extends PureComponent<Props> {
/> />
</div> </div>
<QueryGroup <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} options={queryOptions}
onRunQueries={this.onRunQueries} onRunQueries={this.onRunQueries}
onOptionsChange={this.onQueryOptionsChange} onOptionsChange={this.onQueryOptionsChange}
...@@ -62,19 +67,7 @@ export class AlertingQueryEditor extends PureComponent<Props> { ...@@ -62,19 +67,7 @@ export class AlertingQueryEditor extends PureComponent<Props> {
} }
} }
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state) => { export const AlertingQueryEditor = connector(AlertingQueryEditorUnconnected);
return {
queryOptions: state.alertDefinition.queryOptions,
queryRunner: state.alertDefinition.queryRunner,
};
};
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
queryOptionsChange,
onRunQueries,
};
export default connect(mapStateToProps, mapDispatchToProps)(AlertingQueryEditor);
const getStyles = stylesFactory((theme: GrafanaTheme) => { const getStyles = stylesFactory((theme: GrafanaTheme) => {
return { return {
......
...@@ -2,8 +2,8 @@ import React, { FC, useMemo, useState } from 'react'; ...@@ -2,8 +2,8 @@ import React, { FC, useMemo, useState } from 'react';
import { useObservable } from 'react-use'; import { useObservable } from 'react-use';
import { css } from 'emotion'; import { css } from 'emotion';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme } from '@grafana/data'; import { DataFrame, DataQuery, GrafanaTheme, PanelData } from '@grafana/data';
import { TabsBar, TabContent, Tab, useStyles, Icon } from '@grafana/ui'; import { Button, Icon, Tab, TabContent, TabsBar, useStyles } from '@grafana/ui';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
import { PreviewQueryTab } from './PreviewQueryTab'; import { PreviewQueryTab } from './PreviewQueryTab';
import { PreviewInstancesTab } from './PreviewInstancesTab'; import { PreviewInstancesTab } from './PreviewInstancesTab';
...@@ -20,14 +20,19 @@ const tabs = [ ...@@ -20,14 +20,19 @@ const tabs = [
interface Props { interface Props {
queryRunner: PanelQueryRunner; 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 [activeTab, setActiveTab] = useState<string>(Tabs.Query);
const styles = useStyles(getStyles); const styles = useStyles(getStyles);
const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []); const observable = useMemo(() => queryRunner.getData({ withFieldConfig: true, withTransforms: true }), []);
const data = useObservable(observable); const data = useObservable<PanelData>(observable);
const instances = getInstances();
return ( return (
<div className={styles.wrapper}> <div className={styles.wrapper}>
...@@ -49,17 +54,34 @@ export const AlertingQueryPreview: FC<Props> = ({ queryRunner }) => { ...@@ -49,17 +54,34 @@ export const AlertingQueryPreview: FC<Props> = ({ queryRunner }) => {
<h4 className={styles.noQueriesHeader}>There was an error :(</h4> <h4 className={styles.noQueriesHeader}>There was an error :(</h4>
<div>{data.error?.data?.error}</div> <div>{data.error?.data?.error}</div>
</div> </div>
) : data && data.series.length > 0 ? ( ) : queries && queries.length > 0 ? (
<AutoSizer style={{ width: '100%', height: '100%' }}> <AutoSizer style={{ width: '100%', height: '100%' }}>
{({ width, height }) => { {({ width, height }) => {
switch (activeTab) { switch (activeTab) {
case Tabs.Instances: 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: case Tabs.Query:
default: default:
if (data) {
return <PreviewQueryTab data={data} width={width} height={height} />; 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> </AutoSizer>
) : ( ) : (
......
import React, { FC } from 'react'; import React, { FC } from 'react';
import { PanelData } from '@grafana/data'; import { DataFrame } from '@grafana/data';
import { Button } from '@grafana/ui'; import { Button, Table } from '@grafana/ui';
import { PreviewStyles } from './AlertingQueryPreview'; import { PreviewStyles } from './AlertingQueryPreview';
interface Props { interface Props {
data: PanelData; instances: DataFrame[];
isTested: boolean; isTested: boolean;
styles: PreviewStyles; 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) { if (!isTested) {
return ( return (
<div className={styles.noQueries}> <div className={styles.noQueries}>
<h4 className={styles.noQueriesHeader}>You haven’t tested your alert yet.</h4> <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> <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> </div>
); );
} }
return <div>Instances</div>; return <Table data={instances[0]} height={height} width={width} />;
}; };
import React, { FC, useMemo, useState } from 'react'; 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 { Select, stylesFactory, Table, useTheme } from '@grafana/ui';
import { css } from 'emotion'; import { css } from 'emotion';
interface Props { interface Props {
data: PanelData; data?: PanelData;
width: number; width: number;
height: number; height: number;
} }
...@@ -13,14 +13,21 @@ export const PreviewQueryTab: FC<Props> = ({ data, height, width }) => { ...@@ -13,14 +13,21 @@ export const PreviewQueryTab: FC<Props> = ({ data, height, width }) => {
const [currentSeries, setSeries] = useState<number>(0); const [currentSeries, setSeries] = useState<number>(0);
const theme = useTheme(); const theme = useTheme();
const styles = getStyles(theme, height); const styles = getStyles(theme, height);
const series = useMemo( const series = useMemo<Array<SelectableValue<number>>>(() => {
() => data.series.map((frame, index) => ({ value: index, label: getFrameDisplayName(frame) })), if (data?.series) {
[data.series] return data.series.map((frame, index) => ({ value: index, label: getFrameDisplayName(frame) }));
); }
return [];
}, [data]);
// Select padding // Select padding
const padding = 16; const padding = 16;
if (!data?.series?.length) {
return <Table data={toDataFrame([])} height={height} width={width} />;
}
if (data.series.length > 1) { if (data.series.length > 1) {
return ( return (
<div className={styles.wrapper}> <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 { config, getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
import { appEvents } from 'app/core/core'; import { appEvents } from 'app/core/core';
import { updateLocation } from 'app/core/actions'; import { updateLocation } from 'app/core/actions';
import store from 'app/core/store'; import store from 'app/core/store';
import { import {
notificationChannelLoaded, ALERT_DEFINITION_UI_STATE_STORAGE_KEY,
cleanUpState,
loadAlertRules, loadAlertRules,
loadedAlertRules, loadedAlertRules,
notificationChannelLoaded,
setAlertDefinition,
setAlertDefinitions,
setInstanceData,
setNotificationChannels, setNotificationChannels,
setQueryOptions,
setUiState, setUiState,
ALERT_DEFINITION_UI_STATE_STORAGE_KEY,
updateAlertDefinitionOptions, updateAlertDefinitionOptions,
setQueryOptions,
setAlertDefinitions,
setAlertDefinition,
} from './reducers'; } from './reducers';
import { import {
AlertDefinition, AlertDefinition,
AlertDefinitionState,
AlertDefinitionUiState, AlertDefinitionUiState,
AlertRuleDTO, AlertRuleDTO,
NotifierDTO, NotifierDTO,
ThunkResult,
QueryGroupOptions,
QueryGroupDataSource, QueryGroupDataSource,
AlertDefinitionState, QueryGroupOptions,
ThunkResult,
} from 'app/types'; } from 'app/types';
import { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource'; import { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource';
import { ExpressionQuery } from '../../expressions/types'; import { ExpressionQuery } from '../../expressions/types';
...@@ -161,10 +170,12 @@ export function queryOptionsChange(queryOptions: QueryGroupOptions): ThunkResult ...@@ -161,10 +170,12 @@ export function queryOptionsChange(queryOptions: QueryGroupOptions): ThunkResult
export function onRunQueries(): ThunkResult<void> { export function onRunQueries(): ThunkResult<void> {
return (dispatch, getStore) => { return (dispatch, getStore) => {
const { queryRunner, queryOptions } = getStore().alertDefinition; const { queryRunner, getQueryOptions } = getStore().alertDefinition;
const timeRange = { from: 'now-1h', to: 'now' }; 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', timezone: 'browser',
timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange }, timeRange: { from: dateMath.parse(timeRange.from)!, to: dateMath.parse(timeRange.to)!, raw: timeRange },
maxDataPoints: queryOptions.maxDataPoints ?? 100, maxDataPoints: queryOptions.maxDataPoints ?? 100,
...@@ -175,14 +186,73 @@ export function onRunQueries(): ThunkResult<void> { ...@@ -175,14 +186,73 @@ 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) { async function buildAlertDefinition(state: AlertDefinitionState) {
const queryOptions = state.queryOptions; const queryOptions = state.getQueryOptions();
const currentAlertDefinition = state.alertDefinition; const currentAlertDefinition = state.alertDefinition;
const defaultDataSource = await getDataSourceSrv().get(null); const defaultDataSource = await getDataSourceSrv().get(null);
return { return {
...currentAlertDefinition, ...currentAlertDefinition,
data: queryOptions.queries.map((query) => { 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; let dataSource: QueryGroupDataSource;
const isExpression = query.datasource === ExpressionDatasourceID; const isExpression = query.datasource === ExpressionDatasourceID;
...@@ -210,6 +280,5 @@ async function buildAlertDefinition(state: AlertDefinitionState) { ...@@ -210,6 +280,5 @@ async function buildAlertDefinition(state: AlertDefinitionState) {
To: 0, To: 0,
}, },
}; };
}), });
};
} }
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; 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 alertDef from './alertDef';
import { import {
AlertDefinition, AlertDefinition,
...@@ -62,11 +62,14 @@ export const initialAlertDefinitionState: AlertDefinitionState = { ...@@ -62,11 +62,14 @@ export const initialAlertDefinitionState: AlertDefinitionState = {
data: [], data: [],
intervalSeconds: 60, intervalSeconds: 60,
}, },
queryOptions: { maxDataPoints: 100, dataSource: { name: '-- Mixed --' }, queries: [] },
queryRunner: new PanelQueryRunner(dataConfig), queryRunner: new PanelQueryRunner(dataConfig),
uiState: { ...store.getObject(ALERT_DEFINITION_UI_STATE_STORAGE_KEY, DEFAULT_ALERT_DEFINITION_UI_STATE) }, uiState: { ...store.getObject(ALERT_DEFINITION_UI_STATE_STORAGE_KEY, DEFAULT_ALERT_DEFINITION_UI_STATE) },
data: [], data: [],
alertDefinitions: [] as AlertDefinition[], 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 { function convertToAlertRule(dto: AlertRuleDTO, state: string): AlertRule {
...@@ -160,37 +163,49 @@ const alertDefinitionSlice = createSlice({ ...@@ -160,37 +163,49 @@ const alertDefinitionSlice = createSlice({
initialState: initialAlertDefinitionState, initialState: initialAlertDefinitionState,
reducers: { reducers: {
setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction<AlertDefinitionDTO>) => { setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction<AlertDefinitionDTO>) => {
return { const currentOptions = state.getQueryOptions();
...state,
alertDefinition: { state.alertDefinition.title = action.payload.title;
title: action.payload.title, state.alertDefinition.id = action.payload.id;
id: action.payload.id, state.alertDefinition.uid = action.payload.uid;
uid: action.payload.uid, state.alertDefinition.condition = action.payload.condition;
condition: action.payload.condition, state.alertDefinition.intervalSeconds = action.payload.intervalSeconds;
intervalSeconds: action.payload.intervalSeconds, state.alertDefinition.data = action.payload.data;
data: action.payload.data, state.alertDefinition.description = action.payload.description;
description: '', state.getQueryOptions = () => ({
}, ...currentOptions,
queryOptions: {
...state.queryOptions,
queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })), queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })),
}, });
};
}, },
updateAlertDefinitionOptions: (state: AlertDefinitionState, action: PayloadAction<Partial<AlertDefinition>>) => { 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>) => { 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>) => { setQueryOptions: (state: AlertDefinitionState, action: PayloadAction<QueryGroupOptions>) => {
return { state.getQueryOptions = () => action.payload;
...state,
queryOptions: action.payload,
};
}, },
setAlertDefinitions: (state: AlertDefinitionState, action: PayloadAction<AlertDefinition[]>) => { 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 { ...@@ -209,6 +224,8 @@ export const {
setQueryOptions, setQueryOptions,
setAlertDefinitions, setAlertDefinitions,
setAlertDefinition, setAlertDefinition,
setInstanceData,
cleanUpState,
} = alertDefinitionSlice.actions; } = alertDefinitionSlice.actions;
export const alertRulesReducer = alertRulesSlice.reducer; 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 { PanelQueryRunner } from '../features/query/state/PanelQueryRunner';
import { QueryGroupOptions } from './query'; import { QueryGroupOptions } from './query';
import { ExpressionQuery } from '../features/expressions/types'; import { ExpressionQuery } from '../features/expressions/types';
...@@ -140,10 +140,11 @@ export interface AlertNotification { ...@@ -140,10 +140,11 @@ export interface AlertNotification {
export interface AlertDefinitionState { export interface AlertDefinitionState {
uiState: AlertDefinitionUiState; uiState: AlertDefinitionUiState;
alertDefinition: AlertDefinition; alertDefinition: AlertDefinition;
queryOptions: QueryGroupOptions; queryRunner?: PanelQueryRunner;
queryRunner: PanelQueryRunner;
data: PanelData[]; data: PanelData[];
alertDefinitions: AlertDefinition[]; alertDefinitions: AlertDefinition[];
getInstances: () => DataFrame[];
getQueryOptions: () => QueryGroupOptions;
} }
export interface AlertDefinition { 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