Merge remote-tracking branch 'upstream/develop' into feat-9887

This commit is contained in:
Alexander Zobnin
2017-11-20 15:51:49 +03:00
274 changed files with 15401 additions and 2148 deletions

View File

@@ -11,6 +11,7 @@ export class AlertListCtrl {
stateFilters = [
{text: 'All', value: null},
{text: 'OK', value: 'ok'},
{text: 'Not OK', value: 'not_ok'},
{text: 'Alerting', value: 'alerting'},
{text: 'No Data', value: 'no_data'},
{text: 'Paused', value: 'paused'},

View File

@@ -95,6 +95,7 @@ export class AlertTabCtrl {
case "hipchat": return "fa fa-mail-forward";
case "pushover": return "fa fa-mobile";
case "kafka": return "fa fa-random";
case "teams": return "fa fa-windows";
}
return 'fa fa-bell';
}

View File

@@ -39,7 +39,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
text = text + '<br />' + event.text;
}
} else if (title) {
text = title + '<br />' + text;
text = title + '<br />' + (_.isString(text) ? text : '');
title = '';
}

View File

@@ -122,6 +122,27 @@ export class DashboardSrv {
modalClass: 'modal--narrow'
});
}
starDashboard(dashboardId, isStarred) {
let promise;
if (isStarred) {
promise = this.backendSrv.delete('/api/user/stars/dashboard/' + dashboardId).then(() => {
return false;
});
} else {
promise = this.backendSrv.post('/api/user/stars/dashboard/' + dashboardId).then(() => {
return true;
});
}
return promise.then(res => {
if (this.dash && this.dash.id === dashboardId) {
this.dash.meta.isStarred = res;
}
return res;
});
}
}
coreModule.service('dashboardSrv', DashboardSrv);

View File

@@ -84,7 +84,7 @@
Back to dashboard
</a>
</li>
<li>
<li ng-if="!ctrl.dashboard.timepicker.hidden">
<gf-time-picker dashboard="ctrl.dashboard"></gf-time-picker>
</li>
</ul>

View File

@@ -49,14 +49,9 @@ export class DashNavCtrl {
}
starDashboard() {
if (this.dashboard.meta.isStarred) {
return this.backendSrv.delete('/api/user/stars/dashboard/' + this.dashboard.id).then(() => {
this.dashboard.meta.isStarred = false;
});
}
this.backendSrv.post('/api/user/stars/dashboard/' + this.dashboard.id).then(() => {
this.dashboard.meta.isStarred = true;
this.dashboardSrv.starDashboard(this.dashboard.id, this.dashboard.meta.isStarred)
.then(newState => {
this.dashboard.meta.isStarred = newState;
});
}

View File

@@ -56,7 +56,9 @@ export class SaveDashboardAsModalCtrl {
// do not want to create alert dupes
if (dashboard.id > 0) {
this.clone.panels.forEach(panel => {
delete panel.thresholds;
if (panel.type === "graph" && panel.alert) {
delete panel.thresholds;
}
delete panel.alert;
});
}

View File

@@ -0,0 +1,62 @@
import { SaveDashboardAsModalCtrl } from '../save_as_modal';
import { describe, it, expect } from 'test/lib/common';
describe('saving dashboard as', () => {
function scenario(name, panel, verify) {
describe(name, () => {
var json = {
title: 'name',
panels: [panel],
};
var mockDashboardSrv = {
getCurrent: function() {
return {
id: 5,
meta: {},
getSaveModelClone: function() {
return json;
},
};
},
};
var ctrl = new SaveDashboardAsModalCtrl(mockDashboardSrv);
var ctx: any = {
clone: ctrl.clone,
ctrl: ctrl,
panel: panel
};
it('verify', () => {
verify(ctx);
});
});
}
scenario('default values', {}, ctx => {
var clone = ctx.clone;
expect(clone.id).toBe(null);
expect(clone.title).toBe('name Copy');
expect(clone.editable).toBe(true);
expect(clone.hideControls).toBe(false);
});
var graphPanel = { id: 1, type: 'graph', alert: { rule: 1 }, thresholds: { value: 3000 } };
scenario('should remove alert from graph panel', graphPanel, ctx => {
expect(ctx.panel.alert).toBe(undefined);
});
scenario('should remove threshold from graph panel', graphPanel, ctx => {
expect(ctx.panel.thresholds).toBe(undefined);
});
scenario('singlestat should keep threshold', { id: 1, type: 'singlestat', thresholds: { value: 3000 } }, ctx => {
expect(ctx.panel.thresholds).not.toBe(undefined);
});
scenario('table should keep threshold', { id: 1, type: 'table', thresholds: { value: 3000 } }, ctx => {
expect(ctx.panel.thresholds).not.toBe(undefined);
});
});

View File

@@ -2,7 +2,7 @@ import {describe, beforeEach, it, expect, sinon, angularMocks} from 'test/lib/co
import helpers from 'test/specs/helpers';
import '../shareModalCtrl';
import config from 'app/core/config';
import 'app/features/panellinks/linkSrv';
import 'app/features/panellinks/link_srv';
describe('ShareModalCtrl', function() {
var ctx = new helpers.ControllerTestContext();

View File

@@ -1,5 +1,3 @@
///<reference path="../../../headers/common.d.ts" />
import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
@@ -7,16 +5,16 @@ export function inputDateDirective() {
return {
restrict: 'A',
require: 'ngModel',
link: function ($scope, $elem, attrs, ngModel) {
link: function($scope, $elem, attrs, ngModel) {
var format = 'YYYY-MM-DD HH:mm:ss';
var fromUser = function (text) {
var fromUser = function(text) {
if (text.indexOf('now') !== -1) {
if (!dateMath.isValid(text)) {
ngModel.$setValidity("error", false);
ngModel.$setValidity('error', false);
return undefined;
}
ngModel.$setValidity("error", true);
ngModel.$setValidity('error', true);
return text;
}
@@ -28,15 +26,15 @@ export function inputDateDirective() {
}
if (!parsed.isValid()) {
ngModel.$setValidity("error", false);
ngModel.$setValidity('error', false);
return undefined;
}
ngModel.$setValidity("error", true);
ngModel.$setValidity('error', true);
return parsed;
};
var toUser = function (currentValue) {
var toUser = function(currentValue) {
if (moment.isMoment(currentValue)) {
return currentValue.format(format);
} else {
@@ -46,7 +44,6 @@ export function inputDateDirective() {
ngModel.$parsers.push(fromUser);
ngModel.$formatters.push(toUser);
}
},
};
}

View File

@@ -1,15 +1,14 @@
<div class="editor-row">
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-10">Auto-refresh</span>
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.refresh_intervals" array-join>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Now delay now-</span>
<input type="text" class="gf-form-input max-width-25"
ng-model="ctrl.panel.nowDelay" placeholder="0m"
valid-time-span
bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'"
data-placement="right">
</div>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-10">Auto-refresh</span>
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.refresh_intervals" array-join>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Now delay now-</span>
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.nowDelay" placeholder="0m" valid-time-span bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'"
data-placement="right">
</div>
<gf-form-switch class="gf-form" label="Hide time picker" checked="ctrl.panel.hidden" label-class="width-10"></gf-form-switch>
</div>
</div>

View File

@@ -1,34 +1,38 @@
<ul class="nav gf-timepicker-nav">
<li class="dashnav-move-timeframe gf-timepicker-time-control" bs-tooltip="'Shift time backward <br> (left arrow key)'" data-placement="bottom">
<a ng-click='ctrl.move(-1)'><i class="fa fa-chevron-left"></i></a>
</li>
<li class="dashnav-zoom-out gf-timepicker-time-control" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom">
<a ng-click='ctrl.zoom(2)'>Zoom Out</a></li>
</li>
<li class="dashnav-move-timeframe gf-timepicker-time-control" bs-tooltip="'Shift time forward <br> (right arrow key)'" data-placement="bottom">
<a ng-click='ctrl.move(1)'><i class="fa fa-chevron-right"></i></a>
</li>
<li class="dashnav-move-timeframe gf-timepicker-time-control" bs-tooltip="'Shift time backward <br> (left arrow key)'" data-placement="bottom">
<a ng-click='ctrl.move(-1)'>
<i class="fa fa-chevron-left"></i>
</a>
</li>
<li class="dashnav-zoom-out gf-timepicker-time-control" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom">
<a ng-click='ctrl.zoom(2)'>Zoom Out</a>
</li>
</li>
<li class="dashnav-move-timeframe gf-timepicker-time-control" bs-tooltip="'Shift time forward <br> (right arrow key)'" data-placement="bottom">
<a ng-click='ctrl.move(1)'>
<i class="fa fa-chevron-right"></i>
</a>
</li>
<li>
<a bs-tooltip="ctrl.tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()" class="gf-timepicker-nav-btn">
<i class="fa fa-clock-o"></i>
<span ng-bind="ctrl.rangeString"></span>
<span ng-show="ctrl.isUtc" class="gf-timepicker-utc">
UTC
</span>
<li>
<a bs-tooltip="ctrl.tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()" class="gf-timepicker-nav-btn">
<i class="fa fa-clock-o"></i>
<span ng-bind="ctrl.rangeString"></span>
<span ng-show="ctrl.isUtc" class="gf-timepicker-utc">
UTC
</span>
<span ng-show="ctrl.dashboard.refresh" class="text-warning">
&nbsp;
Refresh every {{ctrl.dashboard.refresh}}
</span>
</a>
</li>
<span ng-show="ctrl.dashboard.refresh" class="text-warning">
&nbsp; Refresh every {{ctrl.dashboard.refresh}}
</span>
</a>
</li>
<li class="dashnav-refresh-action">
<a ng-click="ctrl.timeSrv.refreshDashboard()">
<i class="fa fa-refresh"></i>
</a>
</li>
<li class="dashnav-refresh-action">
<a ng-click="ctrl.timeSrv.refreshDashboard()">
<i class="fa fa-refresh"></i>
</a>
</li>
</ul>

View File

@@ -13,9 +13,9 @@
</div>
<div class="gf-form" ng-show="link.type === 'dashboards'">
<span class="gf-form-label width-8">With tags</span>
<bootstrap-tagsinput ng-model="link.tags" class="width-10" tagclass="label label-tag" placeholder="add tags" style="margin-right: .25rem"></bootstrap-tagsinput>
<bootstrap-tagsinput ng-model="link.tags" tagclass="label label-tag" placeholder="add tags" style="margin-right: .25rem"></bootstrap-tagsinput>
</div>
<gf-form-switch ng-show="link.type === 'dashboards'" class="gf-form" label="As dropdown" checked="link.asDropdown" switch-class="max-width-4" label-class="width-8"></gf-form-switch>
<gf-form-switch ng-show="link.type === 'dashboards'" class="gf-form" label="As dropdown" checked="link.asDropdown" switch-class="max-width-4" label-class="width-8" on-change="updated()"></gf-form-switch>
<div class="gf-form" ng-show="link.type === 'dashboards' && link.asDropdown">
<span class="gf-form-label width-8">Title</span>
<input type="text" ng-model="link.title" class="gf-form-input max-width-10" ng-model-onblur ng-change="updated()">

View File

@@ -226,7 +226,6 @@ class MetricsPanelCtrl extends PanelCtrl {
interval: this.interval,
intervalMs: this.intervalMs,
targets: this.panel.targets,
format: this.panel.renderer === 'png' ? 'png' : 'json',
maxDataPoints: this.resolution,
scopedVars: scopedVars,
cacheTimeout: this.panel.cacheTimeout

View File

@@ -1,118 +0,0 @@
define([
'angular',
'lodash',
'app/core/utils/kbn',
],
function (angular, _, kbn) {
'use strict';
kbn = kbn.default;
angular
.module('grafana.services')
.service('linkSrv', function(templateSrv, timeSrv) {
this.getLinkUrl = function(link) {
var url = templateSrv.replace(link.url || '');
var params = {};
if (link.keepTime) {
var range = timeSrv.timeRangeForUrl();
params['from'] = range.from;
params['to'] = range.to;
}
if (link.includeVars) {
templateSrv.fillVariableValuesForUrl(params);
}
return this.addParamsToUrl(url, params);
};
this.addParamsToUrl = function(url, params) {
var paramsArray = [];
_.each(params, function(value, key) {
if (value === null) { return; }
if (value === true) {
paramsArray.push(key);
}
else if (_.isArray(value)) {
_.each(value, function(instance) {
paramsArray.push(key + '=' + encodeURIComponent(instance));
});
}
else {
paramsArray.push(key + '=' + encodeURIComponent(value));
}
});
if (paramsArray.length === 0) {
return url;
}
return this.appendToQueryString(url, paramsArray.join('&'));
};
this.appendToQueryString = function(url, stringToAppend) {
if (!_.isUndefined(stringToAppend) && stringToAppend !== null && stringToAppend !== '') {
var pos = url.indexOf('?');
if (pos !== -1) {
if (url.length - pos > 1) {
url += '&';
}
} else {
url += '?';
}
url += stringToAppend;
}
return url;
};
this.getAnchorInfo = function(link) {
var info = {};
info.href = this.getLinkUrl(link);
info.title = templateSrv.replace(link.title || '');
return info;
};
this.getPanelLinkAnchorInfo = function(link, scopedVars) {
var info = {};
if (link.type === 'absolute') {
info.target = link.targetBlank ? '_blank' : '_self';
info.href = templateSrv.replace(link.url || '', scopedVars);
info.title = templateSrv.replace(link.title || '', scopedVars);
}
else if (link.dashUri) {
info.href = 'dashboard/' + link.dashUri + '?';
info.title = templateSrv.replace(link.title || '', scopedVars);
info.target = link.targetBlank ? '_blank' : '';
}
else {
info.title = templateSrv.replace(link.title || '', scopedVars);
var slug = kbn.slugifyForUrl(link.dashboard || '');
info.href = 'dashboard/db/' + slug + '?';
}
var params = {};
if (link.keepTime) {
var range = timeSrv.timeRangeForUrl();
params['from'] = range.from;
params['to'] = range.to;
}
if (link.includeVars) {
templateSrv.fillVariableValuesForUrl(params, scopedVars);
}
info.href = this.addParamsToUrl(info.href, params);
if (link.params) {
info.href = this.appendToQueryString(info.href, templateSrv.replace(link.params, scopedVars));
}
return info;
};
});
});

View File

@@ -0,0 +1,113 @@
import angular from 'angular';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
export class LinkSrv {
/** @ngInject */
constructor(private templateSrv, private timeSrv) {}
getLinkUrl(link) {
var url = this.templateSrv.replace(link.url || '');
var params = {};
if (link.keepTime) {
var range = this.timeSrv.timeRangeForUrl();
params['from'] = range.from;
params['to'] = range.to;
}
if (link.includeVars) {
this.templateSrv.fillVariableValuesForUrl(params);
}
return this.addParamsToUrl(url, params);
}
addParamsToUrl(url, params) {
var paramsArray = [];
_.each(params, function(value, key) {
if (value === null) {
return;
}
if (value === true) {
paramsArray.push(key);
} else if (_.isArray(value)) {
_.each(value, function(instance) {
paramsArray.push(key + '=' + encodeURIComponent(instance));
});
} else {
paramsArray.push(key + '=' + encodeURIComponent(value));
}
});
if (paramsArray.length === 0) {
return url;
}
return this.appendToQueryString(url, paramsArray.join('&'));
}
appendToQueryString(url, stringToAppend) {
if (!_.isUndefined(stringToAppend) && stringToAppend !== null && stringToAppend !== '') {
var pos = url.indexOf('?');
if (pos !== -1) {
if (url.length - pos > 1) {
url += '&';
}
} else {
url += '?';
}
url += stringToAppend;
}
return url;
}
getAnchorInfo(link) {
var info: any = {};
info.href = this.getLinkUrl(link);
info.title = this.templateSrv.replace(link.title || '');
return info;
}
getPanelLinkAnchorInfo(link, scopedVars) {
var info: any = {};
if (link.type === 'absolute') {
info.target = link.targetBlank ? '_blank' : '_self';
info.href = this.templateSrv.replace(link.url || '', scopedVars);
info.title = this.templateSrv.replace(link.title || '', scopedVars);
} else if (link.dashUri) {
info.href = 'dashboard/' + link.dashUri + '?';
info.title = this.templateSrv.replace(link.title || '', scopedVars);
info.target = link.targetBlank ? '_blank' : '';
} else {
info.title = this.templateSrv.replace(link.title || '', scopedVars);
var slug = kbn.slugifyForUrl(link.dashboard || '');
info.href = 'dashboard/db/' + slug + '?';
}
var params = {};
if (link.keepTime) {
var range = this.timeSrv.timeRangeForUrl();
params['from'] = range.from;
params['to'] = range.to;
}
if (link.includeVars) {
this.templateSrv.fillVariableValuesForUrl(params, scopedVars);
}
info.href = this.addParamsToUrl(info.href, params);
if (link.params) {
info.href = this.appendToQueryString(info.href, this.templateSrv.replace(link.params, scopedVars));
}
return info;
}
}
angular.module('grafana.services').service('linkSrv', LinkSrv);

View File

@@ -1,7 +1,7 @@
define([
'angular',
'lodash',
'./linkSrv',
'./link_srv',
],
function (angular, _) {
'use strict';

View File

@@ -0,0 +1,47 @@
import { LinkSrv } from '../link_srv';
import _ from 'lodash';
jest.mock('angular', () => {
let AngularJSMock = require('test/mocks/angular');
return new AngularJSMock();
});
describe('linkSrv', function() {
var linkSrv;
var templateSrvMock = {};
var timeSrvMock = {};
beforeEach(() => {
linkSrv = new LinkSrv(templateSrvMock, timeSrvMock);
});
describe('when appending query strings', function() {
it('add ? to URL if not present', function() {
var url = linkSrv.appendToQueryString('http://example.com', 'foo=bar');
expect(url).toBe('http://example.com?foo=bar');
});
it('do not add & to URL if ? is present but query string is empty', function() {
var url = linkSrv.appendToQueryString('http://example.com?', 'foo=bar');
expect(url).toBe('http://example.com?foo=bar');
});
it('add & to URL if query string is present', function() {
var url = linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world');
expect(url).toBe('http://example.com?foo=bar&hello=world');
});
it('do not change the URL if there is nothing to append', function() {
_.each(['', undefined, null], function(toAppend) {
var url1 = linkSrv.appendToQueryString('http://example.com', toAppend);
expect(url1).toBe('http://example.com');
var url2 = linkSrv.appendToQueryString('http://example.com?', toAppend);
expect(url2).toBe('http://example.com?');
var url3 = linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend);
expect(url3).toBe('http://example.com?foo=bar');
});
});
});
});

View File

@@ -1,46 +0,0 @@
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
import 'app/features/panellinks/linkSrv';
import _ from 'lodash';
describe('linkSrv', function() {
var _linkSrv;
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.inject(function(linkSrv) {
_linkSrv = linkSrv;
}));
describe('when appending query strings', function() {
it('add ? to URL if not present', function() {
var url = _linkSrv.appendToQueryString('http://example.com', 'foo=bar');
expect(url).to.be('http://example.com?foo=bar');
});
it('do not add & to URL if ? is present but query string is empty', function() {
var url = _linkSrv.appendToQueryString('http://example.com?', 'foo=bar');
expect(url).to.be('http://example.com?foo=bar');
});
it('add & to URL if query string is present', function() {
var url = _linkSrv.appendToQueryString('http://example.com?foo=bar', 'hello=world');
expect(url).to.be('http://example.com?foo=bar&hello=world');
});
it('do not change the URL if there is nothing to append', function() {
_.each(['', undefined, null], function(toAppend) {
var url1 = _linkSrv.appendToQueryString('http://example.com', toAppend);
expect(url1).to.be('http://example.com');
var url2 = _linkSrv.appendToQueryString('http://example.com?', toAppend);
expect(url2).to.be('http://example.com?');
var url3 = _linkSrv.appendToQueryString('http://example.com?foo=bar', toAppend);
expect(url3).to.be('http://example.com?foo=bar');
});
});
});
});

View File

@@ -157,6 +157,10 @@ export class DataSourceEditCtrl {
return;
}
if (this.current.readOnly) {
return;
}
if (this.current.id) {
return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then((result) => {
this.current = result.datasource;

View File

@@ -5,6 +5,8 @@
<div class="page-header">
<page-h1 model="ctrl.navModel"></page-h1>
<div ng-if="ctrl.current.readOnly" class="grafana-info-box span8">Disclaimer. This datasource was added by config and cannot be modified using the UI. Please contact your server admin to update this datasource.</div>
<div class="page-header-tabs" ng-show="ctrl.hasDashboards">
<ul class="gf-tabs">
<li class="gf-tabs-item">
@@ -21,7 +23,6 @@
</div>
</div>
<div ng-if="ctrl.tabIndex === 0" class="tab-content">
<form name="ctrl.editForm" ng-if="ctrl.current">
@@ -71,8 +72,8 @@
</div>
<div class="gf-form-button-row">
<button type="submit" class="btn btn-success" ng-click="ctrl.saveChanges()">Save</button>
<button type="submit" class="btn btn-danger" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">
<button type="submit" class="btn btn-success" ng-disabled="ctrl.current.readOnly" ng-click="ctrl.saveChanges()">Save</button>
<button type="submit" class="btn btn-danger" ng-disabled="ctrl.current.readOnly" ng-show="!ctrl.isNew" ng-click="ctrl.delete()">
Delete
</button>
<a class="btn btn-link" href="datasources">Cancel</a>
@@ -87,4 +88,3 @@
</div>
</div>
</div>

View File

@@ -40,7 +40,10 @@ System.config({
css: 'vendor/plugin-css/css.js'
},
meta: {
'*': {esModule: true}
'*': {
esModule: true,
authorization: true,
}
}
});