diff --git a/docs/sources/http_api/admin.md b/docs/sources/http_api/admin.md index 2d4be21bb78..a27fd2aac14 100644 --- a/docs/sources/http_api/admin.md +++ b/docs/sources/http_api/admin.md @@ -285,7 +285,7 @@ Content-Type: application/json HTTP/1.1 200 Content-Type: application/json -{message: "User permissions updated"} +{"message": "User permissions updated"} ``` ## Delete global User @@ -308,7 +308,7 @@ Content-Type: application/json HTTP/1.1 200 Content-Type: application/json -{message: "User deleted"} +{"message": "User deleted"} ``` ## Pause all alerts @@ -339,5 +339,5 @@ JSON Body schema: HTTP/1.1 200 Content-Type: application/json -{state: "new state", message: "alerts pause/un paused", "alertsAffected": 100} +{"state": "new state", "message": "alerts pause/un paused", "alertsAffected": 100} ``` diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 7ed44572533..28b975e31e8 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -34,32 +34,23 @@ sudo dpkg -i grafana__amd64.deb Example: ```bash -wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_5.1.4_amd64.deb +wget https://dl.grafana.com/oss/release/grafana_5.4.2_amd64.deb sudo apt-get install -y adduser libfontconfig -sudo dpkg -i grafana_5.1.4_amd64.deb +sudo dpkg -i grafana_5.4.2_amd64.deb ``` ## APT Repository -Add the following line to your `/etc/apt/sources.list` file. +Create a file `/etc/apt/sources.list.d/grafana.list` and add the following to it. ```bash -deb https://packagecloud.io/grafana/stable/debian/ stretch main +deb https://packages.grafana.com/oss/deb stable main ``` -Use the above line even if you are on Ubuntu or another Debian version. -There is also a testing repository if you want beta or release -candidates. +Use the above line even if you are on Ubuntu or another Debian version. Then add our gpg key. This allows you to install signed packages. ```bash -deb https://packagecloud.io/grafana/testing/debian/ stretch main -``` - -Then add the [Package Cloud](https://packagecloud.io/grafana) key. This -allows you to install signed packages. - -```bash -curl https://packagecloud.io/gpg.key | sudo apt-key add - +curl https://packages.grafana.com/gpg.key | sudo apt-key add - ``` Update your Apt repositories and install Grafana diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 5bf3b7ed745..559bb0a8ef4 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -32,7 +32,7 @@ $ sudo yum install Example: ```bash -$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm +$ sudo yum install https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm ``` Or install manually using `rpm`. First execute @@ -44,7 +44,7 @@ $ wget Example: ```bash -$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.1.4-1.x86_64.rpm +$ wget https://dl.grafana.com/oss/release/grafana-5.4.2-1.x86_64.rpm ``` ### On CentOS / Fedora / Redhat: @@ -67,21 +67,15 @@ Add the following to a new file at `/etc/yum.repos.d/grafana.repo` ```bash [grafana] name=grafana -baseurl=https://packagecloud.io/grafana/stable/el/7/$basearch +baseurl=https://packages.grafana.com/oss/rpm repo_gpgcheck=1 enabled=1 gpgcheck=1 -gpgkey=https://packagecloud.io/gpg.key https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana +gpgkey=https://packages.grafana.com/gpg.key sslverify=1 sslcacert=/etc/pki/tls/certs/ca-bundle.crt ``` -There is also a testing repository if you want beta or release candidates. - -```bash -baseurl=https://packagecloud.io/grafana/testing/el/7/$basearch -``` - Then install Grafana via the `yum` command. ```bash @@ -91,7 +85,7 @@ $ sudo yum install grafana ### RPM GPG Key The RPMs are signed, you can verify the signature with this [public GPG -key](https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana). +key](https://packages.grafana.com/gpg.key). ## Package details diff --git a/pkg/components/dashdiffs/formatter_json.go b/pkg/components/dashdiffs/formatter_json.go index 488a345d492..09dac1ae5ac 100644 --- a/pkg/components/dashdiffs/formatter_json.go +++ b/pkg/components/dashdiffs/formatter_json.go @@ -206,10 +206,9 @@ func (f *JSONFormatter) processObject(object map[string]interface{}, deltas []di // Added for _, delta := range deltas { - switch delta.(type) { + switch delta := delta.(type) { case *diff.Added: - d := delta.(*diff.Added) - f.printRecursive(d.Position.String(), d.Value, ChangeAdded) + f.printRecursive(delta.Position.String(), delta.Value, ChangeAdded) } } @@ -222,9 +221,8 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi if len(matchedDeltas) > 0 { for _, matchedDelta := range matchedDeltas { - switch matchedDelta.(type) { + switch matchedDelta := matchedDelta.(type) { case *diff.Object: - d := matchedDelta.(*diff.Object) switch value.(type) { case map[string]interface{}: //ok @@ -238,7 +236,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.print("{") f.closeLine() f.push(positionStr, len(o), false) - f.processObject(o, d.Deltas) + f.processObject(o, matchedDelta.Deltas) f.pop() f.newLine(ChangeNil) f.print("}") @@ -246,7 +244,6 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.closeLine() case *diff.Array: - d := matchedDelta.(*diff.Array) switch value.(type) { case []interface{}: //ok @@ -260,7 +257,7 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.print("[") f.closeLine() f.push(positionStr, len(a), true) - f.processArray(a, d.Deltas) + f.processArray(a, matchedDelta.Deltas) f.pop() f.newLine(ChangeNil) f.print("]") @@ -268,27 +265,23 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi f.closeLine() case *diff.Added: - d := matchedDelta.(*diff.Added) - f.printRecursive(positionStr, d.Value, ChangeAdded) + f.printRecursive(positionStr, matchedDelta.Value, ChangeAdded) f.size[len(f.size)-1]++ case *diff.Modified: - d := matchedDelta.(*diff.Modified) savedSize := f.size[len(f.size)-1] - f.printRecursive(positionStr, d.OldValue, ChangeOld) + f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, ChangeNew) + f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew) case *diff.TextDiff: savedSize := f.size[len(f.size)-1] - d := matchedDelta.(*diff.TextDiff) - f.printRecursive(positionStr, d.OldValue, ChangeOld) + f.printRecursive(positionStr, matchedDelta.OldValue, ChangeOld) f.size[len(f.size)-1] = savedSize - f.printRecursive(positionStr, d.NewValue, ChangeNew) + f.printRecursive(positionStr, matchedDelta.NewValue, ChangeNew) case *diff.Deleted: - d := matchedDelta.(*diff.Deleted) - f.printRecursive(positionStr, d.Value, ChangeDeleted) + f.printRecursive(positionStr, matchedDelta.Value, ChangeDeleted) default: return errors.New("Unknown Delta type detected") @@ -305,13 +298,13 @@ func (f *JSONFormatter) processItem(value interface{}, deltas []diff.Delta, posi func (f *JSONFormatter) searchDeltas(deltas []diff.Delta, position diff.Position) (results []diff.Delta) { results = make([]diff.Delta, 0) for _, delta := range deltas { - switch delta.(type) { + switch typedDelta := delta.(type) { case diff.PostDelta: - if delta.(diff.PostDelta).PostPosition() == position { + if typedDelta.PostPosition() == position { results = append(results, delta) } case diff.PreDelta: - if delta.(diff.PreDelta).PrePosition() == position { + if typedDelta.PrePosition() == position { results = append(results, delta) } default: @@ -417,20 +410,19 @@ func (f *JSONFormatter) print(a string) { } func (f *JSONFormatter) printRecursive(name string, value interface{}, change ChangeType) { - switch value.(type) { + switch value := value.(type) { case map[string]interface{}: f.newLine(change) f.printKey(name) f.print("{") f.closeLine() - m := value.(map[string]interface{}) - size := len(m) + size := len(value) f.push(name, size, false) - keys := sortKeys(m) + keys := sortKeys(value) for _, key := range keys { - f.printRecursive(key, m[key], change) + f.printRecursive(key, value[key], change) } f.pop() @@ -445,10 +437,9 @@ func (f *JSONFormatter) printRecursive(name string, value interface{}, change Ch f.print("[") f.closeLine() - s := value.([]interface{}) - size := len(s) + size := len(value) f.push("", size, true) - for _, item := range s { + for _, item := range value { f.printRecursive("", item, change) } f.pop() diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 6aa1276b093..59ceefa0be5 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -76,7 +76,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, return nil, models.ErrDashboardFolderCannotHaveParent } - if dash.IsFolder && strings.ToLower(dash.Title) == strings.ToLower(models.RootFolderName) { + if dash.IsFolder && strings.EqualFold(dash.Title, models.RootFolderName) { return nil, models.ErrDashboardFolderNameExists } diff --git a/pkg/services/sqlstore/login_attempt.go b/pkg/services/sqlstore/login_attempt.go index 78da198e8e7..ceff2394dce 100644 --- a/pkg/services/sqlstore/login_attempt.go +++ b/pkg/services/sqlstore/login_attempt.go @@ -78,14 +78,14 @@ func GetUserLoginAttemptCount(query *m.GetUserLoginAttemptCountQuery) error { } func toInt64(i interface{}) int64 { - switch i.(type) { + switch i := i.(type) { case []byte: - n, _ := strconv.ParseInt(string(i.([]byte)), 10, 64) + n, _ := strconv.ParseInt(string(i), 10, 64) return n case int: - return int64(i.(int)) + return int64(i) case int64: - return i.(int64) + return i } return 0 } diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 26a3d1e53ee..0b3685c2610 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -86,11 +86,11 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil case "__timeFrom": - return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339Nano)), nil case "__timeTo": - return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339Nano)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 6a71caedeb9..90f6c6d879a 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -41,7 +41,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano))) }) Convey("interpolate __timeFrom function", func() { @@ -138,7 +138,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano))) }) Convey("interpolate __unixEpochFilter function", func() { @@ -158,7 +158,7 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") So(err, ShouldBeNil) - So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339))) + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano))) }) Convey("interpolate __unixEpochFilter function", func() { @@ -168,5 +168,22 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select time >= %d AND time <= %d", from.Unix(), to.Unix())) }) }) + + Convey("Given a time range between 1960-02-01 07:00:00.5 and 1980-02-03 08:00:00.5", func() { + from := time.Date(1960, 2, 1, 7, 0, 0, 500e6, time.UTC) + to := time.Date(1980, 2, 3, 8, 0, 0, 500e6, time.UTC) + timeRange := tsdb.NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10)) + + So(from.Format(time.RFC3339Nano), ShouldEqual, "1960-02-01T07:00:00.5Z") + So(to.Format(time.RFC3339Nano), ShouldEqual, "1980-02-03T08:00:00.5Z") + Convey("interpolate __timeFilter function", func() { + sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339Nano), to.Format(time.RFC3339Nano))) + }) + + }) + }) } diff --git a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx index 590c11c0615..977892c637d 100644 --- a/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx +++ b/public/app/core/components/CustomScrollbar/CustomScrollbar.tsx @@ -28,8 +28,10 @@ class CustomScrollbar extends PureComponent {
} renderTrackVertical={props =>
} renderThumbHorizontal={props =>
} diff --git a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap b/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap index 310eb714af9..0a7de5fcffe 100644 --- a/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap +++ b/public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap @@ -6,8 +6,8 @@ exports[`CustomScrollbar renders correctly 1`] = ` style={ Object { "height": "auto", - "maxHeight": "inherit", - "minHeight": "inherit", + "maxHeight": "100%", + "minHeight": "0", "overflow": "hidden", "position": "relative", "width": "100%", @@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = ` "left": undefined, "marginBottom": 0, "marginRight": 0, - "maxHeight": "calc(inherit + 0px)", - "minHeight": "calc(inherit + 0px)", + "maxHeight": "calc(100% + 0px)", + "minHeight": "calc(0 + 0px)", "overflow": "scroll", "position": "relative", "right": undefined, 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/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 7ceebbd8047..a3b08516d16 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -14,7 +14,6 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { datasourceError: null, datasourceLoading: null, datasourceMissing: false, - datasourceName: '', exploreDatasources: [], graphInterval: 1000, history: [], @@ -69,7 +68,7 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - datasourceName: 'foo', + initialDatasource: 'foo', range: { from: 'now-5h', to: 'now', @@ -94,7 +93,7 @@ describe('state functions', () => { it('returns url parameter value for a state object', () => { const state = { ...DEFAULT_EXPLORE_STATE, - datasourceName: 'foo', + initialDatasource: 'foo', range: { from: 'now-5h', to: 'now', @@ -120,7 +119,7 @@ describe('state functions', () => { it('can parse the serialized state into the original state', () => { const state = { ...DEFAULT_EXPLORE_STATE, - datasourceName: 'foo', + initialDatasource: 'foo', range: { from: 'now - 5h', to: 'now', @@ -144,7 +143,7 @@ describe('state functions', () => { const resultState = { ...rest, datasource: DEFAULT_EXPLORE_STATE.datasource, - datasourceName: datasource, + initialDatasource: datasource, initialQueries: queries, }; diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index b7de48ac479..bea166075dc 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -105,7 +105,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { const urlState: ExploreUrlState = { - datasource: state.datasourceName, + datasource: state.initialDatasource, queries: state.initialQueries.map(clearQueryKeys), range: state.range, }; 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/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 569e0909c4d..120e002ac68 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -16,7 +16,7 @@ const BasicSettings: SFC = ({ dataSourceName, isDefault, onDefaultChange,