mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into react-mobx
This commit is contained in:
commit
3e7420320c
@ -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)
|
||||
|
||||
|
@ -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"}
|
||||
|
@ -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)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -8,4 +8,5 @@ type PrometheusQuery struct {
|
||||
LegendFormat string
|
||||
Start time.Time
|
||||
End time.Time
|
||||
RefId string
|
||||
}
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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';
|
||||
|
@ -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 };
|
||||
};
|
||||
|
||||
});
|
||||
});
|
139
public/app/features/dashboard/dashboard_loader_srv.ts
Normal file
139
public/app/features/dashboard/dashboard_loader_srv.ts
Normal file
@ -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);
|
@ -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<AddPanelPanelProps, AddPanelP
|
||||
panelPlugins: this.getPanelPlugins(),
|
||||
filter: '',
|
||||
};
|
||||
|
||||
this.onPanelSelected = this.onPanelSelected.bind(this);
|
||||
}
|
||||
|
||||
getPanelPlugins() {
|
||||
let panels = _.chain(config.panels)
|
||||
.filter({hideFromList: false})
|
||||
.filter({ hideFromList: false })
|
||||
.map(item => 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 (
|
||||
<div key={panel.id} className="add-panel__item" onClick={() => this.onPanelSelected(panel)} title={panel.name}>
|
||||
<div key={index} className="add-panel__item" onClick={() => this.onAddPanel(panel)} title={panel.name}>
|
||||
<img className="add-panel__item-img" src={panel.info.logos.small} />
|
||||
<div className="add-panel__item-name">{panel.name}</div>
|
||||
</div>
|
||||
@ -75,7 +98,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
||||
<div className="panel-container">
|
||||
<div className="add-panel">
|
||||
<div className="add-panel__header">
|
||||
<i className="gicon gicon-add-panel"></i>
|
||||
<i className="gicon gicon-add-panel" />
|
||||
<span className="add-panel__title">New Panel</span>
|
||||
<span className="add-panel__sub-title">Select a visualization</span>
|
||||
</div>
|
||||
@ -87,4 +110,3 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -31,6 +31,7 @@ export class DashExportCtrl {
|
||||
var clone = this.dash;
|
||||
let editScope = this.$rootScope.$new();
|
||||
editScope.object = clone;
|
||||
editScope.enableCopy = true;
|
||||
|
||||
this.$rootScope.appEvent('show-modal', {
|
||||
src: 'public/app/partials/edit_json.html',
|
||||
|
@ -89,7 +89,7 @@
|
||||
<h3 class="dashboard-settings__header">View JSON</h3>
|
||||
|
||||
<div class="gf-form">
|
||||
<textarea class="gf-form-input" ng-model="ctrl.json" rows="30" spellcheck="false"></textarea>
|
||||
<code-editor content="ctrl.json" data-mode="json" data-max-lines=30 ></code-editor>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -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);
|
@ -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() {
|
||||
|
@ -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: '<unsaved-changes-modal dismiss="dismiss()"></unsaved-changes-modal>',
|
||||
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;
|
||||
};
|
||||
});
|
||||
});
|
216
public/app/features/dashboard/unsaved_changes_srv.ts
Normal file
216
public/app/features/dashboard/unsaved_changes_srv.ts
Normal file
@ -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: '<unsaved-changes-modal dismiss="dismiss()"></unsaved-changes-modal>',
|
||||
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);
|
@ -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 => {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
152
public/app/features/plugins/datasource_srv.ts
Normal file
152
public/app/features/plugins/datasource_srv.ts
Normal file
@ -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);
|
@ -16,6 +16,9 @@
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button" class="btn btn-success" ng-show="canUpdate" ng-click="update(); dismiss();">Update</button>
|
||||
<button class="btn btn-secondary" ng-if="canCopy" clipboard-button="getContentForClipboard()">
|
||||
<i class="fa fa-clipboard"></i> Copy to Clipboard
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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) {
|
||||
|
2
public/sass/base/font-awesome/_larger.scss
vendored
2
public/sass/base/font-awesome/_larger.scss
vendored
@ -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;
|
||||
|
@ -65,3 +65,7 @@
|
||||
.add-panel__item-img {
|
||||
height: calc(100% - 15px);
|
||||
}
|
||||
|
||||
.add-panel__item-icon {
|
||||
padding: 2px;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
}
|
||||
|
||||
.singlestat-panel-value-container {
|
||||
line-height: 1;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
|
Loading…
Reference in New Issue
Block a user