diff --git a/CHANGELOG.md b/CHANGELOG.md index bb0d2e026b9..c76ab12984c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: Config files for provisioning datasources as configuration have changed from `/conf/datasources` to `/conf/provisioning/datasources`. From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when installed with deb/rpm packages. +The pagerduty notifier now defaults to not auto resolve incidents. More details at [#10222](https://github.com/grafana/grafana/issues/10222) + ## New Features * **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson) * **Postgres/MySQL**: add __timeGroup macro for mysql [#9596](https://github.com/grafana/grafana/pull/9596), thanks [@svenklemm](https://github.com/svenklemm) @@ -55,6 +57,7 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when * **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand) * **Singlestat**: suppress error when result contains no datapoints [#9636](https://github.com/grafana/grafana/issues/9636), thx [@utkarshcmu](https://github.com/utkarshcmu) * **Postgres/MySQL**: Control quoting in SQL-queries when using template variables [#9030](https://github.com/grafana/grafana/issues/9030), thanks [@svenklemm](https://github.com/svenklemm) +* **Pagerduty**: Pagerduty dont auto resolve incidents by default anymore. [#10222](https://github.com/grafana/grafana/issues/10222) # 4.6.3 (2017-12-14) diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 35f90a9e4b7..c4067abec3b 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -42,7 +42,7 @@ var ( ) func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) { - autoResolve := model.Settings.Get("autoResolve").MustBool(true) + autoResolve := model.Settings.Get("autoResolve").MustBool(false) key := model.Settings.Get("integrationKey").MustString() if key == "" { return nil, alerting.ValidationError{Reason: "Could not find integration key property in settings"} diff --git a/pkg/services/alerting/notifiers/pagerduty_test.go b/pkg/services/alerting/notifiers/pagerduty_test.go index 522bb133d77..1d2eeec4a52 100644 --- a/pkg/services/alerting/notifiers/pagerduty_test.go +++ b/pkg/services/alerting/notifiers/pagerduty_test.go @@ -10,7 +10,6 @@ import ( func TestPagerdutyNotifier(t *testing.T) { Convey("Pagerduty notifier tests", t, func() { - Convey("Parsing alert notification from settings", func() { Convey("empty settings should return error", func() { json := `{ }` @@ -26,10 +25,31 @@ func TestPagerdutyNotifier(t *testing.T) { So(err, ShouldNotBeNil) }) + Convey("auto resolve should default to false", func() { + json := `{ "integrationKey": "abcdefgh0123456789" }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "pagerduty_testing", + Type: "pagerduty", + Settings: settingsJSON, + } + + not, err := NewPagerdutyNotifier(model) + pagerdutyNotifier := not.(*PagerdutyNotifier) + + So(err, ShouldBeNil) + So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing") + So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty") + So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789") + So(pagerdutyNotifier.AutoResolve, ShouldBeFalse) + }) + Convey("settings should trigger incident", func() { json := ` { - "integrationKey": "abcdefgh0123456789" + "integrationKey": "abcdefgh0123456789", + "autoResolve": false }` settingsJSON, _ := simplejson.NewJson([]byte(json)) @@ -46,8 +66,8 @@ func TestPagerdutyNotifier(t *testing.T) { So(pagerdutyNotifier.Name, ShouldEqual, "pagerduty_testing") So(pagerdutyNotifier.Type, ShouldEqual, "pagerduty") So(pagerdutyNotifier.Key, ShouldEqual, "abcdefgh0123456789") + So(pagerdutyNotifier.AutoResolve, ShouldBeFalse) }) - }) }) } diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index e798b92c6fe..1186fccbbf9 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -83,41 +83,48 @@ func (e *PrometheusExecutor) getClient(dsInfo *models.DataSource) (apiv1.API, er } func (e *PrometheusExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - result := &tsdb.Response{} + result := &tsdb.Response{ + Results: map[string]*tsdb.QueryResult{}, + } client, err := e.getClient(dsInfo) if err != nil { return nil, err } - query, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) + querys, err := parseQuery(dsInfo, tsdbQuery.Queries, tsdbQuery) if err != nil { return nil, err } - timeRange := apiv1.Range{ - Start: query.Start, - End: query.End, - Step: query.Step, + for _, query := range querys { + timeRange := apiv1.Range{ + Start: query.Start, + End: query.End, + Step: query.Step, + } + + plog.Debug("Sending query", "start", timeRange.Start, "end", timeRange.End, "step", timeRange.Step, "query", query.Expr) + + span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus") + span.SetTag("expr", query.Expr) + span.SetTag("start_unixnano", int64(query.Start.UnixNano())) + span.SetTag("stop_unixnano", int64(query.End.UnixNano())) + defer span.Finish() + + value, err := client.QueryRange(ctx, query.Expr, timeRange) + + if err != nil { + return nil, err + } + + queryResult, err := parseResponse(value, query) + if err != nil { + return nil, err + } + result.Results[query.RefId] = queryResult } - span, ctx := opentracing.StartSpanFromContext(ctx, "alerting.prometheus") - span.SetTag("expr", query.Expr) - span.SetTag("start_unixnano", int64(query.Start.UnixNano())) - span.SetTag("stop_unixnano", int64(query.End.UnixNano())) - defer span.Finish() - - value, err := client.QueryRange(ctx, query.Expr, timeRange) - - if err != nil { - return nil, err - } - - queryResult, err := parseResponse(value, query) - if err != nil { - return nil, err - } - result.Results = queryResult return result, nil } @@ -140,51 +147,54 @@ func formatLegend(metric model.Metric, query *PrometheusQuery) string { return string(result) } -func parseQuery(dsInfo *models.DataSource, queries []*tsdb.Query, queryContext *tsdb.TsdbQuery) (*PrometheusQuery, error) { - queryModel := queries[0] +func parseQuery(dsInfo *models.DataSource, queries []*tsdb.Query, queryContext *tsdb.TsdbQuery) ([]*PrometheusQuery, error) { + qs := []*PrometheusQuery{} + for _, queryModel := range queries { + expr, err := queryModel.Model.Get("expr").String() + if err != nil { + return nil, err + } - expr, err := queryModel.Model.Get("expr").String() - if err != nil { - return nil, err + format := queryModel.Model.Get("legendFormat").MustString("") + + start, err := queryContext.TimeRange.ParseFrom() + if err != nil { + return nil, err + } + + end, err := queryContext.TimeRange.ParseTo() + if err != nil { + return nil, err + } + + dsInterval, err := tsdb.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15) + if err != nil { + return nil, err + } + + intervalFactor := queryModel.Model.Get("intervalFactor").MustInt64(1) + interval := intervalCalculator.Calculate(queryContext.TimeRange, dsInterval) + step := time.Duration(int64(interval.Value) * intervalFactor) + + qs = append(qs, &PrometheusQuery{ + Expr: expr, + Step: step, + LegendFormat: format, + Start: start, + End: end, + RefId: queryModel.RefId, + }) } - format := queryModel.Model.Get("legendFormat").MustString("") - - start, err := queryContext.TimeRange.ParseFrom() - if err != nil { - return nil, err - } - - end, err := queryContext.TimeRange.ParseTo() - if err != nil { - return nil, err - } - - dsInterval, err := tsdb.GetIntervalFrom(dsInfo, queryModel.Model, time.Second*15) - if err != nil { - return nil, err - } - - intervalFactor := queryModel.Model.Get("intervalFactor").MustInt64(1) - interval := intervalCalculator.Calculate(queryContext.TimeRange, dsInterval) - step := time.Duration(int64(interval.Value) * intervalFactor) - - return &PrometheusQuery{ - Expr: expr, - Step: step, - LegendFormat: format, - Start: start, - End: end, - }, nil + return qs, nil } -func parseResponse(value model.Value, query *PrometheusQuery) (map[string]*tsdb.QueryResult, error) { - queryResults := make(map[string]*tsdb.QueryResult) +func parseResponse(value model.Value, query *PrometheusQuery) (*tsdb.QueryResult, error) { queryRes := tsdb.NewQueryResult() data, ok := value.(model.Matrix) if !ok { - return queryResults, fmt.Errorf("Unsupported result format: %s", value.Type().String()) + return queryRes, fmt.Errorf("Unsupported result format: %s", value.Type().String()) } for _, v := range data { @@ -204,6 +214,5 @@ func parseResponse(value model.Value, query *PrometheusQuery) (map[string]*tsdb. queryRes.Series = append(queryRes.Series, &series) } - queryResults["A"] = queryRes - return queryResults, nil + return queryRes, nil } diff --git a/pkg/tsdb/prometheus/prometheus_test.go b/pkg/tsdb/prometheus/prometheus_test.go index c551ab98112..efb42318214 100644 --- a/pkg/tsdb/prometheus/prometheus_test.go +++ b/pkg/tsdb/prometheus/prometheus_test.go @@ -60,9 +60,10 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("12h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) - + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Second*30) }) }) @@ -83,18 +84,22 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*2) }) Convey("with 1h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("1h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Second*15) }) }) @@ -116,9 +121,11 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*20) }) }) @@ -139,9 +146,11 @@ func TestPrometheus(t *testing.T) { Convey("with 48h time range", func() { queryContext.TimeRange = tsdb.NewTimeRange("48h", "now") - model, err := parseQuery(dsInfo, queryModels, queryContext) + models, err := parseQuery(dsInfo, queryModels, queryContext) So(err, ShouldBeNil) + + model := models[0] So(model.Step, ShouldEqual, time.Minute*2) }) }) diff --git a/pkg/tsdb/prometheus/types.go b/pkg/tsdb/prometheus/types.go index 8ed665d0123..cf8c16682e8 100644 --- a/pkg/tsdb/prometheus/types.go +++ b/pkg/tsdb/prometheus/types.go @@ -8,4 +8,5 @@ type PrometheusQuery struct { LegendFormat string Start time.Time End time.Time + RefId string } diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index a45a14f1ea2..2642c5e400a 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -6,3 +6,5 @@ export const REPEAT_DIR_VERTICAL = 'v'; export const DEFAULT_PANEL_SPAN = 4; export const DEFAULT_ROW_HEIGHT = 250; export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3; + +export const LS_PANEL_COPY_KEY = 'panel-copy'; diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index 08491acc682..d369fe8b3c0 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -6,11 +6,14 @@ export class JsonEditorCtrl { constructor($scope) { $scope.json = angular.toJson($scope.object, true); $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor; + $scope.canCopy = $scope.enableCopy; $scope.update = function() { var newObject = angular.fromJson($scope.json); $scope.updateHandler(newObject, $scope.object); }; + + $scope.getContentForClipboard = () => $scope.json; } } diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 0d913c32349..299de05f112 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import Clipboard from 'clipboard'; import coreModule from '../core_module'; import kbn from 'app/core/utils/kbn'; +import { appEvents } from 'app/core/core'; /** @ngInject */ function tip($compile) { @@ -32,6 +33,10 @@ function clipboardButton() { }, }); + scope.clipboard.on('success', () => { + appEvents.emit('alert-success', ['Content copied to clipboard']); + }); + scope.$on('$destroy', function() { if (scope.clipboard) { scope.clipboard.destroy(); diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index a3535c8fb35..fd79b7b1f03 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -1,18 +1,18 @@ import './dashboard_ctrl'; import './alerting_srv'; import './history/history'; -import './dashboardLoaderSrv'; +import './dashboard_loader_srv'; import './dashnav/dashnav'; import './submenu/submenu'; import './save_as_modal'; import './save_modal'; import './shareModalCtrl'; -import './shareSnapshotCtrl'; +import './share_snapshot_ctrl'; import './dashboard_srv'; import './view_state_srv'; import './validation_srv'; import './time_srv'; -import './unsavedChangesSrv'; +import './unsaved_changes_srv'; import './unsaved_changes_modal'; import './timepicker/timepicker'; import './upload'; diff --git a/public/app/features/dashboard/dashboardLoaderSrv.js b/public/app/features/dashboard/dashboardLoaderSrv.js deleted file mode 100644 index d5e257de665..00000000000 --- a/public/app/features/dashboard/dashboardLoaderSrv.js +++ /dev/null @@ -1,109 +0,0 @@ -define([ - 'angular', - 'moment', - 'lodash', - 'jquery', - 'app/core/utils/kbn', - 'app/core/utils/datemath', - 'app/core/services/impression_srv' -], -function (angular, moment, _, $, kbn, dateMath, impressionSrv) { - 'use strict'; - - kbn = kbn.default; - impressionSrv = impressionSrv.default; - - var module = angular.module('grafana.services'); - - module.service('dashboardLoaderSrv', function(backendSrv, - dashboardSrv, - datasourceSrv, - $http, $q, $timeout, - contextSrv, $routeParams, - $rootScope) { - var self = this; - - this._dashboardLoadFailed = function(title, snapshot) { - snapshot = snapshot || false; - return { - meta: { canStar: false, isSnapshot: snapshot, canDelete: false, canSave: false, canEdit: false, dashboardNotFound: true }, - dashboard: {title: title } - }; - }; - - this.loadDashboard = function(type, slug) { - var promise; - - if (type === 'script') { - promise = this._loadScriptedDashboard(slug); - } else if (type === 'snapshot') { - promise = backendSrv.get('/api/snapshots/' + $routeParams.slug) - .catch(function() { - return self._dashboardLoadFailed("Snapshot not found", true); - }); - } else { - promise = backendSrv.getDashboard($routeParams.type, $routeParams.slug) - .then(function(result) { - if (result.meta.isFolder) { - $rootScope.appEvent("alert-error", ['Dashboard not found']); - throw new Error("Dashboard not found"); - } - return result; - }) - .catch(function() { - return self._dashboardLoadFailed("Not found"); - }); - } - - promise.then(function(result) { - - if (result.meta.dashboardNotFound !== true) { - impressionSrv.addDashboardImpression(result.dashboard.id); - } - - return result; - }); - - return promise; - }; - - this._loadScriptedDashboard = function(file) { - var url = 'public/dashboards/'+file.replace(/\.(?!js)/,"/") + '?' + new Date().getTime(); - - return $http({ url: url, method: "GET" }) - .then(this._executeScript).then(function(result) { - return { meta: { fromScript: true, canDelete: false, canSave: false, canStar: false}, dashboard: result.data }; - }, function(err) { - console.log('Script dashboard error '+ err); - $rootScope.appEvent('alert-error', ["Script Error", "Please make sure it exists and returns a valid dashboard"]); - return self._dashboardLoadFailed('Scripted dashboard'); - }); - }; - - this._executeScript = function(result) { - var services = { - dashboardSrv: dashboardSrv, - datasourceSrv: datasourceSrv, - $q: $q, - }; - - /*jshint -W054 */ - var script_func = new Function('ARGS','kbn','dateMath','_','moment','window','document','$','jQuery', 'services', result.data); - var script_result = script_func($routeParams, kbn, dateMath, _ , moment, window, document, $, $, services); - - // Handle async dashboard scripts - if (_.isFunction(script_result)) { - var deferred = $q.defer(); - script_result(function(dashboard) { - $timeout(function() { - deferred.resolve({ data: dashboard }); - }); - }); - return deferred.promise; - } - - return { data: script_result }; - }; - - }); -}); diff --git a/public/app/features/dashboard/dashboard_loader_srv.ts b/public/app/features/dashboard/dashboard_loader_srv.ts new file mode 100644 index 00000000000..d429d7f0a7d --- /dev/null +++ b/public/app/features/dashboard/dashboard_loader_srv.ts @@ -0,0 +1,139 @@ +import angular from 'angular'; +import moment from 'moment'; +import _ from 'lodash'; +import $ from 'jquery'; +import kbn from 'app/core/utils/kbn'; +import * as dateMath from 'app/core/utils/datemath'; +import impressionSrv from 'app/core/services/impression_srv'; + +export class DashboardLoaderSrv { + /** @ngInject */ + constructor( + private backendSrv, + private dashboardSrv, + private datasourceSrv, + private $http, + private $q, + private $timeout, + contextSrv, + private $routeParams, + private $rootScope + ) {} + + _dashboardLoadFailed(title, snapshot) { + snapshot = snapshot || false; + return { + meta: { + canStar: false, + isSnapshot: snapshot, + canDelete: false, + canSave: false, + canEdit: false, + dashboardNotFound: true, + }, + dashboard: { title: title }, + }; + } + + loadDashboard(type, slug) { + var promise; + + if (type === 'script') { + promise = this._loadScriptedDashboard(slug); + } else if (type === 'snapshot') { + promise = this.backendSrv.get('/api/snapshots/' + this.$routeParams.slug).catch(() => { + return this._dashboardLoadFailed('Snapshot not found', true); + }); + } else { + promise = this.backendSrv + .getDashboard(this.$routeParams.type, this.$routeParams.slug) + .then(result => { + if (result.meta.isFolder) { + this.$rootScope.appEvent('alert-error', ['Dashboard not found']); + throw new Error('Dashboard not found'); + } + return result; + }) + .catch(() => { + return this._dashboardLoadFailed('Not found', true); + }); + } + + promise.then(function(result) { + if (result.meta.dashboardNotFound !== true) { + impressionSrv.addDashboardImpression(result.dashboard.id); + } + + return result; + }); + + return promise; + } + + _loadScriptedDashboard(file) { + var url = 'public/dashboards/' + file.replace(/\.(?!js)/, '/') + '?' + new Date().getTime(); + + return this.$http({ url: url, method: 'GET' }) + .then(this._executeScript) + .then( + function(result) { + return { + meta: { + fromScript: true, + canDelete: false, + canSave: false, + canStar: false, + }, + dashboard: result.data, + }; + }, + function(err) { + console.log('Script dashboard error ' + err); + this.$rootScope.appEvent('alert-error', [ + 'Script Error', + 'Please make sure it exists and returns a valid dashboard', + ]); + return this._dashboardLoadFailed('Scripted dashboard'); + } + ); + } + + _executeScript(result) { + var services = { + dashboardSrv: this.dashboardSrv, + datasourceSrv: this.datasourceSrv, + $q: this.$q, + }; + + /*jshint -W054 */ + var script_func = new Function( + 'ARGS', + 'kbn', + 'dateMath', + '_', + 'moment', + 'window', + 'document', + '$', + 'jQuery', + 'services', + result.data + ); + var script_result = script_func(this.$routeParams, kbn, dateMath, _, moment, window, document, $, $, services); + + // Handle async dashboard scripts + if (_.isFunction(script_result)) { + var deferred = this.$q.defer(); + script_result(dashboard => { + this.$timeout(() => { + deferred.resolve({ data: dashboard }); + }); + }); + return deferred.promise; + } + + return { data: script_result }; + } +} + +angular.module('grafana.services').service('dashboardLoaderSrv', DashboardLoaderSrv); diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 75c8cbf36bc..1f143f3d7f7 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -2,9 +2,11 @@ import React from 'react'; import _ from 'lodash'; import config from 'app/core/config'; -import {PanelModel} from '../panel_model'; -import {PanelContainer} from './PanelContainer'; +import { PanelModel } from '../panel_model'; +import { PanelContainer } from './PanelContainer'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; +import store from 'app/core/store'; +import { LS_PANEL_COPY_KEY } from 'app/core/constants'; export interface AddPanelPanelProps { panel: PanelModel; @@ -24,46 +26,67 @@ export class AddPanelPanel extends React.Component item) .value(); // add special row type - panels.push({id: 'row', name: 'Row', sort: 8, info: {logos: {small: 'public/img/icn-row.svg'}}}); + panels.push({ id: 'row', name: 'Row', sort: 8, info: { logos: { small: 'public/img/icn-row.svg' } } }); + + let copiedPanelJson = store.get(LS_PANEL_COPY_KEY); + if (copiedPanelJson) { + let copiedPanel = JSON.parse(copiedPanelJson); + let pluginInfo = _.find(panels, { id: copiedPanel.type }); + if (pluginInfo) { + let pluginCopy = _.cloneDeep(pluginInfo); + pluginCopy.name = copiedPanel.title; + pluginCopy.sort = -1; + pluginCopy.defaults = copiedPanel; + panels.push(pluginCopy); + } + } // add sort by sort property return _.sortBy(panels, 'sort'); } - onPanelSelected(panelPluginInfo) { + onAddPanel = panelPluginInfo => { const panelContainer = this.props.getPanelContainer(); const dashboard = panelContainer.getDashboard(); - const {gridPos} = this.props.panel; + const { gridPos } = this.props.panel; var newPanel: any = { type: panelPluginInfo.id, title: 'Panel Title', - gridPos: {x: gridPos.x, y: gridPos.y, w: gridPos.w, h: gridPos.h} + gridPos: { x: gridPos.x, y: gridPos.y, w: gridPos.w, h: gridPos.h }, }; if (panelPluginInfo.id === 'row') { newPanel.title = 'Row title'; - newPanel.gridPos = {x: 0, y: 0}; + newPanel.gridPos = { x: 0, y: 0 }; + } + + // apply panel template / defaults + if (panelPluginInfo.defaults) { + _.defaults(newPanel, panelPluginInfo.defaults); + newPanel.gridPos.w = panelPluginInfo.defaults.gridPos.w; + newPanel.gridPos.h = panelPluginInfo.defaults.gridPos.h; + newPanel.title = panelPluginInfo.defaults.title; + store.delete(LS_PANEL_COPY_KEY); } dashboard.addPanel(newPanel); dashboard.removePanel(this.props.panel); - } + }; - renderPanelItem(panel) { + renderPanelItem(panel, index) { + console.log('render panel', index); return ( -
this.onPanelSelected(panel)} title={panel.name}> +
this.onAddPanel(panel)} title={panel.name}>
{panel.name}
@@ -75,7 +98,7 @@ export class AddPanelPanel extends React.Component
- + New Panel Select a visualization
@@ -87,4 +110,3 @@ export class AddPanelPanel extends React.ComponentView JSON
- +
diff --git a/public/app/features/dashboard/shareSnapshotCtrl.js b/public/app/features/dashboard/share_snapshot_ctrl.ts similarity index 64% rename from public/app/features/dashboard/shareSnapshotCtrl.js rename to public/app/features/dashboard/share_snapshot_ctrl.ts index 617e54db59c..4c549cc052b 100644 --- a/public/app/features/dashboard/shareSnapshotCtrl.js +++ b/public/app/features/dashboard/share_snapshot_ctrl.ts @@ -1,14 +1,8 @@ -define([ - 'angular', - 'lodash', -], -function (angular, _) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - - module.controller('ShareSnapshotCtrl', function($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) { +import angular from 'angular'; +import _ from 'lodash'; +export class ShareSnapshotCtrl { + constructor($scope, $rootScope, $location, backendSrv, $timeout, timeSrv) { $scope.snapshot = { name: $scope.dashboard.title, expires: 0, @@ -18,16 +12,16 @@ function (angular, _) { $scope.step = 1; $scope.expireOptions = [ - {text: '1 Hour', value: 60*60}, - {text: '1 Day', value: 60*60*24}, - {text: '7 Days', value: 60*60*24*7}, - {text: 'Never', value: 0}, + { text: '1 Hour', value: 60 * 60 }, + { text: '1 Day', value: 60 * 60 * 24 }, + { text: '7 Days', value: 60 * 60 * 24 * 7 }, + { text: 'Never', value: 0 }, ]; $scope.accessOptions = [ - {text: 'Anyone with the link', value: 1}, - {text: 'Organization users', value: 2}, - {text: 'Public on the web', value: 3}, + { text: 'Anyone with the link', value: 1 }, + { text: 'Organization users', value: 2 }, + { text: 'Public on the web', value: 3 }, ]; $scope.init = function() { @@ -42,7 +36,7 @@ function (angular, _) { $scope.createSnapshot = function(external) { $scope.dashboard.snapshot = { - timestamp: new Date() + timestamp: new Date(), }; if (!external) { @@ -71,29 +65,32 @@ function (angular, _) { var postUrl = external ? $scope.externalUrl + $scope.apiUrl : $scope.apiUrl; - backendSrv.post(postUrl, cmdData).then(function(results) { - $scope.loading = false; + backendSrv.post(postUrl, cmdData).then( + function(results) { + $scope.loading = false; - if (external) { - $scope.deleteUrl = results.deleteUrl; - $scope.snapshotUrl = results.url; - $scope.saveExternalSnapshotRef(cmdData, results); - } else { - var url = $location.url(); - var baseUrl = $location.absUrl(); + if (external) { + $scope.deleteUrl = results.deleteUrl; + $scope.snapshotUrl = results.url; + $scope.saveExternalSnapshotRef(cmdData, results); + } else { + var url = $location.url(); + var baseUrl = $location.absUrl(); - if (url !== '/') { - baseUrl = baseUrl.replace(url, '') + '/'; + if (url !== '/') { + baseUrl = baseUrl.replace(url, '') + '/'; + } + + $scope.snapshotUrl = baseUrl + 'dashboard/snapshot/' + results.key; + $scope.deleteUrl = baseUrl + 'api/snapshots-delete/' + results.deleteKey; } - $scope.snapshotUrl = baseUrl + 'dashboard/snapshot/' + results.key; - $scope.deleteUrl = baseUrl + 'api/snapshots-delete/' + results.deleteKey; + $scope.step = 2; + }, + function() { + $scope.loading = false; } - - $scope.step = 2; - }, function() { - $scope.loading = false; - }); + ); }; $scope.getSnapshotUrl = function() { @@ -116,21 +113,22 @@ function (angular, _) { // remove annotation queries dash.annotations.list = _.chain(dash.annotations.list) - .filter(function(annotation) { - return annotation.enable; - }) - .map(function(annotation) { - return { - name: annotation.name, - enable: annotation.enable, - iconColor: annotation.iconColor, - snapshotData: annotation.snapshotData - }; - }).value(); + .filter(function(annotation) { + return annotation.enable; + }) + .map(function(annotation) { + return { + name: annotation.name, + enable: annotation.enable, + iconColor: annotation.iconColor, + snapshotData: annotation.snapshotData, + }; + }) + .value(); // remove template queries _.each(dash.templating.list, function(variable) { - variable.query = ""; + variable.query = ''; variable.options = variable.current; variable.refresh = false; }); @@ -168,7 +166,7 @@ function (angular, _) { cmdData.deleteKey = results.deleteKey; backendSrv.post('/api/snapshots/', cmdData); }; + } +} - }); - -}); +angular.module('grafana.controllers').controller('ShareSnapshotCtrl', ShareSnapshotCtrl); diff --git a/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts b/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts index 4737bf8fbbd..4d720945553 100644 --- a/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts +++ b/public/app/features/dashboard/specs/unsaved_changes_srv_specs.ts @@ -1,12 +1,15 @@ import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common'; -import 'app/features/dashboard/unsavedChangesSrv'; +import { Tracker } from 'app/features/dashboard/unsaved_changes_srv'; import 'app/features/dashboard/dashboard_srv'; +import { contextSrv } from 'app/core/core'; describe('unsavedChangesSrv', function() { - var _unsavedChangesSrv; var _dashboardSrv; var _contextSrvStub = { isEditor: true }; var _rootScope; + var _location; + var _timeout; + var _window; var tracker; var dash; var scope; @@ -21,10 +24,12 @@ describe('unsavedChangesSrv', function() { ); beforeEach( - angularMocks.inject(function(unsavedChangesSrv, $location, $rootScope, dashboardSrv) { - _unsavedChangesSrv = unsavedChangesSrv; + angularMocks.inject(function($location, $rootScope, dashboardSrv, $timeout, $window) { _dashboardSrv = dashboardSrv; _rootScope = $rootScope; + _location = $location; + _timeout = $timeout; + _window = $window; }) ); @@ -42,7 +47,7 @@ describe('unsavedChangesSrv', function() { scope.appEvent = sinon.spy(); scope.onAppEvent = sinon.spy(); - tracker = new _unsavedChangesSrv.Tracker(dash, scope); + tracker = new Tracker(dash, scope, undefined, _location, _window, _timeout, contextSrv, _rootScope); }); it('No changes should not have changes', function() { diff --git a/public/app/features/dashboard/unsavedChangesSrv.js b/public/app/features/dashboard/unsavedChangesSrv.js deleted file mode 100644 index 7ffdb36952e..00000000000 --- a/public/app/features/dashboard/unsavedChangesSrv.js +++ /dev/null @@ -1,189 +0,0 @@ -define([ - 'angular', - 'lodash', -], -function(angular, _) { - 'use strict'; - - var module = angular.module('grafana.services'); - - module.service('unsavedChangesSrv', function($rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) { - - function Tracker(dashboard, scope, originalCopyDelay) { - var self = this; - - this.current = dashboard; - this.originalPath = $location.path(); - this.scope = scope; - - // register events - scope.onAppEvent('dashboard-saved', function() { - this.original = this.current.getSaveModelClone(); - this.originalPath = $location.path(); - }.bind(this)); - - $window.onbeforeunload = function() { - if (self.ignoreChanges()) { return; } - if (self.hasChanges()) { - return "There are unsaved changes to this dashboard"; - } - }; - - scope.$on("$locationChangeStart", function(event, next) { - // check if we should look for changes - if (self.originalPath === $location.path()) { return true; } - if (self.ignoreChanges()) { return true; } - - if (self.hasChanges()) { - event.preventDefault(); - self.next = next; - - $timeout(function() { - self.open_modal(); - }); - } - }); - - if (originalCopyDelay) { - $timeout(function() { - // wait for different services to patch the dashboard (missing properties) - self.original = dashboard.getSaveModelClone(); - }, originalCopyDelay); - } else { - self.original = dashboard.getSaveModelClone(); - } - } - - var p = Tracker.prototype; - - // for some dashboards and users - // changes should be ignored - p.ignoreChanges = function() { - if (!this.original) { return true; } - if (!contextSrv.isEditor) { return true; } - if (!this.current || !this.current.meta) { return true; } - - var meta = this.current.meta; - return !meta.canSave || meta.fromScript || meta.fromFile; - }; - - // remove stuff that should not count in diff - p.cleanDashboardFromIgnoredChanges = function(dash) { - // ignore time and refresh - dash.time = 0; - dash.refresh = 0; - dash.schemaVersion = 0; - - // filter row and panels properties that should be ignored - dash.rows = _.filter(dash.rows, function(row) { - if (row.repeatRowId) { - return false; - } - - row.panels = _.filter(row.panels, function(panel) { - if (panel.repeatPanelId) { - return false; - } - - // remove scopedVars - panel.scopedVars = null; - - // ignore span changes - panel.span = null; - - // ignore panel legend sort - if (panel.legend) { - delete panel.legend.sort; - delete panel.legend.sortDesc; - } - - return true; - }); - - // ignore collapse state - row.collapse = false; - return true; - }); - - dash.panels = _.filter(dash.panels, function(panel) { - if (panel.repeatPanelId) { - return false; - } - - // remove scopedVars - panel.scopedVars = null; - - // ignore panel legend sort - if (panel.legend) { - delete panel.legend.sort; - delete panel.legend.sortDesc; - } - - return true; - }); - - // ignore template variable values - _.each(dash.templating.list, function(value) { - value.current = null; - value.options = null; - value.filters = null; - }); - }; - - p.hasChanges = function() { - var current = this.current.getSaveModelClone(); - var original = this.original; - - this.cleanDashboardFromIgnoredChanges(current); - this.cleanDashboardFromIgnoredChanges(original); - - var currentTimepicker = _.find(current.nav, { type: 'timepicker' }); - var originalTimepicker = _.find(original.nav, { type: 'timepicker' }); - - if (currentTimepicker && originalTimepicker) { - currentTimepicker.now = originalTimepicker.now; - } - - var currentJson = angular.toJson(current); - var originalJson = angular.toJson(original); - - return currentJson !== originalJson; - }; - - p.discardChanges = function() { - this.original = null; - this.gotoNext(); - }; - - p.open_modal = function() { - $rootScope.appEvent('show-modal', { - templateHtml: '', - modalClass: 'modal--narrow confirm-modal' - }); - }; - - p.saveChanges = function() { - var self = this; - var cancel = $rootScope.$on('dashboard-saved', function() { - cancel(); - $timeout(function() { - self.gotoNext(); - }); - }); - - $rootScope.appEvent('save-dashboard'); - }; - - p.gotoNext = function() { - var baseLen = $location.absUrl().length - $location.url().length; - var nextUrl = this.next.substring(baseLen); - $location.url(nextUrl); - }; - - this.Tracker = Tracker; - this.init = function(dashboard, scope) { - this.tracker = new Tracker(dashboard, scope, 1000); - return this.tracker; - }; - }); -}); diff --git a/public/app/features/dashboard/unsaved_changes_srv.ts b/public/app/features/dashboard/unsaved_changes_srv.ts new file mode 100644 index 00000000000..adf808b9bf0 --- /dev/null +++ b/public/app/features/dashboard/unsaved_changes_srv.ts @@ -0,0 +1,216 @@ +import angular from 'angular'; +import _ from 'lodash'; + +export class Tracker { + current: any; + originalPath: any; + scope: any; + original: any; + next: any; + $window: any; + + /** @ngInject */ + constructor( + dashboard, + scope, + originalCopyDelay, + private $location, + $window, + private $timeout, + private contextSrv, + private $rootScope + ) { + this.$location = $location; + this.$window = $window; + + this.current = dashboard; + this.originalPath = $location.path(); + this.scope = scope; + + // register events + scope.onAppEvent('dashboard-saved', () => { + this.original = this.current.getSaveModelClone(); + this.originalPath = $location.path(); + }); + + $window.onbeforeunload = () => { + if (this.ignoreChanges()) { + return ''; + } + if (this.hasChanges()) { + return 'There are unsaved changes to this dashboard'; + } + return ''; + }; + + scope.$on('$locationChangeStart', (event, next) => { + // check if we should look for changes + if (this.originalPath === $location.path()) { + return true; + } + if (this.ignoreChanges()) { + return true; + } + + if (this.hasChanges()) { + event.preventDefault(); + this.next = next; + + this.$timeout(() => { + this.open_modal(); + }); + } + return false; + }); + + if (originalCopyDelay) { + this.$timeout(() => { + // wait for different services to patch the dashboard (missing properties) + this.original = dashboard.getSaveModelClone(); + }, originalCopyDelay); + } else { + this.original = dashboard.getSaveModelClone(); + } + } + + // for some dashboards and users + // changes should be ignored + ignoreChanges() { + if (!this.original) { + return true; + } + if (!this.contextSrv.isEditor) { + return true; + } + if (!this.current || !this.current.meta) { + return true; + } + + var meta = this.current.meta; + return !meta.canSave || meta.fromScript || meta.fromFile; + } + + // remove stuff that should not count in diff + cleanDashboardFromIgnoredChanges(dash) { + // ignore time and refresh + dash.time = 0; + dash.refresh = 0; + dash.schemaVersion = 0; + + // filter row and panels properties that should be ignored + dash.rows = _.filter(dash.rows, function(row) { + if (row.repeatRowId) { + return false; + } + + row.panels = _.filter(row.panels, function(panel) { + if (panel.repeatPanelId) { + return false; + } + + // remove scopedVars + panel.scopedVars = null; + + // ignore span changes + panel.span = null; + + // ignore panel legend sort + if (panel.legend) { + delete panel.legend.sort; + delete panel.legend.sortDesc; + } + + return true; + }); + + // ignore collapse state + row.collapse = false; + return true; + }); + + dash.panels = _.filter(dash.panels, panel => { + if (panel.repeatPanelId) { + return false; + } + + // remove scopedVars + panel.scopedVars = null; + + // ignore panel legend sort + if (panel.legend) { + delete panel.legend.sort; + delete panel.legend.sortDesc; + } + + return true; + }); + + // ignore template variable values + _.each(dash.templating.list, function(value) { + value.current = null; + value.options = null; + value.filters = null; + }); + } + + hasChanges() { + var current = this.current.getSaveModelClone(); + var original = this.original; + + this.cleanDashboardFromIgnoredChanges(current); + this.cleanDashboardFromIgnoredChanges(original); + + var currentTimepicker = _.find(current.nav, { type: 'timepicker' }); + var originalTimepicker = _.find(original.nav, { type: 'timepicker' }); + + if (currentTimepicker && originalTimepicker) { + currentTimepicker.now = originalTimepicker.now; + } + + var currentJson = angular.toJson(current); + var originalJson = angular.toJson(original); + + return currentJson !== originalJson; + } + + discardChanges() { + this.original = null; + this.gotoNext(); + } + + open_modal() { + this.$rootScope.appEvent('show-modal', { + templateHtml: '', + modalClass: 'modal--narrow confirm-modal', + }); + } + + saveChanges() { + var self = this; + var cancel = this.$rootScope.$on('dashboard-saved', () => { + cancel(); + this.$timeout(() => { + self.gotoNext(); + }); + }); + + this.$rootScope.appEvent('save-dashboard'); + } + + gotoNext() { + var baseLen = this.$location.absUrl().length - this.$location.url().length; + var nextUrl = this.next.substring(baseLen); + this.$location.url(nextUrl); + } +} + +/** @ngInject */ +export function unsavedChangesSrv($rootScope, $q, $location, $timeout, contextSrv, dashboardSrv, $window) { + this.Tracker = Tracker; + this.init = function(dashboard, scope) { + this.tracker = new Tracker(dashboard, scope, 1000, $location, $window, $timeout, contextSrv, $rootScope); + return this.tracker; + }; +} + +angular.module('grafana.services').service('unsavedChangesSrv', unsavedChangesSrv); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index bba83486cdc..d8757f49be6 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -4,7 +4,8 @@ import $ from 'jquery'; import { appEvents, profiler } from 'app/core/core'; import { PanelModel } from 'app/features/dashboard/panel_model'; import Remarkable from 'remarkable'; -import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; +import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, LS_PANEL_COPY_KEY } from 'app/core/constants'; +import store from 'app/core/store'; const TITLE_HEIGHT = 27; const PANEL_BORDER = 2; @@ -190,11 +191,19 @@ export class PanelCtrl { click: 'ctrl.duplicate()', role: 'Editor', }); + + menu.push({ + text: 'Add to Panel List', + click: 'ctrl.addToPanelList()', + role: 'Editor', + }); } + menu.push({ text: 'Panel JSON', click: 'ctrl.editPanelJson(); dismiss();', }); + this.events.emit('init-panel-actions', menu); return menu; } @@ -263,6 +272,7 @@ export class PanelCtrl { let editScope = this.$scope.$root.$new(); editScope.object = this.panel.getSaveModel(); editScope.updateHandler = this.replacePanel.bind(this); + editScope.enableCopy = true; this.publishAppEvent('show-modal', { src: 'public/app/partials/edit_json.html', @@ -270,6 +280,11 @@ export class PanelCtrl { }); } + addToPanelList() { + store.set(LS_PANEL_COPY_KEY, JSON.stringify(this.panel.getSaveModel())); + appEvents.emit('alert-success', ['Panel temporarily added to panel list']); + } + replacePanel(newPanel, oldPanel) { let dashboard = this.dashboard; let index = _.findIndex(dashboard.panels, panel => { diff --git a/public/app/features/plugins/datasource_srv.js b/public/app/features/plugins/datasource_srv.js deleted file mode 100644 index bdd9a921c3d..00000000000 --- a/public/app/features/plugins/datasource_srv.js +++ /dev/null @@ -1,153 +0,0 @@ -define([ - 'angular', - 'lodash', - 'app/core/core_module', - 'app/core/config', - './plugin_loader', -], -function (angular, _, coreModule, config, pluginLoader) { - 'use strict'; - - config = config.default; - - coreModule.default.service('datasourceSrv', function($q, $injector, $rootScope, templateSrv) { - var self = this; - - this.init = function() { - this.datasources = {}; - }; - - this.get = function(name) { - if (!name) { - return this.get(config.defaultDatasource); - } - - name = templateSrv.replace(name); - - if (name === 'default') { - return this.get(config.defaultDatasource); - } - - if (this.datasources[name]) { - return $q.when(this.datasources[name]); - } - - return this.loadDatasource(name); - }; - - this.loadDatasource = function(name) { - var dsConfig = config.datasources[name]; - if (!dsConfig) { - return $q.reject({message: "Datasource named " + name + " was not found"}); - } - - var deferred = $q.defer(); - var pluginDef = dsConfig.meta; - - pluginLoader.importPluginModule(pluginDef.module).then(function(plugin) { - // check if its in cache now - if (self.datasources[name]) { - deferred.resolve(self.datasources[name]); - return; - } - - // plugin module needs to export a constructor function named Datasource - if (!plugin.Datasource) { - throw "Plugin module is missing Datasource constructor"; - } - - var instance = $injector.instantiate(plugin.Datasource, {instanceSettings: dsConfig}); - instance.meta = pluginDef; - instance.name = name; - self.datasources[name] = instance; - deferred.resolve(instance); - }).catch(function(err) { - $rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]); - }); - - return deferred.promise; - }; - - this.getAll = function() { - return config.datasources; - }; - - this.getAnnotationSources = function() { - var sources = []; - - this.addDataSourceVariables(sources); - - _.each(config.datasources, function(value) { - if (value.meta && value.meta.annotations) { - sources.push(value); - } - }); - - return sources; - }; - - this.getMetricSources = function(options) { - var metricSources = []; - - _.each(config.datasources, function(value, key) { - if (value.meta && value.meta.metrics) { - metricSources.push({value: key, name: key, meta: value.meta}); - - if (key === config.defaultDatasource) { - metricSources.push({value: null, name: 'default', meta: value.meta}); - } - } - }); - - if (!options || !options.skipVariables) { - this.addDataSourceVariables(metricSources); - } - - metricSources.sort(function(a, b) { - // these two should always be at the bottom - if (a.meta.id === "mixed" || a.meta.id === "grafana") { - return 1; - } - if (b.meta.id === "mixed" || b.meta.id === "grafana") { - return -1; - } - if (a.name.toLowerCase() > b.name.toLowerCase()) { - return 1; - } - if (a.name.toLowerCase() < b.name.toLowerCase()) { - return -1; - } - return 0; - }); - - return metricSources; - }; - - this.addDataSourceVariables = function(list) { - // look for data source variables - for (var i = 0; i < templateSrv.variables.length; i++) { - var variable = templateSrv.variables[i]; - if (variable.type !== 'datasource') { - continue; - } - - var first = variable.current.value; - if (first === 'default') { - first = config.defaultDatasource; - } - - var ds = config.datasources[first]; - - if (ds) { - list.push({ - name: '$' + variable.name, - value: '$' + variable.name, - meta: ds.meta, - }); - } - } - }; - - this.init(); - }); -}); diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts new file mode 100644 index 00000000000..fb7a9ece37a --- /dev/null +++ b/public/app/features/plugins/datasource_srv.ts @@ -0,0 +1,152 @@ +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; +import { importPluginModule } from './plugin_loader'; + +export class DatasourceSrv { + datasources: any; + + /** @ngInject */ + constructor(private $q, private $injector, $rootScope, private templateSrv) { + this.init(); + } + + init() { + this.datasources = {}; + } + + get(name) { + if (!name) { + return this.get(config.defaultDatasource); + } + + name = this.templateSrv.replace(name); + + if (name === 'default') { + return this.get(config.defaultDatasource); + } + + if (this.datasources[name]) { + return this.$q.when(this.datasources[name]); + } + + return this.loadDatasource(name); + } + + loadDatasource(name) { + var dsConfig = config.datasources[name]; + if (!dsConfig) { + return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' }); + } + + var deferred = this.$q.defer(); + var pluginDef = dsConfig.meta; + + importPluginModule(pluginDef.module) + .then(plugin => { + // check if its in cache now + if (this.datasources[name]) { + deferred.resolve(this.datasources[name]); + return; + } + + // plugin module needs to export a constructor function named Datasource + if (!plugin.Datasource) { + throw new Error('Plugin module is missing Datasource constructor'); + } + + var instance = this.$injector.instantiate(plugin.Datasource, { instanceSettings: dsConfig }); + instance.meta = pluginDef; + instance.name = name; + this.datasources[name] = instance; + deferred.resolve(instance); + }) + .catch(function(err) { + this.$rootScope.appEvent('alert-error', [dsConfig.name + ' plugin failed', err.toString()]); + }); + + return deferred.promise; + } + + getAll() { + return config.datasources; + } + + getAnnotationSources() { + var sources = []; + + this.addDataSourceVariables(sources); + + _.each(config.datasources, function(value) { + if (value.meta && value.meta.annotations) { + sources.push(value); + } + }); + + return sources; + } + + getMetricSources(options) { + var metricSources = []; + + _.each(config.datasources, function(value, key) { + if (value.meta && value.meta.metrics) { + metricSources.push({ value: key, name: key, meta: value.meta }); + + if (key === config.defaultDatasource) { + metricSources.push({ value: null, name: 'default', meta: value.meta }); + } + } + }); + + if (!options || !options.skipVariables) { + this.addDataSourceVariables(metricSources); + } + + metricSources.sort(function(a, b) { + // these two should always be at the bottom + if (a.meta.id === 'mixed' || a.meta.id === 'grafana') { + return 1; + } + if (b.meta.id === 'mixed' || b.meta.id === 'grafana') { + return -1; + } + if (a.name.toLowerCase() > b.name.toLowerCase()) { + return 1; + } + if (a.name.toLowerCase() < b.name.toLowerCase()) { + return -1; + } + return 0; + }); + + return metricSources; + } + + addDataSourceVariables(list) { + // look for data source variables + for (var i = 0; i < this.templateSrv.variables.length; i++) { + var variable = this.templateSrv.variables[i]; + if (variable.type !== 'datasource') { + continue; + } + + var first = variable.current.value; + if (first === 'default') { + first = config.defaultDatasource; + } + + var ds = config.datasources[first]; + + if (ds) { + list.push({ + name: '$' + variable.name, + value: '$' + variable.name, + meta: ds.meta, + }); + } + } + } +} + +coreModule.service('datasourceSrv', DatasourceSrv); diff --git a/public/app/partials/edit_json.html b/public/app/partials/edit_json.html index b87bb0ce261..f3237193954 100644 --- a/public/app/partials/edit_json.html +++ b/public/app/partials/edit_json.html @@ -16,6 +16,9 @@
+
diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index 9c834b7157d..b27f1cd50af 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -12,9 +12,9 @@ export default class PrometheusMetricFindQuery { } process() { - var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)$/; - var metric_names_regex = /^metrics\((.+)\)$/; - var query_result_regex = /^query_result\((.+)\)$/; + var label_values_regex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)\s*$/; + var metric_names_regex = /^metrics\((.+)\)\s*$/; + var query_result_regex = /^query_result\((.+)\)\s*$/; var label_values_query = this.query.match(label_values_regex); if (label_values_query) { diff --git a/public/sass/base/font-awesome/_larger.scss b/public/sass/base/font-awesome/_larger.scss index 1efeef30392..5d7bebcda14 100644 --- a/public/sass/base/font-awesome/_larger.scss +++ b/public/sass/base/font-awesome/_larger.scss @@ -8,7 +8,7 @@ vertical-align: -15%; } .#{$fa-css-prefix}-2x { - font-size: 2em; + font-size: 2em !important; } .#{$fa-css-prefix}-3x { font-size: 3em; diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index a6b5aebd107..548d677ef47 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -65,3 +65,7 @@ .add-panel__item-img { height: calc(100% - 15px); } + +.add-panel__item-icon { + padding: 2px; +} diff --git a/public/sass/components/_panel_singlestat.scss b/public/sass/components/_panel_singlestat.scss index 0510c46cd47..33a956a0244 100644 --- a/public/sass/components/_panel_singlestat.scss +++ b/public/sass/components/_panel_singlestat.scss @@ -6,6 +6,7 @@ } .singlestat-panel-value-container { + line-height: 1; display: table-cell; vertical-align: middle; text-align: center;