diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index ae0e39cc26d..d63af72ae4d 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -24,12 +24,14 @@ class EmptyListCTA extends Component { {buttonTitle} -
- ProTip: {proTip} - - {proTipLinkTitle} - -
+ {proTip && ( +
+ ProTip: {proTip} + + {proTipLinkTitle} + +
+ )} ); } diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 6d87c159d02..2be25e9df6a 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -45,6 +45,7 @@ export class AlertTabCtrl { this.noDataModes = alertDef.noDataModes; this.executionErrorModes = alertDef.executionErrorModes; this.appSubUrl = config.appSubUrl; + this.panelCtrl._enableAlert = this.enable; } $onInit() { @@ -114,7 +115,7 @@ export class AlertTabCtrl { } getNotifications() { - return Promise.resolve( + return this.$q.when( this.notifications.map(item => { return this.uiSegmentSrv.newSegment(item.name); }) @@ -147,6 +148,7 @@ export class AlertTabCtrl { // reset plus button this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value; this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html; + this.addNotificationSegment.fake = true; } removeNotification(index) { @@ -353,11 +355,11 @@ export class AlertTabCtrl { }); } - enable() { + enable = () => { this.panel.alert = {}; this.initModel(); this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes - } + }; evaluatorParamsChanged() { ThresholdMapper.alertToGraphThresholds(this.panel); diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 0e4e48a89a9..da862203da6 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -1,191 +1,168 @@ -
-
- +
+
+ {{ctrl.error}} +
+
+
+
+

Rule

+
+
+ Name + +
+
+ Evaluate every + +
+
+ + + + If an alert rule has a configured For and the query violates the configured + threshold it + will first go from OK to Pending. + Going from OK to Pending Grafana will not send any notifications. Once the alert + rule + has + been firing for more than For duration, it will change to Alerting and send alert + notifications. + +
+
+
-
-
-
- {{ctrl.error}} -
+
+

Conditions

+
+
+ + WHEN +
+
+ + + OF +
+
+ + +
+
+ + + + +
+
+ +
+
-
-
Alert Config
-
- Name - -
-
-
- Evaluate every - -
-
- - - - If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending. - Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications. - -
-
-
+
+ +
+
-
-
Conditions
-
-
- - WHEN -
-
- - - OF -
-
- - -
-
- - - - -
-
- -
-
+
+

No Data & Error Handling

+
+
+ If no data or all values are null +
+
+ SET STATE TO +
+ +
+
+
-
- -
-
+
+
+ If execution error or timeout +
+
+ SET STATE TO +
+ +
+
+
-
-
- If no data or all values are null - SET STATE TO -
- -
-
+
+ +
+
-
- If execution error or timeout - SET STATE TO -
- -
-
+
+ Evaluating rule +
-
- -
-
+
+ +
+
+
-
- Evaluating rule -
- -
- -
-
- -
-
Notifications
-
-
- Send to - -  {{nc.name}}  - - - -
-
-
- Message - -
-
- -
- -
- State history (last 50 state changes) -
- -
-
- No state changes recorded -
- -
    -
  1. -
    - -
    -
    -
    -
    - {{al.stateModel.text}} -
    -
    - {{al.info}} -
    -
    - {{al.time}} -
    -
  2. -
-
-
-
-
- -
-
-
Panel has no alert rule defined
- -
-
+
+
Notifications
+
+
+
+ Send to +
+
+ +  {{nc.name}}  + + +
+
+ +
+
+
+ Message + +
+
+
diff --git a/public/app/features/dashboard/dashgrid/AlertTab.tsx b/public/app/features/dashboard/dashgrid/AlertTab.tsx index 7df7864c758..20f7e90633e 100644 --- a/public/app/features/dashboard/dashgrid/AlertTab.tsx +++ b/public/app/features/dashboard/dashgrid/AlertTab.tsx @@ -1,20 +1,30 @@ +// Libraries import React, { PureComponent } from 'react'; -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; -import { EditorTabBody } from './EditorTabBody'; +// Services & Utils +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; +import appEvents from 'app/core/app_events'; + +// Components +import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import StateHistory from './StateHistory'; import 'app/features/alerting/AlertTabCtrl'; +// Types +import { DashboardModel } from '../dashboard_model'; +import { PanelModel } from '../panel_model'; + interface Props { angularPanel?: AngularComponent; + dashboard: DashboardModel; + panel: PanelModel; } export class AlertTab extends PureComponent { element: any; component: AngularComponent; - - constructor(props) { - super(props); - } + panelCtrl: any; componentDidMount() { if (this.shouldLoadAlertTab()) { @@ -29,7 +39,7 @@ export class AlertTab extends PureComponent { } shouldLoadAlertTab() { - return this.props.angularPanel && this.element; + return this.props.angularPanel && this.element && !this.component; } componentWillUnmount() { @@ -51,21 +61,80 @@ export class AlertTab extends PureComponent { return; } - const panelCtrl = scope.$$childHead.ctrl; + this.panelCtrl = scope.$$childHead.ctrl; const loader = getAngularLoader(); const template = ''; const scopeProps = { - ctrl: panelCtrl, + ctrl: this.panelCtrl, }; this.component = loader.load(this.element, scopeProps, template); } + stateHistory = (): EditorToolbarView => { + return { + title: 'State history', + render: () => { + return ( + + ); + }, + }; + }; + + deleteAlert = (): EditorToolbarView => { + const { panel } = this.props; + return { + title: 'Delete', + btnType: 'danger', + onClick: () => { + appEvents.emit('confirm-modal', { + title: 'Delete Alert', + text: 'Are you sure you want to delete this alert rule?', + text2: 'You need to save dashboard for the delete to take effect', + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + delete panel.alert; + panel.thresholds = []; + this.panelCtrl.alertState = null; + this.panelCtrl.render(); + this.forceUpdate(); + }, + }); + }, + }; + }; + + onAddAlert = () => { + this.panelCtrl._enableAlert(); + this.component.digest(); + this.forceUpdate(); + }; + render() { + const { alert } = this.props.panel; + + const toolbarItems = alert ? [this.stateHistory(), this.deleteAlert()] : []; + + const model = { + title: 'Panel has no alert rule defined', + icon: 'icon-gf icon-gf-alert', + onClick: this.onAddAlert, + buttonTitle: 'Create Alert', + }; + return ( - -
(this.element = element)} /> + + <> +
(this.element = element)} /> + {!alert && } + ); } diff --git a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx index 7606d327405..b7da81a23f8 100644 --- a/public/app/features/dashboard/dashgrid/EditorTabBody.tsx +++ b/public/app/features/dashboard/dashgrid/EditorTabBody.tsx @@ -10,21 +10,22 @@ interface Props { children: JSX.Element; heading: string; renderToolbar?: () => JSX.Element; - toolbarItems?: EditorToolBarView[]; + toolbarItems?: EditorToolbarView[]; } -export interface EditorToolBarView { +export interface EditorToolbarView { title?: string; heading?: string; - imgSrc?: string; icon?: string; disabled?: boolean; onClick?: () => void; - render: (closeFunction?: any) => JSX.Element | JSX.Element[]; + render?: () => JSX.Element; + action?: () => void; + btnType?: 'danger'; } interface State { - openView?: EditorToolBarView; + openView?: EditorToolbarView; isOpen: boolean; fadeIn: boolean; } @@ -48,7 +49,7 @@ export class EditorTabBody extends PureComponent { this.setState({ fadeIn: true }); } - onToggleToolBarView = (item: EditorToolBarView) => { + onToggleToolBarView = (item: EditorToolbarView) => { this.setState({ openView: item, isOpen: !this.state.isOpen, @@ -74,12 +75,15 @@ export class EditorTabBody extends PureComponent { return state; } - renderButton(view: EditorToolBarView) { + renderButton(view: EditorToolbarView) { const onClick = () => { if (view.onClick) { view.onClick(); } - this.onToggleToolBarView(view); + + if (view.render) { + this.onToggleToolBarView(view); + } }; return ( @@ -91,7 +95,7 @@ export class EditorTabBody extends PureComponent { ); } - renderOpenView(view: EditorToolBarView) { + renderOpenView(view: EditorToolbarView) { return ( {view.render()} diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index a746d6c4b91..fbc683c2eb3 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -54,7 +54,7 @@ export class PanelEditor extends PureComponent { case 'queries': return ; case 'alert': - return ; + return ; case 'visualization': return ( { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; - const queryInspector = { + const queryInspector: EditorToolbarView = { title: 'Query Inspector', render: this.renderQueryInspector, }; - const dsHelp = { + const dsHelp: EditorToolbarView = { heading: 'Help', icon: 'fa fa-question', render: this.renderHelp, diff --git a/public/app/features/dashboard/dashgrid/StateHistory.tsx b/public/app/features/dashboard/dashgrid/StateHistory.tsx new file mode 100644 index 00000000000..99229b41848 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/StateHistory.tsx @@ -0,0 +1,110 @@ +import React, { PureComponent } from 'react'; +import alertDef from '../../alerting/state/alertDef'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { DashboardModel } from '../dashboard_model'; +import appEvents from '../../../core/app_events'; + +interface Props { + dashboard: DashboardModel; + panelId: number; + onRefresh: () => void; +} + +interface State { + stateHistoryItems: any[]; +} + +class StateHistory extends PureComponent { + state = { + stateHistoryItems: [], + }; + + componentDidMount(): void { + const { dashboard, panelId } = this.props; + + getBackendSrv() + .get(`/api/annotations?dashboardId=${dashboard.id}&panelId=${panelId}&limit=50&type=alert`) + .then(res => { + const items = res.map(item => { + return { + stateModel: alertDef.getStateDisplayModel(item.newState), + time: dashboard.formatDate(item.time, 'MMM D, YYYY HH:mm:ss'), + info: alertDef.getAlertAnnotationInfo(item), + }; + }); + + this.setState({ + stateHistoryItems: items, + }); + }); + } + + clearHistory = () => { + const { dashboard, onRefresh, panelId } = this.props; + + appEvents.emit('confirm-modal', { + title: 'Delete Alert History', + text: 'Are you sure you want to remove all history & annotations for this alert?', + icon: 'fa-trash', + yesText: 'Yes', + onConfirm: () => { + getBackendSrv() + .post('/api/annotations/mass-delete', { + dashboardId: dashboard.id, + panelId: panelId, + }) + .then(() => { + onRefresh(); + }); + + this.setState({ + stateHistoryItems: [], + }); + }, + }); + }; + + render() { + const { stateHistoryItems } = this.state; + + return ( +
+ {stateHistoryItems.length > 0 && ( +
+ Last 50 state changes + +
+ )} +
    + {stateHistoryItems.length > 0 ? ( + stateHistoryItems.map((item, index) => { + return ( +
  1. +
    + +
    +
    +
    +

    {item.alertName}

    +
    + {item.stateModel.text} +
    +
    + {item.info} +
    +
    {item.time}
    +
  2. + ); + }) + ) : ( + No state changes recorded + )} +
+
+ ); + } +} + +export default StateHistory; diff --git a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx index 42d9bf6a6eb..bc7102f35dd 100644 --- a/public/app/features/dashboard/dashgrid/VisualizationTab.tsx +++ b/public/app/features/dashboard/dashgrid/VisualizationTab.tsx @@ -2,10 +2,10 @@ import React, { PureComponent } from 'react'; // Utils & Services -import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader'; +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; // Components -import { EditorTabBody } from './EditorTabBody'; +import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { VizTypePicker } from './VizTypePicker'; import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp'; import { FadeIn } from 'app/core/components/Animations/FadeIn'; @@ -206,7 +206,7 @@ export class VisualizationTab extends PureComponent { const { plugin } = this.props; const { isVizPickerOpen, searchQuery } = this.state; - const pluginHelp = { + const pluginHelp: EditorToolbarView = { heading: 'Help', icon: 'fa fa-question', render: this.renderHelp, diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 77752be11bc..f285ab753ff 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -107,6 +107,7 @@ display: flex; flex-direction: column; flex-grow: 1; + justify-content: center; overflow: hidden; }