From aaf571074822b1a14fe97d2299d8a119664ed455 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Thu, 4 Feb 2021 09:13:02 +0100 Subject: [PATCH] AlertingNG: Edit Alert Definition (#30676) * 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 * remove edit and add landing page * remove snapshot tests ans snapshots * wrap linkbutton in array Co-authored-by: Sofia Papagiannaki Co-authored-by: Sofia Papagiannaki --- pkg/api/index.go | 10 - pkg/services/ngalert/api.go | 33 ++- pkg/services/ngalert/common_test.go | 22 +- pkg/services/ngalert/database.go | 8 +- pkg/services/ngalert/database_test.go | 110 ++++----- pkg/services/ngalert/models.go | 25 +- .../features/alerting/AlertRuleList.test.tsx | 45 ---- .../app/features/alerting/AlertRuleList.tsx | 5 +- .../features/alerting/NextGenAlertingPage.tsx | 73 +++--- .../__snapshots__/AlertRuleList.test.tsx.snap | 218 ------------------ .../components/AlertDefinitionItem.tsx | 11 +- .../components/AlertDefinitionOptions.tsx | 19 +- public/app/features/alerting/state/actions.ts | 105 +++++---- .../app/features/alerting/state/reducers.ts | 40 +++- public/app/routes/routes.ts | 13 +- public/app/types/alerting.ts | 21 +- 16 files changed, 287 insertions(+), 471 deletions(-) delete mode 100644 public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap diff --git a/pkg/api/index.go b/pkg/api/index.go index 1e38afab6a37a..57b782017cbd4 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -196,16 +196,6 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto }) } - if hs.Cfg.IsNgAlertEnabled() { - navTree = append(navTree, &dtos.NavLink{ - Text: "NgAlerting", - Id: "ngalerting", - SubTitle: "Next generation alerting", - Icon: "bell", - Url: setting.AppSubUrl + "/ngalerting", - }) - } - if c.IsSignedIn { navTree = append(navTree, getProfileNode(c)) } diff --git a/pkg/services/ngalert/api.go b/pkg/services/ngalert/api.go index a4305328af637..615ae09f57463 100644 --- a/pkg/services/ngalert/api.go +++ b/pkg/services/ngalert/api.go @@ -38,13 +38,24 @@ func (ng *AlertNG) registerAPIEndpoints() { } // conditionEvalEndpoint handles POST /api/alert-definitions/eval. -func (ng *AlertNG) conditionEvalEndpoint(c *models.ReqContext, dto evalAlertConditionCommand) response.Response { - if err := ng.validateCondition(dto.Condition, c.SignedInUser, c.SkipCache); err != nil { +func (ng *AlertNG) conditionEvalEndpoint(c *models.ReqContext, cmd evalAlertConditionCommand) response.Response { + evalCond := eval.Condition{ + RefID: cmd.Condition, + OrgID: c.SignedInUser.OrgId, + QueriesAndExpressions: cmd.Data, + } + if err := ng.validateCondition(evalCond, c.SignedInUser, c.SkipCache); err != nil { return response.Error(400, "invalid condition", err) } + now := cmd.Now + if now.IsZero() { + now = timeNow() + } + evaluator := eval.Evaluator{Cfg: ng.Cfg} - evalResults, err := evaluator.ConditionEval(&dto.Condition, timeNow()) + + evalResults, err := evaluator.ConditionEval(&evalCond, now) if err != nil { return response.Error(400, "Failed to evaluate conditions", err) } @@ -61,7 +72,7 @@ func (ng *AlertNG) conditionEvalEndpoint(c *models.ReqContext, dto evalAlertCond }) } -// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID. +// alertDefinitionEvalEndpoint handles POST /api/alert-definitions/eval/:alertDefinitionUID. func (ng *AlertNG) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Response { alertDefinitionUID := c.Params(":alertDefinitionUID") @@ -132,7 +143,12 @@ func (ng *AlertNG) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd updat cmd.UID = c.Params(":alertDefinitionUID") cmd.OrgID = c.SignedInUser.OrgId - if err := ng.validateCondition(cmd.Condition, c.SignedInUser, c.SkipCache); err != nil { + evalCond := eval.Condition{ + RefID: cmd.Condition, + OrgID: c.SignedInUser.OrgId, + QueriesAndExpressions: cmd.Data, + } + if err := ng.validateCondition(evalCond, c.SignedInUser, c.SkipCache); err != nil { return response.Error(400, "invalid condition", err) } @@ -147,7 +163,12 @@ func (ng *AlertNG) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd updat func (ng *AlertNG) createAlertDefinitionEndpoint(c *models.ReqContext, cmd saveAlertDefinitionCommand) response.Response { cmd.OrgID = c.SignedInUser.OrgId - if err := ng.validateCondition(cmd.Condition, c.SignedInUser, c.SkipCache); err != nil { + evalCond := eval.Condition{ + RefID: cmd.Condition, + OrgID: c.SignedInUser.OrgId, + QueriesAndExpressions: cmd.Data, + } + if err := ng.validateCondition(evalCond, c.SignedInUser, c.SkipCache); err != nil { return response.Error(400, "invalid condition", err) } diff --git a/pkg/services/ngalert/common_test.go b/pkg/services/ngalert/common_test.go index aae7b41bd8a57..5d437f64ad544 100644 --- a/pkg/services/ngalert/common_test.go +++ b/pkg/services/ngalert/common_test.go @@ -57,23 +57,21 @@ func overrideAlertNGInRegistry(cfg *setting.Cfg) AlertNG { func createTestAlertDefinition(t *testing.T, ng *AlertNG, intervalSeconds int64) *AlertDefinition { cmd := saveAlertDefinitionCommand{ - OrgID: 1, - Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)), - Condition: eval.Condition{ - RefID: "A", - QueriesAndExpressions: []eval.AlertQuery{ - { - Model: json.RawMessage(`{ + OrgID: 1, + Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)), + Condition: "A", + Data: []eval.AlertQuery{ + { + Model: json.RawMessage(`{ "datasource": "__expr__", "type":"math", "expression":"2 + 2 > 1" }`), - RelativeTimeRange: eval.RelativeTimeRange{ - From: eval.Duration(5 * time.Hour), - To: eval.Duration(3 * time.Hour), - }, - RefID: "A", + RelativeTimeRange: eval.RelativeTimeRange{ + From: eval.Duration(5 * time.Hour), + To: eval.Duration(3 * time.Hour), }, + RefID: "A", }, }, IntervalSeconds: &intervalSeconds, diff --git a/pkg/services/ngalert/database.go b/pkg/services/ngalert/database.go index 3edf08a58fbac..e02a2fd7754b3 100644 --- a/pkg/services/ngalert/database.go +++ b/pkg/services/ngalert/database.go @@ -76,8 +76,8 @@ func (ng *AlertNG) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error { alertDefinition := &AlertDefinition{ OrgID: cmd.OrgID, Title: cmd.Title, - Condition: cmd.Condition.RefID, - Data: cmd.Condition.QueriesAndExpressions, + Condition: cmd.Condition, + Data: cmd.Data, IntervalSeconds: intervalSeconds, Version: initialVersion, UID: uid, @@ -133,11 +133,11 @@ func (ng *AlertNG) updateAlertDefinition(cmd *updateAlertDefinitionCommand) erro if title == "" { title = existingAlertDefinition.Title } - condition := cmd.Condition.RefID + condition := cmd.Condition if condition == "" { condition = existingAlertDefinition.Condition } - data := cmd.Condition.QueriesAndExpressions + data := cmd.Data if data == nil { data = existingAlertDefinition.Data } diff --git a/pkg/services/ngalert/database_test.go b/pkg/services/ngalert/database_test.go index 6016d1e800f31..e8a814c23fb94 100644 --- a/pkg/services/ngalert/database_test.go +++ b/pkg/services/ngalert/database_test.go @@ -74,22 +74,20 @@ func TestCreatingAlertDefinition(t *testing.T) { t.Cleanup(registry.ClearOverrides) q := saveAlertDefinitionCommand{ - OrgID: 1, - Title: tc.inputTitle, - Condition: eval.Condition{ - RefID: "B", - QueriesAndExpressions: []eval.AlertQuery{ - { - Model: json.RawMessage(`{ + OrgID: 1, + Title: tc.inputTitle, + Condition: "B", + Data: []eval.AlertQuery{ + { + Model: json.RawMessage(`{ "datasource": "__expr__", "type":"math", "expression":"2 + 3 > 1" }`), - RefID: "B", - RelativeTimeRange: eval.RelativeTimeRange{ - From: eval.Duration(time.Duration(5) * time.Hour), - To: eval.Duration(time.Duration(3) * time.Hour), - }, + RefID: "B", + RelativeTimeRange: eval.RelativeTimeRange{ + From: eval.Duration(time.Duration(5) * time.Hour), + To: eval.Duration(time.Duration(3) * time.Hour), }, }, }, @@ -117,22 +115,20 @@ func TestCreatingConflictionAlertDefinition(t *testing.T) { t.Cleanup(registry.ClearOverrides) q := saveAlertDefinitionCommand{ - OrgID: 1, - Title: "title", - Condition: eval.Condition{ - RefID: "B", - QueriesAndExpressions: []eval.AlertQuery{ - { - Model: json.RawMessage(`{ + OrgID: 1, + Title: "title", + Condition: "B", + Data: []eval.AlertQuery{ + { + Model: json.RawMessage(`{ "datasource": "__expr__", "type":"math", "expression":"2 + 3 > 1" }`), - RefID: "B", - RelativeTimeRange: eval.RelativeTimeRange{ - From: eval.Duration(time.Duration(5) * time.Hour), - To: eval.Duration(time.Duration(3) * time.Hour), - }, + RefID: "B", + RelativeTimeRange: eval.RelativeTimeRange{ + From: eval.Duration(time.Duration(5) * time.Hour), + To: eval.Duration(time.Duration(3) * time.Hour), }, }, }, @@ -156,23 +152,21 @@ func TestUpdatingAlertDefinition(t *testing.T) { t.Cleanup(registry.ClearOverrides) q := updateAlertDefinitionCommand{ - UID: "unknown", - OrgID: 1, - Title: "something completely different", - Condition: eval.Condition{ - RefID: "A", - QueriesAndExpressions: []eval.AlertQuery{ - { - Model: json.RawMessage(`{ + UID: "unknown", + OrgID: 1, + Title: "something completely different", + Condition: "A", + Data: []eval.AlertQuery{ + { + Model: json.RawMessage(`{ "datasource": "__expr__", "type":"math", "expression":"2 + 2 > 1" }`), - RefID: "A", - RelativeTimeRange: eval.RelativeTimeRange{ - From: eval.Duration(time.Duration(5) * time.Hour), - To: eval.Duration(time.Duration(3) * time.Hour), - }, + RefID: "A", + RelativeTimeRange: eval.RelativeTimeRange{ + From: eval.Duration(time.Duration(5) * time.Hour), + To: eval.Duration(time.Duration(3) * time.Hour), }, }, }, @@ -250,21 +244,19 @@ func TestUpdatingAlertDefinition(t *testing.T) { } q := updateAlertDefinitionCommand{ - UID: (*alertDefinition).UID, - Condition: eval.Condition{ - RefID: "B", - QueriesAndExpressions: []eval.AlertQuery{ - { - Model: json.RawMessage(`{ + UID: (*alertDefinition).UID, + Condition: "B", + Data: []eval.AlertQuery{ + { + Model: json.RawMessage(`{ "datasource": "__expr__", "type":"math", "expression":"2 + 3 > 1" }`), - RefID: "B", - RelativeTimeRange: eval.RelativeTimeRange{ - From: eval.Duration(5 * time.Hour), - To: eval.Duration(3 * time.Hour), - }, + RefID: "B", + RelativeTimeRange: eval.RelativeTimeRange{ + From: eval.Duration(5 * time.Hour), + To: eval.Duration(3 * time.Hour), }, }, }, @@ -335,22 +327,20 @@ func TestUpdatingConflictingAlertDefinition(t *testing.T) { alertDef2 := createTestAlertDefinition(t, ng, initialInterval) q := updateAlertDefinitionCommand{ - UID: (*alertDef2).UID, - Title: alertDef1.Title, - Condition: eval.Condition{ - RefID: "B", - QueriesAndExpressions: []eval.AlertQuery{ - { - Model: json.RawMessage(`{ + UID: (*alertDef2).UID, + Title: alertDef1.Title, + Condition: "B", + Data: []eval.AlertQuery{ + { + Model: json.RawMessage(`{ "datasource": "__expr__", "type":"math", "expression":"2 + 3 > 1" }`), - RefID: "B", - RelativeTimeRange: eval.RelativeTimeRange{ - From: eval.Duration(5 * time.Hour), - To: eval.Duration(3 * time.Hour), - }, + RefID: "B", + RelativeTimeRange: eval.RelativeTimeRange{ + From: eval.Duration(5 * time.Hour), + To: eval.Duration(3 * time.Hour), }, }, }, diff --git a/pkg/services/ngalert/models.go b/pkg/services/ngalert/models.go index 416c00b60da9c..da36f303abb78 100644 --- a/pkg/services/ngalert/models.go +++ b/pkg/services/ngalert/models.go @@ -73,28 +73,31 @@ type deleteAlertDefinitionByUIDCommand struct { // saveAlertDefinitionCommand is the query for saving a new alert definition. type saveAlertDefinitionCommand struct { - Title string `json:"title"` - OrgID int64 `json:"-"` - Condition eval.Condition `json:"condition"` - IntervalSeconds *int64 `json:"interval_seconds"` + Title string `json:"title"` + OrgID int64 `json:"-"` + Condition string `json:"condition"` + Data []eval.AlertQuery `json:"data"` + IntervalSeconds *int64 `json:"intervalSeconds"` Result *AlertDefinition } // updateAlertDefinitionCommand is the query for updating an existing alert definition. type updateAlertDefinitionCommand struct { - Title string `json:"title"` - OrgID int64 `json:"-"` - Condition eval.Condition `json:"condition"` - IntervalSeconds *int64 `json:"interval_seconds"` - UID string `json:"-"` + Title string `json:"title"` + OrgID int64 `json:"-"` + Condition string `json:"condition"` + Data []eval.AlertQuery `json:"data"` + IntervalSeconds *int64 `json:"intervalSeconds"` + UID string `json:"-"` Result *AlertDefinition } type evalAlertConditionCommand struct { - Condition eval.Condition `json:"condition"` - Now time.Time `json:"now"` + Condition string `json:"condition"` + Data []eval.AlertQuery `json:"data"` + Now time.Time `json:"now"` } type listAlertDefinitionsQuery struct { diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index 5a0b8b8495997..8461c67afea93 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -36,51 +36,6 @@ const setup = (propOverrides?: object) => { }; }; -describe('Render', () => { - it('should render component', () => { - const { wrapper } = setup(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render alert rules', () => { - const { wrapper } = setup({ - alertRules: [ - { - id: 1, - dashboardId: 7, - dashboardUid: 'ggHbN42mk', - dashboardSlug: 'alerting-with-testdata', - panelId: 3, - name: 'TestData - Always OK', - state: 'ok', - newStateDate: '2018-09-04T10:01:01+02:00', - evalDate: '0001-01-01T00:00:00Z', - evalData: {}, - executionError: '', - url: '/d/ggHbN42mk/alerting-with-testdata', - }, - { - id: 3, - dashboardId: 7, - dashboardUid: 'ggHbN42mk', - dashboardSlug: 'alerting-with-testdata', - panelId: 3, - name: 'TestData - ok', - state: 'ok', - newStateDate: '2018-09-04T10:01:01+02:00', - evalDate: '0001-01-01T00:00:00Z', - evalData: {}, - executionError: 'error', - url: '/d/ggHbN42mk/alerting-with-testdata', - }, - ], - }); - - expect(wrapper).toMatchSnapshot(); - }); -}); - describe('Life cycle', () => { describe('component did mount', () => { it('should call fetchrules', () => { diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 6515d13befe61..2e003de4fc558 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -12,7 +12,7 @@ import { getAlertRuleItems, getSearchQuery } from './state/selectors'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; import { NavModel, SelectableValue } from '@grafana/data'; import { setSearchQuery } from './state/reducers'; -import { Button, Select, VerticalGroup } from '@grafana/ui'; +import { Button, LinkButton, Select, VerticalGroup } from '@grafana/ui'; import { AlertDefinitionItem } from './components/AlertDefinitionItem'; export interface Props { @@ -118,6 +118,9 @@ export class AlertRuleList extends PureComponent {
+ + Add NG Alert + diff --git a/public/app/features/alerting/NextGenAlertingPage.tsx b/public/app/features/alerting/NextGenAlertingPage.tsx index d458462ebeb04..c872d1cdeefe6 100644 --- a/public/app/features/alerting/NextGenAlertingPage.tsx +++ b/public/app/features/alerting/NextGenAlertingPage.tsx @@ -4,6 +4,7 @@ import { MapDispatchToProps, MapStateToProps } 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'; @@ -13,45 +14,42 @@ import { updateAlertDefinitionOption, createAlertDefinition, updateAlertDefinitionUiState, - loadNotificationTypes, + updateAlertDefinition, + getAlertDefinition, } from './state/actions'; -import { - AlertDefinition, - AlertDefinitionUiState, - NotificationChannelType, - QueryGroupOptions, - StoreState, -} from '../../types'; - -import { config } from 'app/core/config'; +import { getRouteParamsId } from 'app/core/selectors/location'; +import { AlertDefinition, AlertDefinitionUiState, QueryGroupOptions, StoreState } from '../../types'; import { PanelQueryRunner } from '../query/state/PanelQueryRunner'; -interface OwnProps {} +interface OwnProps { + saveDefinition: typeof createAlertDefinition | typeof updateAlertDefinition; +} interface ConnectedProps { - alertDefinition: AlertDefinition; uiState: AlertDefinitionUiState; - notificationChannelTypes: NotificationChannelType[]; queryRunner: PanelQueryRunner; queryOptions: QueryGroupOptions; + alertDefinition: AlertDefinition; + pageId: string; } interface DispatchProps { - createAlertDefinition: typeof createAlertDefinition; updateAlertDefinitionUiState: typeof updateAlertDefinitionUiState; updateAlertDefinitionOption: typeof updateAlertDefinitionOption; - loadNotificationTypes: typeof loadNotificationTypes; + getAlertDefinition: typeof getAlertDefinition; + updateAlertDefinition: typeof updateAlertDefinition; + createAlertDefinition: typeof createAlertDefinition; } -interface State {} - type Props = OwnProps & ConnectedProps & DispatchProps; -class NextGenAlertingPage extends PureComponent { - state = { dataSources: [] }; - +class NextGenAlertingPage extends PureComponent { componentDidMount() { - this.props.loadNotificationTypes(); + const { getAlertDefinition, pageId } = this.props; + + if (pageId) { + getAlertDefinition(pageId); + } } onChangeAlertOption = (event: FormEvent) => { @@ -60,20 +58,24 @@ class NextGenAlertingPage extends PureComponent { onChangeInterval = (interval: SelectableValue) => { this.props.updateAlertDefinitionOption({ - interval: interval.value, + intervalSeconds: interval.value, }); }; onConditionChange = (condition: SelectableValue) => { this.props.updateAlertDefinitionOption({ - condition: { ...this.props.alertDefinition.condition, refId: condition.value! }, + condition: condition.value, }); }; onSaveAlert = () => { - const { createAlertDefinition } = this.props; + const { alertDefinition, createAlertDefinition, updateAlertDefinition } = this.props; - createAlertDefinition(); + if (alertDefinition.uid) { + updateAlertDefinition(); + } else { + createAlertDefinition(); + } }; onDiscard = () => {}; @@ -95,14 +97,7 @@ class NextGenAlertingPage extends PureComponent { } render() { - const { - alertDefinition, - notificationChannelTypes, - uiState, - updateAlertDefinitionUiState, - queryRunner, - queryOptions, - } = this.props; + const { alertDefinition, uiState, updateAlertDefinitionUiState, queryRunner, queryOptions } = this.props; const styles = getStyles(config.theme); return ( @@ -122,7 +117,6 @@ class NextGenAlertingPage extends PureComponent { { } const mapStateToProps: MapStateToProps = (state) => { + const pageId = getRouteParamsId(state.location); + return { uiState: state.alertDefinition.uiState, - alertDefinition: state.alertDefinition.alertDefinition, queryOptions: state.alertDefinition.queryOptions, - notificationChannelTypes: state.notificationChannel.notificationChannelTypes, queryRunner: state.alertDefinition.queryRunner, + alertDefinition: state.alertDefinition.alertDefinition, + pageId: (pageId as string) ?? '', }; }; const mapDispatchToProps: MapDispatchToProps = { - createAlertDefinition, updateAlertDefinitionUiState, updateAlertDefinitionOption, - loadNotificationTypes, + updateAlertDefinition, + createAlertDefinition, + getAlertDefinition, }; export default hot(module)( diff --git a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap deleted file mode 100644 index bb9a2e3fabbe0..0000000000000 --- a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap +++ /dev/null @@ -1,218 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render alert rules 1`] = ` - - -
-
- -
-
- -
- -
-
-
- -
- - - -`; diff --git a/public/app/features/alerting/components/AlertDefinitionItem.tsx b/public/app/features/alerting/components/AlertDefinitionItem.tsx index 4d52a5a5c5685..58a10f50a8986 100644 --- a/public/app/features/alerting/components/AlertDefinitionItem.tsx +++ b/public/app/features/alerting/components/AlertDefinitionItem.tsx @@ -1,9 +1,9 @@ import React, { FC } from 'react'; // @ts-ignore import Highlighter from 'react-highlight-words'; -import { Card, FeatureBadge, Icon } from '@grafana/ui'; -import { AlertDefinition } from 'app/types'; import { FeatureState } from '@grafana/data'; +import { Card, FeatureBadge, Icon, LinkButton } from '@grafana/ui'; +import { AlertDefinition } from 'app/types'; interface Props { alertDefinition: AlertDefinition; @@ -21,6 +21,13 @@ export const AlertDefinitionItem: FC = ({ alertDefinition, search }) => { {alertDefinition.description} + + {[ + + Edit alert + , + ]} + ); }; diff --git a/public/app/features/alerting/components/AlertDefinitionOptions.tsx b/public/app/features/alerting/components/AlertDefinitionOptions.tsx index 5a33165a8c8d7..c929960a5ab34 100644 --- a/public/app/features/alerting/components/AlertDefinitionOptions.tsx +++ b/public/app/features/alerting/components/AlertDefinitionOptions.tsx @@ -2,11 +2,16 @@ import React, { FC, FormEvent, useMemo } from 'react'; import { css } from 'emotion'; import { GrafanaTheme, SelectableValue } from '@grafana/data'; import { Field, Input, Select, Tab, TabContent, TabsBar, TextArea, useStyles } from '@grafana/ui'; -import { AlertDefinition, NotificationChannelType, QueryGroupOptions } from 'app/types'; +import { AlertDefinition, QueryGroupOptions } from 'app/types'; + +const intervalOptions: Array> = [ + { value: 60, label: '1m' }, + { value: 300, label: '5m' }, + { value: 600, label: '10m' }, +]; interface Props { alertDefinition: AlertDefinition; - notificationChannelTypes: NotificationChannelType[]; onChange: (event: FormEvent) => void; onIntervalChange: (interval: SelectableValue) => void; onConditionChange: (refId: SelectableValue) => void; @@ -49,12 +54,8 @@ export const AlertDefinitionOptions: FC = ({ Every r.value === alertDefinition.condition)} options={refIds} noOptionsMessage="No queries added" /> diff --git a/public/app/features/alerting/state/actions.ts b/public/app/features/alerting/state/actions.ts index fe1362d126e1e..cc94c6005e644 100644 --- a/public/app/features/alerting/state/actions.ts +++ b/public/app/features/alerting/state/actions.ts @@ -10,9 +10,10 @@ import { setNotificationChannels, setUiState, ALERT_DEFINITION_UI_STATE_STORAGE_KEY, - updateAlertDefinition, + updateAlertDefinitionOptions, setQueryOptions, setAlertDefinitions, + setAlertDefinition, } from './reducers'; import { AlertDefinition, @@ -22,6 +23,7 @@ import { ThunkResult, QueryGroupOptions, QueryGroupDataSource, + AlertDefinitionState, } from 'app/types'; import { ExpressionDatasourceID } from '../../expressions/ExpressionDatasource'; import { ExpressionQuery } from '../../expressions/types'; @@ -102,47 +104,16 @@ export function loadNotificationChannel(id: number): ThunkResult { }; } +export function getAlertDefinition(id: string): ThunkResult { + return async (dispatch) => { + const alertDefinition = await getBackendSrv().get(`/api/alert-definitions/${id}`); + dispatch(setAlertDefinition(alertDefinition)); + }; +} + export function createAlertDefinition(): ThunkResult { return async (dispatch, getStore) => { - const queryOptions = getStore().alertDefinition.queryOptions; - const currentAlertDefinition = getStore().alertDefinition.alertDefinition; - const defaultDataSource = await getDataSourceSrv().get(null); - - const alertDefinition: AlertDefinition = { - ...currentAlertDefinition, - condition: { - 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: { - ...query, - type: isExpression ? (query as ExpressionQuery).type : query.queryType, - datasource: dataSource.name, - datasourceUid: dataSource.uid, - }, - refId: query.refId, - relativeTimeRange: { - From: 500, - To: 0, - }, - }; - }), - }, - }; + const alertDefinition = await buildAlertDefinition(getStore().alertDefinition); await getBackendSrv().post(`/api/alert-definitions`, alertDefinition); appEvents.emit(AppEvents.alertSuccess, ['Alert definition created']); @@ -150,6 +121,19 @@ export function createAlertDefinition(): ThunkResult { }; } +export function updateAlertDefinition(): ThunkResult { + return async (dispatch, getStore) => { + const alertDefinition = await buildAlertDefinition(getStore().alertDefinition); + + const updatedAlertDefinition = await getBackendSrv().put( + `/api/alert-definitions/${alertDefinition.uid}`, + alertDefinition + ); + appEvents.emit(AppEvents.alertSuccess, ['Alert definition updated']); + dispatch(setAlertDefinition(updatedAlertDefinition)); + }; +} + export function updateAlertDefinitionUiState(uiState: Partial): ThunkResult { return (dispatch, getStore) => { const nextState = { ...getStore().alertDefinition.uiState, ...uiState }; @@ -165,7 +149,7 @@ export function updateAlertDefinitionUiState(uiState: Partial): ThunkResult { return (dispatch) => { - dispatch(updateAlertDefinition(alertDefinition)); + dispatch(updateAlertDefinitionOptions(alertDefinition)); }; } @@ -190,3 +174,42 @@ export function onRunQueries(): ThunkResult { }); }; } + +async function buildAlertDefinition(state: AlertDefinitionState) { + const queryOptions = state.queryOptions; + const currentAlertDefinition = state.alertDefinition; + const defaultDataSource = await getDataSourceSrv().get(null); + + return { + ...currentAlertDefinition, + data: queryOptions.queries.map((query) => { + let dataSource: QueryGroupDataSource; + const isExpression = query.datasource === ExpressionDatasourceID; + + if (isExpression) { + dataSource = { name: ExpressionDatasourceID, uid: ExpressionDatasourceID }; + } else { + const dataSourceSetting = getDataSourceSrv().getInstanceSettings(query.datasource); + + dataSource = { + name: dataSourceSetting?.name ?? defaultDataSource.name, + uid: dataSourceSetting?.uid ?? defaultDataSource.uid, + }; + } + + return { + model: { + ...query, + type: isExpression ? (query as ExpressionQuery).type : query.queryType, + datasource: dataSource.name, + datasourceUid: dataSource.uid, + }, + refId: query.refId, + relativeTimeRange: { + From: 500, + To: 0, + }, + }; + }), + }; +} diff --git a/public/app/features/alerting/state/reducers.ts b/public/app/features/alerting/state/reducers.ts index 4cc051f0ed9b9..79407f55ce9b4 100644 --- a/public/app/features/alerting/state/reducers.ts +++ b/public/app/features/alerting/state/reducers.ts @@ -2,8 +2,9 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { ApplyFieldOverrideOptions, DataTransformerConfig, dateTime, FieldColorModeId } from '@grafana/data'; import alertDef from './alertDef'; import { - AlertCondition, AlertDefinition, + AlertDefinitionDTO, + AlertDefinitionQueryModel, AlertDefinitionState, AlertDefinitionUiState, AlertRule, @@ -54,12 +55,14 @@ const dataConfig = { export const initialAlertDefinitionState: AlertDefinitionState = { alertDefinition: { id: 0, + uid: '', title: '', description: '', - condition: {} as AlertCondition, - interval: 60, + condition: '', + data: [], + intervalSeconds: 60, }, - queryOptions: { maxDataPoints: 100, dataSource: {}, queries: [] }, + 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: [], @@ -156,10 +159,25 @@ const alertDefinitionSlice = createSlice({ name: 'alertDefinition', initialState: initialAlertDefinitionState, reducers: { - setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction) => { - return { ...state, alertDefinition: action.payload }; + setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction) => { + return { + ...state, + alertDefinition: { + title: action.payload.title, + id: action.payload.id, + uid: action.payload.uid, + condition: action.payload.condition, + intervalSeconds: action.payload.intervalSeconds, + data: action.payload.data, + description: '', + }, + queryOptions: { + ...state.queryOptions, + queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })), + }, + }; }, - updateAlertDefinition: (state: AlertDefinitionState, action: PayloadAction>) => { + updateAlertDefinitionOptions: (state: AlertDefinitionState, action: PayloadAction>) => { return { ...state, alertDefinition: { ...state.alertDefinition, ...action.payload } }; }, setUiState: (state: AlertDefinitionState, action: PayloadAction) => { @@ -185,7 +203,13 @@ export const { resetSecureField, } = notificationChannelSlice.actions; -export const { setUiState, updateAlertDefinition, setQueryOptions, setAlertDefinitions } = alertDefinitionSlice.actions; +export const { + setUiState, + updateAlertDefinitionOptions, + setQueryOptions, + setAlertDefinitions, + setAlertDefinition, +} = alertDefinitionSlice.actions; export const alertRulesReducer = alertRulesSlice.reducer; export const notificationChannelReducer = notificationChannelSlice.reducer; diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 9edc0000bbbbd..f07cdf3804419 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -556,7 +556,18 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati ), }, }) - .when('/ngalerting', { + .when('/alerting/new', { + template: '', + resolve: { + component: () => + SafeDynamicImport( + import(/* webpackChunkName: "NgAlertingPage"*/ 'app/features/alerting/NextGenAlertingPage') + ), + }, + //@ts-ignore + pageClass: 'page-alerting', + }) + .when('/alerting/:id/edit', { template: '', resolve: { component: () => diff --git a/public/app/types/alerting.ts b/public/app/types/alerting.ts index fe040e54e7349..71c04156f03d6 100644 --- a/public/app/types/alerting.ts +++ b/public/app/types/alerting.ts @@ -1,6 +1,7 @@ -import { PanelData, SelectableValue } from '@grafana/data'; +import { DataQuery, PanelData, SelectableValue, TimeRange } from '@grafana/data'; import { PanelQueryRunner } from '../features/query/state/PanelQueryRunner'; import { QueryGroupOptions } from './query'; +import { ExpressionQuery } from '../features/expressions/types'; export interface AlertRuleDTO { id: number; @@ -147,15 +148,25 @@ export interface AlertDefinitionState { export interface AlertDefinition { id: number; + uid: string; title: string; description: string; - condition: AlertCondition; - interval: number; + condition: string; + data: any[]; + intervalSeconds: number; } -export interface AlertCondition { +export interface AlertDefinitionDTO extends AlertDefinition { + queryType: string; refId: string; - queriesAndExpressions: any[]; + relativeTimeRange: TimeRange; + orgId: number; + updated: string; + version: number; +} + +export interface AlertDefinitionQueryModel { + model: DataQuery | ExpressionQuery; } export interface AlertDefinitionUiState {