mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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`
|
||||
|
||||
@@ -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};
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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' }));
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user