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