Merge branch 'master' into react-mobx

This commit is contained in:
Torkel Ödegaard 2017-12-26 13:53:39 +01:00
commit 3e7420320c
27 changed files with 759 additions and 602 deletions

View File

@ -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)

View File

@ -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"}

View File

@ -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)
})
})
})
}

View File

@ -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
}

View File

@ -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)
})
})

View File

@ -8,4 +8,5 @@ type PrometheusQuery struct {
LegendFormat string
Start time.Time
End time.Time
RefId string
}

View File

@ -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';

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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';

View File

@ -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 };
};
});
});

View 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);

View File

@ -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
);
}
}

View File

@ -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',

View File

@ -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>

View File

@ -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);

View File

@ -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() {

View File

@ -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;
};
});
});

View 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);

View File

@ -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 => {

View File

@ -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();
});
});

View 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);

View File

@ -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>&nbsp;Copy to Clipboard
</button>
</div>
</div>
</div>

View File

@ -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) {

View File

@ -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;

View File

@ -65,3 +65,7 @@
.add-panel__item-img {
height: calc(100% - 15px);
}
.add-panel__item-icon {
padding: 2px;
}

View File

@ -6,6 +6,7 @@
}
.singlestat-panel-value-container {
line-height: 1;
display: table-cell;
vertical-align: middle;
text-align: center;