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 <papagian@users.noreply.github.com>
Co-authored-by: Sofia Papagiannaki <sofia@grafana.com>
This commit is contained in:
Peter Holmberg
2021-02-04 09:13:02 +01:00
committed by GitHub
parent 21817055bd
commit aaf5710748
16 changed files with 287 additions and 471 deletions

View File

@@ -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))
}

View File

@@ -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)
}

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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),
},
},
},

View File

@@ -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 {

View File

@@ -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', () => {

View File

@@ -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<Props, any> {
</div>
</div>
<div className="page-action-bar__spacer" />
<LinkButton variant="primary" href="alerting/new">
Add NG Alert
</LinkButton>
<Button variant="secondary" onClick={this.onOpenHowTo}>
How to add an alert
</Button>

View File

@@ -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<Props, State> {
state = { dataSources: [] };
class NextGenAlertingPage extends PureComponent<Props> {
componentDidMount() {
this.props.loadNotificationTypes();
const { getAlertDefinition, pageId } = this.props;
if (pageId) {
getAlertDefinition(pageId);
}
}
onChangeAlertOption = (event: FormEvent<HTMLFormElement>) => {
@@ -60,20 +58,24 @@ class NextGenAlertingPage extends PureComponent<Props, State> {
onChangeInterval = (interval: SelectableValue<number>) => {
this.props.updateAlertDefinitionOption({
interval: interval.value,
intervalSeconds: interval.value,
});
};
onConditionChange = (condition: SelectableValue<string>) => {
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<Props, State> {
}
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<Props, State> {
<AlertDefinitionOptions
alertDefinition={alertDefinition}
onChange={this.onChangeAlertOption}
notificationChannelTypes={notificationChannelTypes}
onIntervalChange={this.onChangeInterval}
onConditionChange={this.onConditionChange}
queryOptions={queryOptions}
@@ -136,20 +130,23 @@ class NextGenAlertingPage extends PureComponent<Props, State> {
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (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<DispatchProps, OwnProps> = {
createAlertDefinition,
updateAlertDefinitionUiState,
updateAlertDefinitionOption,
loadNotificationTypes,
updateAlertDefinition,
createAlertDefinition,
getAlertDefinition,
};
export default hot(module)(

View File

@@ -1,218 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render alert rules 1`] = `
<Page
navModel={Object {}}
>
<PageContents
isLoading={false}
>
<div
className="page-action-bar"
>
<div
className="gf-form gf-form--grow"
>
<FilterInput
inputClassName="gf-form-input"
labelClassName="gf-form--has-input-icon gf-form--grow"
onChange={[Function]}
placeholder="Search alerts"
value=""
/>
</div>
<div
className="gf-form"
>
<label
className="gf-form-label"
>
States
</label>
<div
className="width-13"
>
<Select
onChange={[Function]}
options={
Array [
Object {
"label": "All",
"value": "all",
},
Object {
"label": "OK",
"value": "ok",
},
Object {
"label": "Not OK",
"value": "not_ok",
},
Object {
"label": "Alerting",
"value": "alerting",
},
Object {
"label": "No Data",
"value": "no_data",
},
Object {
"label": "Paused",
"value": "paused",
},
Object {
"label": "Pending",
"value": "pending",
},
]
}
value="all"
/>
</div>
</div>
<div
className="page-action-bar__spacer"
/>
<Button
onClick={[Function]}
variant="secondary"
>
How to add an alert
</Button>
</div>
<VerticalGroup
spacing="none"
>
<AlertRuleItem
key="1"
onTogglePause={[Function]}
rule={
Object {
"dashboardId": 7,
"dashboardSlug": "alerting-with-testdata",
"dashboardUid": "ggHbN42mk",
"evalData": Object {},
"evalDate": "0001-01-01T00:00:00Z",
"executionError": "",
"id": 1,
"name": "TestData - Always OK",
"newStateDate": "2018-09-04T10:01:01+02:00",
"panelId": 3,
"state": "ok",
"url": "/d/ggHbN42mk/alerting-with-testdata",
}
}
search=""
/>
<AlertRuleItem
key="3"
onTogglePause={[Function]}
rule={
Object {
"dashboardId": 7,
"dashboardSlug": "alerting-with-testdata",
"dashboardUid": "ggHbN42mk",
"evalData": Object {},
"evalDate": "0001-01-01T00:00:00Z",
"executionError": "error",
"id": 3,
"name": "TestData - ok",
"newStateDate": "2018-09-04T10:01:01+02:00",
"panelId": 3,
"state": "ok",
"url": "/d/ggHbN42mk/alerting-with-testdata",
}
}
search=""
/>
</VerticalGroup>
</PageContents>
</Page>
`;
exports[`Render should render component 1`] = `
<Page
navModel={Object {}}
>
<PageContents
isLoading={false}
>
<div
className="page-action-bar"
>
<div
className="gf-form gf-form--grow"
>
<FilterInput
inputClassName="gf-form-input"
labelClassName="gf-form--has-input-icon gf-form--grow"
onChange={[Function]}
placeholder="Search alerts"
value=""
/>
</div>
<div
className="gf-form"
>
<label
className="gf-form-label"
>
States
</label>
<div
className="width-13"
>
<Select
onChange={[Function]}
options={
Array [
Object {
"label": "All",
"value": "all",
},
Object {
"label": "OK",
"value": "ok",
},
Object {
"label": "Not OK",
"value": "not_ok",
},
Object {
"label": "Alerting",
"value": "alerting",
},
Object {
"label": "No Data",
"value": "no_data",
},
Object {
"label": "Paused",
"value": "paused",
},
Object {
"label": "Pending",
"value": "pending",
},
]
}
value="all"
/>
</div>
</div>
<div
className="page-action-bar__spacer"
/>
<Button
onClick={[Function]}
variant="secondary"
>
How to add an alert
</Button>
</div>
<VerticalGroup
spacing="none"
/>
</PageContents>
</Page>
`;

View File

@@ -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<Props> = ({ alertDefinition, search }) => {
<span key="text">{alertDefinition.description}</span>
</span>
</Card.Meta>
<Card.Actions>
{[
<LinkButton key="edit" variant="secondary" href={`/alerting/${alertDefinition.uid}/edit`} icon="cog">
Edit alert
</LinkButton>,
]}
</Card.Actions>
</Card>
);
};

View File

@@ -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<SelectableValue<number>> = [
{ 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<number>) => void;
onConditionChange: (refId: SelectableValue<string>) => void;
@@ -49,12 +54,8 @@ export const AlertDefinitionOptions: FC<Props> = ({
<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' },
]}
value={intervalOptions.find((i) => i.value === alertDefinition.intervalSeconds)}
options={intervalOptions}
width={10}
/>
</div>
@@ -63,7 +64,7 @@ export const AlertDefinitionOptions: FC<Props> = ({
<div className={styles.optionRow}>
<Select
onChange={onConditionChange}
value={alertDefinition.condition.refId}
value={refIds.find((r) => r.value === alertDefinition.condition)}
options={refIds}
noOptionsMessage="No queries added"
/>

View File

@@ -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<void> {
};
}
export function getAlertDefinition(id: string): ThunkResult<void> {
return async (dispatch) => {
const alertDefinition = await getBackendSrv().get(`/api/alert-definitions/${id}`);
dispatch(setAlertDefinition(alertDefinition));
};
}
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 = {
...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<void> {
};
}
export function updateAlertDefinition(): ThunkResult<void> {
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<AlertDefinitionUiState>): ThunkResult<void> {
return (dispatch, getStore) => {
const nextState = { ...getStore().alertDefinition.uiState, ...uiState };
@@ -165,7 +149,7 @@ export function updateAlertDefinitionUiState(uiState: Partial<AlertDefinitionUiS
export function updateAlertDefinitionOption(alertDefinition: Partial<AlertDefinition>): ThunkResult<void> {
return (dispatch) => {
dispatch(updateAlertDefinition(alertDefinition));
dispatch(updateAlertDefinitionOptions(alertDefinition));
};
}
@@ -190,3 +174,42 @@ export function onRunQueries(): ThunkResult<void> {
});
};
}
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,
},
};
}),
};
}

View File

@@ -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<any>) => {
return { ...state, alertDefinition: action.payload };
setAlertDefinition: (state: AlertDefinitionState, action: PayloadAction<AlertDefinitionDTO>) => {
return {
...state,
alertDefinition: {
title: action.payload.title,
id: action.payload.id,
uid: action.payload.uid,
condition: action.payload.condition,
intervalSeconds: action.payload.intervalSeconds,
data: action.payload.data,
description: '',
},
queryOptions: {
...state.queryOptions,
queries: action.payload.data.map((q: AlertDefinitionQueryModel) => ({ ...q.model })),
},
};
},
updateAlertDefinition: (state: AlertDefinitionState, action: PayloadAction<Partial<AlertDefinition>>) => {
updateAlertDefinitionOptions: (state: AlertDefinitionState, action: PayloadAction<Partial<AlertDefinition>>) => {
return { ...state, alertDefinition: { ...state.alertDefinition, ...action.payload } };
},
setUiState: (state: AlertDefinitionState, action: PayloadAction<AlertDefinitionUiState>) => {
@@ -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;

View File

@@ -556,7 +556,18 @@ export function setupAngularRoutes($routeProvider: route.IRouteProvider, $locati
),
},
})
.when('/ngalerting', {
.when('/alerting/new', {
template: '<react-container />',
resolve: {
component: () =>
SafeDynamicImport(
import(/* webpackChunkName: "NgAlertingPage"*/ 'app/features/alerting/NextGenAlertingPage')
),
},
//@ts-ignore
pageClass: 'page-alerting',
})
.when('/alerting/:id/edit', {
template: '<react-container />',
resolve: {
component: () =>

View File

@@ -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 {