AlertingNG: Enable UI to Save Alert Definitions (#30394)

* transform state to what the api expects

* add expr prop to dataquery

* Add evalutate field

* add refid picker to options

* minor fix to enable save

* fix  import

* more fixes after merge

* use default datasource if not changed

* replace name with title

* Change name in ui as well

* remove not used loadDataSources function

* prettier fixes

* look up datasource

* correct datasource per query model

* revert dataquery change, use expressionid const

* fix for type

* fix faulty const

* description readonly
This commit is contained in:
Peter Holmberg
2021-01-22 16:08:54 +01:00
committed by GitHub
parent 7126a91901
commit 529f564bd4
7 changed files with 159 additions and 79 deletions

View File

@@ -1,11 +1,12 @@
import React, { FormEvent, PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { Button, Icon, stylesFactory } from '@grafana/ui';
import { PageToolbar } from 'app/core/components/PageToolbar/PageToolbar';
import { SplitPaneWrapper } from 'app/core/components/SplitPaneWrapper/SplitPaneWrapper';
import { connectWithCleanUp } from 'app/core/components/connectWithCleanUp';
import AlertingQueryEditor from './components/AlertingQueryEditor';
import { AlertDefinitionOptions } from './components/AlertDefinitionOptions';
import { AlertingQueryPreview } from './components/AlertingQueryPreview';
@@ -15,7 +16,13 @@ import {
updateAlertDefinitionUiState,
loadNotificationTypes,
} from './state/actions';
import { AlertDefinition, AlertDefinitionUiState, NotificationChannelType, StoreState } from '../../types';
import {
AlertDefinition,
AlertDefinitionUiState,
NotificationChannelType,
QueryGroupOptions,
StoreState,
} from '../../types';
import { config } from 'app/core/config';
import { PanelQueryRunner } from '../query/state/PanelQueryRunner';
@@ -27,6 +34,7 @@ interface ConnectedProps {
uiState: AlertDefinitionUiState;
notificationChannelTypes: NotificationChannelType[];
queryRunner: PanelQueryRunner;
queryOptions: QueryGroupOptions;
}
interface DispatchProps {
@@ -51,6 +59,18 @@ class NextGenAlertingPage extends PureComponent<Props, State> {
this.props.updateAlertDefinitionOption({ [event.currentTarget.name]: event.currentTarget.value });
};
onChangeInterval = (interval: SelectableValue<number>) => {
this.props.updateAlertDefinitionOption({
interval: interval.value,
});
};
onConditionChange = (condition: SelectableValue<string>) => {
this.props.updateAlertDefinitionOption({
condition: { ...this.props.alertDefinition.condition, refId: condition.value! },
});
};
onSaveAlert = () => {
const { createAlertDefinition } = this.props;
@@ -82,6 +102,7 @@ class NextGenAlertingPage extends PureComponent<Props, State> {
uiState,
updateAlertDefinitionUiState,
queryRunner,
queryOptions,
} = this.props;
const styles = getStyles(config.theme);
@@ -106,6 +127,9 @@ class NextGenAlertingPage extends PureComponent<Props, State> {
alertDefinition={alertDefinition}
onChange={this.onChangeAlertOption}
notificationChannelTypes={notificationChannelTypes}
onIntervalChange={this.onChangeInterval}
onConditionChange={this.onConditionChange}
queryOptions={queryOptions}
/>
}
/>
@@ -119,6 +143,7 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (
return {
uiState: state.alertDefinition.uiState,
alertDefinition: state.alertDefinition.alertDefinition,
queryOptions: state.alertDefinition.queryOptions,
notificationChannelTypes: state.notificationChannel.notificationChannelTypes,
queryRunner: state.alertDefinition.queryRunner,
};
@@ -131,7 +156,9 @@ const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
loadNotificationTypes,
};
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(NextGenAlertingPage));
export default hot(module)(
connectWithCleanUp(mapStateToProps, mapDispatchToProps, (state) => state.alertDefinition)(NextGenAlertingPage)
);
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
wrapper: css`

View File

@@ -1,79 +1,94 @@
import React, { FC, FormEvent, useState } from 'react';
import React, { FC, FormEvent, useMemo } from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { Field, Input, Tab, TabContent, TabsBar, TextArea, useStyles } from '@grafana/ui';
import { AlertDefinition, NotificationChannelType } from 'app/types';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { Field, Input, Select, TextArea, useStyles } from '@grafana/ui';
import { AlertDefinition, NotificationChannelType, QueryGroupOptions } from 'app/types';
interface Props {
alertDefinition: AlertDefinition;
notificationChannelTypes: NotificationChannelType[];
onChange: (event: FormEvent) => void;
onIntervalChange: (interval: SelectableValue<number>) => void;
onConditionChange: (refId: SelectableValue<string>) => void;
queryOptions: QueryGroupOptions;
}
enum Tabs {
Alert = 'alert',
Panel = 'panel',
}
const tabs = [
{ id: Tabs.Alert, text: 'Alert definition' },
{ id: Tabs.Panel, text: 'Panel' },
];
export const AlertDefinitionOptions: FC<Props> = ({ alertDefinition, onChange }) => {
export const AlertDefinitionOptions: FC<Props> = ({
alertDefinition,
onChange,
onIntervalChange,
onConditionChange,
queryOptions,
}) => {
const styles = useStyles(getStyles);
const [activeTab, setActiveTab] = useState<string>(Tabs.Alert);
const refIds = useMemo(() => queryOptions.queries.map((q) => ({ value: q.refId, label: q.refId })), [
queryOptions.queries,
]);
return (
<div className={styles.container}>
<TabsBar>
{tabs.map((tab, index) => (
<Tab
key={`${tab.id}-${index}`}
label={tab.text}
active={tab.id === activeTab}
onChangeTab={() => setActiveTab(tab.id)}
<div style={{ paddingTop: '16px' }}>
<div className={styles.container}>
<h4>Alert definition</h4>
<Field label="Title">
<Input width={25} name="title" value={alertDefinition.title} onChange={onChange} />
</Field>
<Field label="Description" description="What does the alert do and why was it created">
<TextArea
rows={5}
width={25}
name="description"
value={alertDefinition.description}
onChange={onChange}
readOnly={true}
/>
))}
</TabsBar>
<TabContent className={styles.tabContent}>
{activeTab === Tabs.Alert && (
<div>
<Field label="Name">
<Input width={25} name="name" value={alertDefinition.name} onChange={onChange} />
</Field>
<Field label="Description" description="What does the alert do and why was it created">
<TextArea
rows={5}
width={25}
name="description"
value={alertDefinition.description}
onChange={onChange}
/>
</Field>
<Field label="Evaluate">
<span>Every For</span>
</Field>
<Field label="Conditions">
<div></div>
</Field>
</Field>
<Field label="Evaluate">
<div className={styles.optionRow}>
<span className={styles.optionName}>Every</span>
<Select
onChange={onIntervalChange}
value={alertDefinition.interval}
options={[
{ value: 60, label: '1m' },
{ value: 300, label: '5m' },
{ value: 600, label: '10m' },
]}
width={10}
/>
</div>
)}
{activeTab === Tabs.Panel && <div>VizPicker</div>}
</TabContent>
</Field>
<Field label="Conditions">
<div className={styles.optionRow}>
<Select
onChange={onConditionChange}
value={alertDefinition.condition.refId}
options={refIds}
noOptionsMessage="No queries added"
/>
</div>
</Field>
</div>
</div>
);
};
const getStyles = (theme: GrafanaTheme) => {
return {
container: css`
margin-top: ${theme.spacing.md};
height: 100%;
wrapper: css`
padding-top: ${theme.spacing.md};
`,
tabContent: css`
background: ${theme.colors.panelBg};
height: 100%;
container: css`
padding: ${theme.spacing.md};
background-color: ${theme.colors.panelBg};
`,
optionRow: css`
display: flex;
align-items: baseline;
`,
optionName: css`
font-size: ${theme.typography.size.md};
color: ${theme.colors.formInputText};
margin-right: ${theme.spacing.sm};
`,
};
};

View File

@@ -1,5 +1,5 @@
import { AppEvents, dateMath } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime';
import { getBackendSrv, getDataSourceSrv } from '@grafana/runtime';
import { appEvents } from 'app/core/core';
import { updateLocation } from 'app/core/actions';
import store from 'app/core/store';
@@ -13,8 +13,17 @@ import {
updateAlertDefinition,
setQueryOptions,
} from './reducers';
import { AlertDefinition, AlertDefinitionUiState, AlertRuleDTO, NotifierDTO, ThunkResult } from 'app/types';
import { QueryGroupOptions } from 'app/types';
import {
AlertDefinition,
AlertDefinitionUiState,
AlertRuleDTO,
NotifierDTO,
ThunkResult,
QueryGroupOptions,
QueryGroupDataSource,
} from 'app/types';
import { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource';
import { ExpressionQuery } from '../../expressions/types';
export function getAlertRulesAsync(options: { state: string }): ThunkResult<void> {
return async (dispatch) => {
@@ -88,26 +97,46 @@ export function loadNotificationChannel(id: number): ThunkResult<void> {
export function createAlertDefinition(): ThunkResult<void> {
return async (dispatch, getStore) => {
const queryOptions = getStore().alertDefinition.queryOptions;
const currentAlertDefinition = getStore().alertDefinition.alertDefinition;
const defaultDataSource = await getDataSourceSrv().get(null);
const alertDefinition: AlertDefinition = {
...getStore().alertDefinition.alertDefinition,
...currentAlertDefinition,
condition: {
ref: 'A',
queriesAndExpressions: [
{
refId: currentAlertDefinition.condition.refId,
queriesAndExpressions: 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: {
expression: '2 + 2 > 1',
type: 'math',
datasource: '__expr__',
...query,
type: isExpression ? (query as ExpressionQuery).type : query.queryType,
datasource: dataSource.name,
datasourceUid: dataSource.uid,
},
refId: query.refId,
relativeTimeRange: {
From: 500,
To: 0,
},
refId: 'A',
},
],
};
}),
},
};
await getBackendSrv().post(`/api/alert-definitions`, alertDefinition);
appEvents.emit(AppEvents.alertSuccess, ['Alert definition created']);
dispatch(updateLocation({ path: 'alerting/list' }));

View File

@@ -54,11 +54,12 @@ const dataConfig = {
export const initialAlertDefinitionState: AlertDefinitionState = {
alertDefinition: {
id: 0,
name: '',
title: '',
description: '',
condition: {} as AlertCondition,
interval: 60,
},
queryOptions: { maxDataPoints: 100, dataSource: { name: 'gdev-testdata' }, queries: [] },
queryOptions: { maxDataPoints: 100, dataSource: {}, queries: [] },
queryRunner: new PanelQueryRunner(dataConfig),
uiState: { ...store.getObject(ALERT_DEFINITION_UI_STATE_STORAGE_KEY, DEFAULT_ALERT_DEFINITION_UI_STATE) },
data: [],

View File

@@ -146,13 +146,14 @@ export interface AlertDefinitionState {
export interface AlertDefinition {
id: number;
name: string;
title: string;
description: string;
condition: AlertCondition;
interval: number;
}
export interface AlertCondition {
ref: string;
refId: string;
queriesAndExpressions: any[];
}

View File

@@ -1,7 +1,8 @@
import { DataQuery } from '@grafana/data';
import { ExpressionQuery } from '../features/expressions/types';
export interface QueryGroupOptions {
queries: DataQuery[];
queries: Array<DataQuery | ExpressionQuery>;
dataSource: QueryGroupDataSource;
maxDataPoints?: number | null;
minInterval?: string | null;
@@ -13,7 +14,7 @@ export interface QueryGroupOptions {
};
}
interface QueryGroupDataSource {
export interface QueryGroupDataSource {
name?: string | null;
uid?: string;
default?: boolean;