Merge branch 'master' into dashboard-acl-ux2

This commit is contained in:
Marcus Efraimsson
2018-04-16 21:19:01 +02:00
215 changed files with 4447 additions and 2180 deletions

View File

@@ -1,5 +1,5 @@
import React from 'react';
import PerfectScrollbar from 'perfect-scrollbar';
import baron from 'baron';
export interface Props {
children: any;
@@ -8,31 +8,36 @@ export interface Props {
export default class ScrollBar extends React.Component<Props, any> {
private container: any;
private ps: PerfectScrollbar;
private scrollbar: baron;
constructor(props) {
super(props);
}
componentDidMount() {
this.ps = new PerfectScrollbar(this.container, {
wheelPropagation: true,
this.scrollbar = baron({
root: this.container.parentElement,
scroller: this.container,
bar: '.baron__bar',
barOnCls: '_scrollbar',
scrollingCls: '_scrolling',
track: '.baron__track',
});
}
componentDidUpdate() {
this.ps.update();
this.scrollbar.update();
}
componentWillUnmount() {
this.ps.destroy();
this.scrollbar.dispose();
}
// methods can be invoked by outside
setScrollTop(top) {
if (this.container) {
this.container.scrollTop = top;
this.ps.update();
this.scrollbar.update();
return true;
}
@@ -42,7 +47,7 @@ export default class ScrollBar extends React.Component<Props, any> {
setScrollLeft(left) {
if (this.container) {
this.container.scrollLeft = left;
this.ps.update();
this.scrollbar.update();
return true;
}
@@ -55,8 +60,14 @@ export default class ScrollBar extends React.Component<Props, any> {
render() {
return (
<div className={this.props.className} ref={this.handleRef}>
{this.props.children}
<div className="baron baron__root baron__clipper">
<div className={this.props.className + ' baron__scroller'} ref={this.handleRef}>
{this.props.children}
</div>
<div className="baron__track">
<div className="baron__bar" />
</div>
</div>
);
}

View File

@@ -117,6 +117,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
appEvents.emit('toggle-kiosk-mode');
}
// check for 'inactive' url param for clean looks like kiosk, but with title
if (data.params.inactive) {
body.addClass('user-activity-low');
// for some reason, with this class it looks cleanest
body.addClass('sidemenu-open');
}
// close all drops
for (let drop of Drop.drops) {
drop.destroy();
@@ -167,6 +175,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
if (sidemenuHidden) {
sidemenuHidden = false;
body.addClass('sidemenu-open');
appEvents.emit('toggle-inactive-mode');
$timeout(function() {
$rootScope.$broadcast('render');
}, 100);

View File

@@ -2,7 +2,7 @@
// Licence MIT, Copyright (c) 2015 Mohsen Azimi
/*
* Escapes `"` charachters from string
* Escapes `"` characters from string
*/
function escapeString(str: string): string {
return str.replace('"', '"');
@@ -100,7 +100,7 @@ export function cssClass(className: string): string {
}
/*
* Creates a new DOM element wiht given type and class
* Creates a new DOM element with given type and class
* TODO: move me to helpers
*/
export function createElement(type: string, className?: string, content?: Element | string): Element {

View File

@@ -146,7 +146,7 @@ export class JsonExplorer {
}
/*
* did we recieve a key argument?
* did we receive a key argument?
* This means that the formatter was called as a sub formatter of a parent formatter
*/
private get hasKey(): boolean {

View File

@@ -0,0 +1,41 @@
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
export function pageScrollbar() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
let lastPos = 0;
appEvents.on(
'dash-scroll',
evt => {
if (evt.restore) {
elem[0].scrollTop = lastPos;
return;
}
lastPos = elem[0].scrollTop;
if (evt.animate) {
elem.animate({ scrollTop: evt.pos }, 500);
} else {
elem[0].scrollTop = evt.pos;
}
},
scope
);
scope.$on('$routeChangeSuccess', () => {
lastPos = 0;
elem[0].scrollTop = 0;
elem[0].focus();
});
elem[0].tabIndex = -1;
elem[0].focus();
},
};
}
coreModule.directive('pageScrollbar', pageScrollbar);

View File

@@ -1,15 +1,44 @@
import PerfectScrollbar from 'perfect-scrollbar';
import $ from 'jquery';
import baron from 'baron';
import coreModule from 'app/core/core_module';
import appEvents from 'app/core/app_events';
const scrollBarHTML = `
<div class="baron__track">
<div class="baron__bar"></div>
</div>
`;
const scrollRootClass = 'baron baron__root';
const scrollerClass = 'baron__scroller';
export function geminiScrollbar() {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
let scrollbar = new PerfectScrollbar(elem[0], {
wheelPropagation: true,
wheelSpeed: 3,
});
let scrollRoot = elem.parent();
let scroller = elem;
if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
scrollRoot = scroller;
}
scrollRoot.addClass(scrollRootClass);
$(scrollBarHTML).appendTo(scrollRoot);
elem.addClass(scrollerClass);
let scrollParams = {
root: scrollRoot[0],
scroller: scroller[0],
bar: '.baron__bar',
barOnCls: '_scrollbar',
scrollingCls: '_scrolling',
track: '.baron__track',
direction: 'v',
};
let scrollbar = baron(scrollParams);
let lastPos = 0;
appEvents.on(
@@ -31,13 +60,24 @@ export function geminiScrollbar() {
scope
);
// force updating dashboard width
appEvents.on('toggle-sidemenu', forceUpdate, scope);
appEvents.on('toggle-sidemenu-hidden', forceUpdate, scope);
appEvents.on('toggle-view-mode', forceUpdate, scope);
appEvents.on('toggle-kiosk-mode', forceUpdate, scope);
appEvents.on('toggle-inactive-mode', forceUpdate, scope);
function forceUpdate() {
scrollbar.scroll();
}
scope.$on('$routeChangeSuccess', () => {
lastPos = 0;
elem[0].scrollTop = 0;
});
scope.$on('$destroy', () => {
scrollbar.destroy();
scrollbar.dispose();
});
},
};

View File

@@ -19,6 +19,7 @@
<div class="search-dropdown">
<div class="search-dropdown__col_1">
<div class="search-results-scroller">
<div class="search-results-container" grafana-scrollbar>
<h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
<dashboard-search-results
@@ -27,6 +28,7 @@
on-folder-expanding="ctrl.folderExpanding()"
on-folder-expanded="ctrl.folderExpanded($folder)" />
</div>
</div>
</div>
<div class="search-dropdown__col_2">

View File

@@ -20,7 +20,7 @@
<div class="search-section__header" ng-show="section.hideHeader"></div>
<div ng-if="section.expanded">
<a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
<a ng-repeat="item in section.items" class="search-item search-item--indent" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}" >
<div ng-click="ctrl.toggleSelection(item, $event)">
<gf-form-switch
ng-show="ctrl.editable"

View File

@@ -54,6 +54,9 @@
</span>
</a>
<ul class="dropdown-menu dropdown-menu--sidemenu" role="menu">
<li ng-if="item.subTitle" class="sidemenu-subtitle">
<span class="sidemenu-item-text">{{::item.subTitle}}</span>
</li>
<li ng-if="item.showOrgSwitcher" class="sidemenu-org-switcher">
<a ng-click="ctrl.switchOrg()">
<div>
@@ -75,4 +78,4 @@
</li>
</ul>
</div>
</div>
</div>

View File

@@ -47,6 +47,7 @@ import { NavModelSrv, NavModel } from './nav_model_srv';
import { userPicker } from './components/user_picker';
import { teamPicker } from './components/team_picker';
import { geminiScrollbar } from './components/scroll/scroll';
import { pageScrollbar } from './components/scroll/page_scroll';
import { gfPageDirective } from './components/gf_page';
import { orgSwitcher } from './components/org_switcher';
import { profiler } from './profiler';
@@ -85,6 +86,7 @@ export {
userPicker,
teamPicker,
geminiScrollbar,
pageScrollbar,
gfPageDirective,
orgSwitcher,
manageDashboardsDirective,

View File

@@ -1,36 +0,0 @@
define([
'lodash',
'jquery',
'../core_module',
],
function (_, $, coreModule) {
'use strict';
coreModule.default.directive('dashClass', function() {
return {
link: function($scope, elem) {
$scope.onAppEvent('panel-fullscreen-enter', function() {
elem.toggleClass('panel-in-fullscreen', true);
});
$scope.onAppEvent('panel-fullscreen-exit', function() {
elem.toggleClass('panel-in-fullscreen', false);
});
$scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
if (newValue) {
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
setTimeout(function() {
elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
}, 10);
} else {
elem.removeClass('dashboard-page--settings-opening');
elem.removeClass('dashboard-page--settings-open');
}
});
}
};
});
});

View File

@@ -0,0 +1,31 @@
import _ from 'lodash';
import coreModule from '../core_module';
/** @ngInject */
export function dashClass() {
return {
link: function($scope, elem) {
$scope.onAppEvent('panel-fullscreen-enter', function() {
elem.toggleClass('panel-in-fullscreen', true);
});
$scope.onAppEvent('panel-fullscreen-exit', function() {
elem.toggleClass('panel-in-fullscreen', false);
});
$scope.$watch('ctrl.dashboardViewState.state.editview', function(newValue) {
if (newValue) {
elem.toggleClass('dashboard-page--settings-opening', _.isString(newValue));
setTimeout(function() {
elem.toggleClass('dashboard-page--settings-open', _.isString(newValue));
}, 10);
} else {
elem.removeClass('dashboard-page--settings-opening');
elem.removeClass('dashboard-page--settings-open');
}
});
},
};
}
coreModule.directive('dashClass', dashClass);

View File

@@ -1,236 +0,0 @@
define([
'lodash',
'jquery',
'../core_module',
],
function (_, $, coreModule) {
'use strict';
coreModule.default.directive('dropdownTypeahead', function($compile) {
var inputTemplate = '<input type="text"'+
' class="gf-form-input input-medium tight-form-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' data-placement="top"><i class="fa fa-plus"></i></a>';
return {
scope: {
menuItems: "=dropdownTypeahead",
dropdownTypeaheadOnSelect: "&dropdownTypeaheadOnSelect",
model: '=ngModel'
},
link: function($scope, elem, attrs) {
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
if (attrs.linkText) {
$button.html(attrs.linkText);
}
if (attrs.ngModel) {
$scope.$watch('model', function(newValue) {
_.each($scope.menuItems, function(item) {
_.each(item.submenu, function(subItem) {
if (subItem.value === newValue) {
$button.html(subItem.text);
}
});
});
});
}
var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) {
if (!value.submenu) {
value.click = 'menuItemSelected(' + index + ')';
memo.push(value.text);
} else {
_.each(value.submenu, function(item, subIndex) {
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
memo.push(value.text + ' ' + item.text);
});
}
return memo;
}, []);
$scope.menuItemSelected = function(index, subIndex) {
var menuItem = $scope.menuItems[index];
var payload = {$item: menuItem};
if (menuItem.submenu && subIndex !== void 0) {
payload.$subItem = menuItem.submenu[subIndex];
}
$scope.dropdownTypeaheadOnSelect(payload);
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: typeaheadValues,
minLength: 1,
items: 10,
updater: function (value) {
var result = {};
_.each($scope.menuItems, function(menuItem) {
_.each(menuItem.submenu, function(submenuItem) {
if (value === (menuItem.text + ' ' + submenuItem.text)) {
result.$subItem = submenuItem;
result.$item = menuItem;
}
});
});
if (result.$item) {
$scope.$apply(function() {
$scope.dropdownTypeaheadOnSelect(result);
});
}
$input.trigger('blur');
return '';
}
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(function() {
$input.hide();
$input.val('');
$button.show();
$button.focus();
// clicking the function dropdown menu wont
// work if you remove class at once
setTimeout(function() {
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
}
};
});
coreModule.default.directive('dropdownTypeahead2', function($compile) {
var inputTemplate = '<input type="text"'+
' class="gf-form-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="gf-form-input dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' data-placement="top"><i class="fa fa-plus"></i></a>';
return {
scope: {
menuItems: "=dropdownTypeahead2",
dropdownTypeaheadOnSelect: "&dropdownTypeaheadOnSelect",
model: '=ngModel'
},
link: function($scope, elem, attrs) {
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
if (attrs.linkText) {
$button.html(attrs.linkText);
}
if (attrs.ngModel) {
$scope.$watch('model', function(newValue) {
_.each($scope.menuItems, function(item) {
_.each(item.submenu, function(subItem) {
if (subItem.value === newValue) {
$button.html(subItem.text);
}
});
});
});
}
var typeaheadValues = _.reduce($scope.menuItems, function(memo, value, index) {
if (!value.submenu) {
value.click = 'menuItemSelected(' + index + ')';
memo.push(value.text);
} else {
_.each(value.submenu, function(item, subIndex) {
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
memo.push(value.text + ' ' + item.text);
});
}
return memo;
}, []);
$scope.menuItemSelected = function(index, subIndex) {
var menuItem = $scope.menuItems[index];
var payload = {$item: menuItem};
if (menuItem.submenu && subIndex !== void 0) {
payload.$subItem = menuItem.submenu[subIndex];
}
$scope.dropdownTypeaheadOnSelect(payload);
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: typeaheadValues,
minLength: 1,
items: 10,
updater: function (value) {
var result = {};
_.each($scope.menuItems, function(menuItem) {
_.each(menuItem.submenu, function(submenuItem) {
if (value === (menuItem.text + ' ' + submenuItem.text)) {
result.$subItem = submenuItem;
result.$item = menuItem;
}
});
});
if (result.$item) {
$scope.$apply(function() {
$scope.dropdownTypeaheadOnSelect(result);
});
}
$input.trigger('blur');
return '';
}
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(function() {
$input.hide();
$input.val('');
$button.show();
$button.focus();
// clicking the function dropdown menu wont
// work if you remove class at once
setTimeout(function() {
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
}
};
});
});

View File

@@ -0,0 +1,244 @@
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../core_module';
/** @ngInject */
export function dropdownTypeahead($compile) {
let inputTemplate =
'<input type="text"' +
' class="gf-form-input input-medium tight-form-input"' +
' spellcheck="false" style="display:none"></input>';
let buttonTemplate =
'<a class="gf-form-label tight-form-func dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' data-placement="top"><i class="fa fa-plus"></i></a>';
return {
scope: {
menuItems: '=dropdownTypeahead',
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
model: '=ngModel',
},
link: function($scope, elem, attrs) {
let $input = $(inputTemplate);
let $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
if (attrs.linkText) {
$button.html(attrs.linkText);
}
if (attrs.ngModel) {
$scope.$watch('model', function(newValue) {
_.each($scope.menuItems, function(item) {
_.each(item.submenu, function(subItem) {
if (subItem.value === newValue) {
$button.html(subItem.text);
}
});
});
});
}
let typeaheadValues = _.reduce(
$scope.menuItems,
function(memo, value, index) {
if (!value.submenu) {
value.click = 'menuItemSelected(' + index + ')';
memo.push(value.text);
} else {
_.each(value.submenu, function(item, subIndex) {
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
memo.push(value.text + ' ' + item.text);
});
}
return memo;
},
[]
);
$scope.menuItemSelected = function(index, subIndex) {
let menuItem = $scope.menuItems[index];
let payload: any = { $item: menuItem };
if (menuItem.submenu && subIndex !== void 0) {
payload.$subItem = menuItem.submenu[subIndex];
}
$scope.dropdownTypeaheadOnSelect(payload);
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: typeaheadValues,
minLength: 1,
items: 10,
updater: function(value) {
let result: any = {};
_.each($scope.menuItems, function(menuItem) {
_.each(menuItem.submenu, function(submenuItem) {
if (value === menuItem.text + ' ' + submenuItem.text) {
result.$subItem = submenuItem;
result.$item = menuItem;
}
});
});
if (result.$item) {
$scope.$apply(function() {
$scope.dropdownTypeaheadOnSelect(result);
});
}
$input.trigger('blur');
return '';
},
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(function() {
$input.hide();
$input.val('');
$button.show();
$button.focus();
// clicking the function dropdown menu won't
// work if you remove class at once
setTimeout(function() {
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
},
};
}
/** @ngInject */
export function dropdownTypeahead2($compile) {
let inputTemplate =
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
let buttonTemplate =
'<a class="gf-form-input dropdown-toggle"' +
' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
' data-placement="top"><i class="fa fa-plus"></i></a>';
return {
scope: {
menuItems: '=dropdownTypeahead2',
dropdownTypeaheadOnSelect: '&dropdownTypeaheadOnSelect',
model: '=ngModel',
},
link: function($scope, elem, attrs) {
let $input = $(inputTemplate);
let $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
if (attrs.linkText) {
$button.html(attrs.linkText);
}
if (attrs.ngModel) {
$scope.$watch('model', function(newValue) {
_.each($scope.menuItems, function(item) {
_.each(item.submenu, function(subItem) {
if (subItem.value === newValue) {
$button.html(subItem.text);
}
});
});
});
}
let typeaheadValues = _.reduce(
$scope.menuItems,
function(memo, value, index) {
if (!value.submenu) {
value.click = 'menuItemSelected(' + index + ')';
memo.push(value.text);
} else {
_.each(value.submenu, function(item, subIndex) {
item.click = 'menuItemSelected(' + index + ',' + subIndex + ')';
memo.push(value.text + ' ' + item.text);
});
}
return memo;
},
[]
);
$scope.menuItemSelected = function(index, subIndex) {
let menuItem = $scope.menuItems[index];
let payload: any = { $item: menuItem };
if (menuItem.submenu && subIndex !== void 0) {
payload.$subItem = menuItem.submenu[subIndex];
}
$scope.dropdownTypeaheadOnSelect(payload);
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: typeaheadValues,
minLength: 1,
items: 10,
updater: function(value) {
let result: any = {};
_.each($scope.menuItems, function(menuItem) {
_.each(menuItem.submenu, function(submenuItem) {
if (value === menuItem.text + ' ' + submenuItem.text) {
result.$subItem = submenuItem;
result.$item = menuItem;
}
});
});
if (result.$item) {
$scope.$apply(function() {
$scope.dropdownTypeaheadOnSelect(result);
});
}
$input.trigger('blur');
return '';
},
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(function() {
$input.hide();
$input.val('');
$button.show();
$button.focus();
// clicking the function dropdown menu won't
// work if you remove class at once
setTimeout(function() {
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
},
};
}
coreModule.directive('dropdownTypeahead', dropdownTypeahead);
coreModule.directive('dropdownTypeahead2', dropdownTypeahead2);

View File

@@ -1,246 +0,0 @@
define([
'lodash',
'jquery',
'../core_module',
],
function (_, $, coreModule) {
'use strict';
coreModule.default.directive('metricSegment', function($compile, $sce) {
var inputTemplate = '<input type="text" data-provide="typeahead" ' +
' class="gf-form-input input-medium"' +
' spellcheck="false" style="display:none"></input>';
var linkTemplate = '<a class="gf-form-label" ng-class="segment.cssClass" ' +
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
var selectTemplate = '<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
return {
scope: {
segment: "=",
getOptions: "&",
onChange: "&",
debounce: "@",
},
link: function($scope, elem) {
var $input = $(inputTemplate);
var segment = $scope.segment;
var $button = $(segment.selectMode ? selectTemplate : linkTemplate);
var options = null;
var cancelBlur = null;
var linkMode = true;
var debounceLookup = $scope.debounce;
$input.appendTo(elem);
$button.appendTo(elem);
$scope.updateVariableValue = function(value) {
if (value === '' || segment.value === value) {
return;
}
value = _.unescape(value);
$scope.$apply(function() {
var selected = _.find($scope.altSegments, {value: value});
if (selected) {
segment.value = selected.value;
segment.html = selected.html || selected.value;
segment.fake = false;
segment.expandable = selected.expandable;
if (selected.type) {
segment.type = selected.type;
}
}
else if (segment.custom !== 'false') {
segment.value = value;
segment.html = $sce.trustAsHtml(value);
segment.expandable = true;
segment.fake = false;
}
$scope.onChange();
});
};
$scope.switchToLink = function(fromClick) {
if (linkMode && !fromClick) { return; }
clearTimeout(cancelBlur);
cancelBlur = null;
linkMode = true;
$input.hide();
$button.show();
$scope.updateVariableValue($input.val());
};
$scope.inputBlur = function() {
// happens long before the click event on the typeahead options
// need to have long delay because the blur
cancelBlur = setTimeout($scope.switchToLink, 200);
};
$scope.source = function(query, callback) {
$scope.$apply(function() {
$scope.getOptions({ $query: query }).then(function(altSegments) {
$scope.altSegments = altSegments;
options = _.map($scope.altSegments, function(alt) {
return _.escape(alt.value);
});
// add custom values
if (segment.custom !== 'false') {
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
options.unshift(segment.value);
}
}
callback(options);
});
});
};
$scope.updater = function(value) {
if (value === segment.value) {
clearTimeout(cancelBlur);
$input.focus();
return value;
}
$input.val(value);
$scope.switchToLink(true);
return value;
};
$scope.matcher = function(item) {
var str = this.query;
if (str[0] === '/') { str = str.substring(1); }
if (str[str.length - 1] === '/') { str = str.substring(0, str.length-1); }
try {
return item.toLowerCase().match(str.toLowerCase());
} catch(e) {
return false;
}
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater, matcher: $scope.matcher });
var typeahead = $input.data('typeahead');
typeahead.lookup = function () {
this.query = this.$element.val() || '';
var items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
if (debounceLookup) {
typeahead.lookup = _.debounce(typeahead.lookup, 500, {leading: true});
}
$button.keydown(function(evt) {
// trigger typeahead on down arrow or enter key
if (evt.keyCode === 40 || evt.keyCode === 13) {
$button.click();
}
});
$button.click(function() {
options = null;
$input.css('width', (Math.max($button.width(), 80) + 16) + 'px');
$button.hide();
$input.show();
$input.focus();
linkMode = false;
var typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
});
$input.blur($scope.inputBlur);
$compile(elem.contents())($scope);
}
};
});
coreModule.default.directive('metricSegmentModel', function(uiSegmentSrv, $q) {
return {
template: '<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>',
restrict: 'E',
scope: {
property: "=",
options: "=",
getOptions: "&",
onChange: "&",
},
link: {
pre: function postLink($scope, elem, attrs) {
var cachedOptions;
$scope.valueToSegment = function(value) {
var option = _.find($scope.options, {value: value});
var segment = {
cssClass: attrs.cssClass,
custom: attrs.custom,
value: option ? option.text : value,
selectMode: attrs.selectMode,
};
return uiSegmentSrv.newSegment(segment);
};
$scope.getOptionsInternal = function() {
if ($scope.options) {
cachedOptions = $scope.options;
return $q.when(_.map($scope.options, function(option) {
return {value: option.text};
}));
} else {
return $scope.getOptions().then(function(options) {
cachedOptions = options;
return _.map(options, function(option) {
if (option.html) {
return option;
}
return {value: option.text};
});
});
}
};
$scope.onSegmentChange = function() {
if (cachedOptions) {
var option = _.find(cachedOptions, {text: $scope.segment.value});
if (option && option.value !== $scope.property) {
$scope.property = option.value;
} else if (attrs.custom !== 'false') {
$scope.property = $scope.segment.value;
}
} else {
$scope.property = $scope.segment.value;
}
// needs to call this after digest so
// property is synced with outerscope
$scope.$$postDigest(function() {
$scope.$apply(function() {
$scope.onChange();
});
});
};
$scope.segment = $scope.valueToSegment($scope.property);
}
}
};
});
});

View File

@@ -0,0 +1,263 @@
import _ from 'lodash';
import $ from 'jquery';
import coreModule from '../core_module';
/** @ngInject */
export function metricSegment($compile, $sce) {
let inputTemplate =
'<input type="text" data-provide="typeahead" ' +
' class="gf-form-input input-medium"' +
' spellcheck="false" style="display:none"></input>';
let linkTemplate =
'<a class="gf-form-label" ng-class="segment.cssClass" ' +
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
let selectTemplate =
'<a class="gf-form-input gf-form-input--dropdown" ng-class="segment.cssClass" ' +
'tabindex="1" give-focus="segment.focus" ng-bind-html="segment.html"></a>';
return {
scope: {
segment: '=',
getOptions: '&',
onChange: '&',
debounce: '@',
},
link: function($scope, elem) {
let $input = $(inputTemplate);
let segment = $scope.segment;
let $button = $(segment.selectMode ? selectTemplate : linkTemplate);
let options = null;
let cancelBlur = null;
let linkMode = true;
let debounceLookup = $scope.debounce;
$input.appendTo(elem);
$button.appendTo(elem);
$scope.updateVariableValue = function(value) {
if (value === '' || segment.value === value) {
return;
}
value = _.unescape(value);
$scope.$apply(function() {
let selected = _.find($scope.altSegments, { value: value });
if (selected) {
segment.value = selected.value;
segment.html = selected.html || selected.value;
segment.fake = false;
segment.expandable = selected.expandable;
if (selected.type) {
segment.type = selected.type;
}
} else if (segment.custom !== 'false') {
segment.value = value;
segment.html = $sce.trustAsHtml(value);
segment.expandable = true;
segment.fake = false;
}
$scope.onChange();
});
};
$scope.switchToLink = function(fromClick) {
if (linkMode && !fromClick) {
return;
}
clearTimeout(cancelBlur);
cancelBlur = null;
linkMode = true;
$input.hide();
$button.show();
$scope.updateVariableValue($input.val());
};
$scope.inputBlur = function() {
// happens long before the click event on the typeahead options
// need to have long delay because the blur
cancelBlur = setTimeout($scope.switchToLink, 200);
};
$scope.source = function(query, callback) {
$scope.$apply(function() {
$scope.getOptions({ $query: query }).then(function(altSegments) {
$scope.altSegments = altSegments;
options = _.map($scope.altSegments, function(alt) {
return _.escape(alt.value);
});
// add custom values
if (segment.custom !== 'false') {
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
options.unshift(segment.value);
}
}
callback(options);
});
});
};
$scope.updater = function(value) {
if (value === segment.value) {
clearTimeout(cancelBlur);
$input.focus();
return value;
}
$input.val(value);
$scope.switchToLink(true);
return value;
};
$scope.matcher = function(item) {
let str = this.query;
if (str[0] === '/') {
str = str.substring(1);
}
if (str[str.length - 1] === '/') {
str = str.substring(0, str.length - 1);
}
try {
return item.toLowerCase().match(str.toLowerCase());
} catch (e) {
return false;
}
};
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: $scope.source,
minLength: 0,
items: 10000,
updater: $scope.updater,
matcher: $scope.matcher,
});
let typeahead = $input.data('typeahead');
typeahead.lookup = function() {
this.query = this.$element.val() || '';
let items = this.source(this.query, $.proxy(this.process, this));
return items ? this.process(items) : items;
};
if (debounceLookup) {
typeahead.lookup = _.debounce(typeahead.lookup, 500, { leading: true });
}
$button.keydown(function(evt) {
// trigger typeahead on down arrow or enter key
if (evt.keyCode === 40 || evt.keyCode === 13) {
$button.click();
}
});
$button.click(function() {
options = null;
$input.css('width', Math.max($button.width(), 80) + 16 + 'px');
$button.hide();
$input.show();
$input.focus();
linkMode = false;
let typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
});
$input.blur($scope.inputBlur);
$compile(elem.contents())($scope);
},
};
}
/** @ngInject */
export function metricSegmentModel(uiSegmentSrv, $q) {
return {
template:
'<metric-segment segment="segment" get-options="getOptionsInternal()" on-change="onSegmentChange()"></metric-segment>',
restrict: 'E',
scope: {
property: '=',
options: '=',
getOptions: '&',
onChange: '&',
},
link: {
pre: function postLink($scope, elem, attrs) {
let cachedOptions;
$scope.valueToSegment = function(value) {
let option = _.find($scope.options, { value: value });
let segment = {
cssClass: attrs.cssClass,
custom: attrs.custom,
value: option ? option.text : value,
selectMode: attrs.selectMode,
};
return uiSegmentSrv.newSegment(segment);
};
$scope.getOptionsInternal = function() {
if ($scope.options) {
cachedOptions = $scope.options;
return $q.when(
_.map($scope.options, function(option) {
return { value: option.text };
})
);
} else {
return $scope.getOptions().then(function(options) {
cachedOptions = options;
return _.map(options, function(option) {
if (option.html) {
return option;
}
return { value: option.text };
});
});
}
};
$scope.onSegmentChange = function() {
if (cachedOptions) {
let option = _.find(cachedOptions, { text: $scope.segment.value });
if (option && option.value !== $scope.property) {
$scope.property = option.value;
} else if (attrs.custom !== 'false') {
$scope.property = $scope.segment.value;
}
} else {
$scope.property = $scope.segment.value;
}
// needs to call this after digest so
// property is synced with outerscope
$scope.$$postDigest(function() {
$scope.$apply(function() {
$scope.onChange();
});
});
};
$scope.segment = $scope.valueToSegment($scope.property);
},
},
};
}
coreModule.directive('metricSegment', metricSegment);
coreModule.directive('metricSegmentModel', metricSegmentModel);

View File

@@ -1,283 +0,0 @@
define([
'angular',
'lodash',
'../core_module',
],
function (angular, _, coreModule) {
'use strict';
coreModule.default.controller('ValueSelectDropdownCtrl', function($q) {
var vm = this;
vm.show = function() {
vm.oldVariableText = vm.variable.current.text;
vm.highlightIndex = -1;
vm.options = vm.variable.options;
vm.selectedValues = _.filter(vm.options, {selected: true});
vm.tags = _.map(vm.variable.tags, function(value) {
var tag = { text: value, selected: false };
_.each(vm.variable.current.tags, function(tagObj) {
if (tagObj.text === value) {
tag = tagObj;
}
});
return tag;
});
vm.search = {
query: '',
options: vm.options.slice(0, Math.min(vm.options.length, 1000))
};
vm.dropdownVisible = true;
};
vm.updateLinkText = function() {
var current = vm.variable.current;
if (current.tags && current.tags.length) {
// filer out values that are in selected tags
var selectedAndNotInTag = _.filter(vm.variable.options, function(option) {
if (!option.selected) { return false; }
for (var i = 0; i < current.tags.length; i++) {
var tag = current.tags[i];
if (_.indexOf(tag.values, option.value) !== -1) {
return false;
}
}
return true;
});
// convert values to text
var currentTexts = _.map(selectedAndNotInTag, 'text');
// join texts
vm.linkText = currentTexts.join(' + ');
if (vm.linkText.length > 0) {
vm.linkText += ' + ';
}
} else {
vm.linkText = vm.variable.current.text;
}
};
vm.clearSelections = function() {
_.each(vm.options, function(option) {
option.selected = false;
});
vm.selectionsChanged(false);
};
vm.selectTag = function(tag) {
tag.selected = !tag.selected;
var tagValuesPromise;
if (!tag.values) {
tagValuesPromise = vm.variable.getValuesForTag(tag.text);
} else {
tagValuesPromise = $q.when(tag.values);
}
tagValuesPromise.then(function(values) {
tag.values = values;
tag.valuesText = values.join(' + ');
_.each(vm.options, function(option) {
if (_.indexOf(tag.values, option.value) !== -1) {
option.selected = tag.selected;
}
});
vm.selectionsChanged(false);
});
};
vm.keyDown = function (evt) {
if (evt.keyCode === 27) {
vm.hide();
}
if (evt.keyCode === 40) {
vm.moveHighlight(1);
}
if (evt.keyCode === 38) {
vm.moveHighlight(-1);
}
if (evt.keyCode === 13) {
if (vm.search.options.length === 0) {
vm.commitChanges();
} else {
vm.selectValue(vm.search.options[vm.highlightIndex], {}, true, false);
}
}
if (evt.keyCode === 32) {
vm.selectValue(vm.search.options[vm.highlightIndex], {}, false, false);
}
};
vm.moveHighlight = function(direction) {
vm.highlightIndex = (vm.highlightIndex + direction) % vm.search.options.length;
};
vm.selectValue = function(option, event, commitChange, excludeOthers) {
if (!option) { return; }
option.selected = vm.variable.multi ? !option.selected: true;
commitChange = commitChange || false;
excludeOthers = excludeOthers || false;
var setAllExceptCurrentTo = function(newValue) {
_.each(vm.options, function(other) {
if (option !== other) { other.selected = newValue; }
});
};
// commit action (enter key), should not deselect it
if (commitChange) {
option.selected = true;
}
if (option.text === 'All' || excludeOthers) {
setAllExceptCurrentTo(false);
commitChange = true;
}
else if (!vm.variable.multi) {
setAllExceptCurrentTo(false);
commitChange = true;
} else if (event.ctrlKey || event.metaKey || event.shiftKey) {
commitChange = true;
setAllExceptCurrentTo(false);
}
vm.selectionsChanged(commitChange);
};
vm.selectionsChanged = function(commitChange) {
vm.selectedValues = _.filter(vm.options, {selected: true});
if (vm.selectedValues.length > 1) {
if (vm.selectedValues[0].text === 'All') {
vm.selectedValues[0].selected = false;
vm.selectedValues = vm.selectedValues.slice(1, vm.selectedValues.length);
}
}
// validate selected tags
_.each(vm.tags, function(tag) {
if (tag.selected) {
_.each(tag.values, function(value) {
if (!_.find(vm.selectedValues, {value: value})) {
tag.selected = false;
}
});
}
});
vm.selectedTags = _.filter(vm.tags, {selected: true});
vm.variable.current.value = _.map(vm.selectedValues, 'value');
vm.variable.current.text = _.map(vm.selectedValues, 'text').join(' + ');
vm.variable.current.tags = vm.selectedTags;
if (!vm.variable.multi) {
vm.variable.current.value = vm.selectedValues[0].value;
}
if (commitChange) {
vm.commitChanges();
}
};
vm.commitChanges = function() {
// if we have a search query and no options use that
if (vm.search.options.length === 0 && vm.search.query.length > 0) {
vm.variable.current = {text: vm.search.query, value: vm.search.query};
}
else if (vm.selectedValues.length === 0) {
// make sure one option is selected
vm.options[0].selected = true;
vm.selectionsChanged(false);
}
vm.dropdownVisible = false;
vm.updateLinkText();
if (vm.variable.current.text !== vm.oldVariableText) {
vm.onUpdated();
}
};
vm.queryChanged = function() {
vm.highlightIndex = -1;
vm.search.options = _.filter(vm.options, function(option) {
return option.text.toLowerCase().indexOf(vm.search.query.toLowerCase()) !== -1;
});
vm.search.options = vm.search.options.slice(0, Math.min(vm.search.options.length, 1000));
};
vm.init = function() {
vm.selectedTags = vm.variable.current.tags || [];
vm.updateLinkText();
};
});
coreModule.default.directive('valueSelectDropdown', function($compile, $window, $timeout, $rootScope) {
return {
scope: { variable: "=", onUpdated: "&"},
templateUrl: 'public/app/partials/valueSelectDropdown.html',
controller: 'ValueSelectDropdownCtrl',
controllerAs: 'vm',
bindToController: true,
link: function(scope, elem) {
var bodyEl = angular.element($window.document.body);
var linkEl = elem.find('.variable-value-link');
var inputEl = elem.find('input');
function openDropdown() {
inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
inputEl.show();
linkEl.hide();
inputEl.focus();
$timeout(function() { bodyEl.on('click', bodyOnClick); }, 0, false);
}
function switchToLink() {
inputEl.hide();
linkEl.show();
bodyEl.off('click', bodyOnClick);
}
function bodyOnClick (e) {
if (elem.has(e.target).length === 0) {
scope.$apply(function() {
scope.vm.commitChanges();
});
}
}
scope.$watch('vm.dropdownVisible', function(newValue) {
if (newValue) {
openDropdown();
} else {
switchToLink();
}
});
var cleanUp = $rootScope.$on('template-variable-value-updated', function() {
scope.vm.updateLinkText();
});
scope.$on("$destroy", function() {
cleanUp();
});
scope.vm.init();
},
};
});
});

View File

@@ -0,0 +1,305 @@
import angular from 'angular';
import _ from 'lodash';
import coreModule from '../core_module';
export class ValueSelectDropdownCtrl {
dropdownVisible: any;
highlightIndex: any;
linkText: any;
oldVariableText: any;
options: any;
search: any;
selectedTags: any;
selectedValues: any;
tags: any;
variable: any;
hide: any;
onUpdated: any;
/** @ngInject */
constructor(private $q) {}
show() {
this.oldVariableText = this.variable.current.text;
this.highlightIndex = -1;
this.options = this.variable.options;
this.selectedValues = _.filter(this.options, { selected: true });
this.tags = _.map(this.variable.tags, value => {
let tag = { text: value, selected: false };
_.each(this.variable.current.tags, tagObj => {
if (tagObj.text === value) {
tag = tagObj;
}
});
return tag;
});
this.search = {
query: '',
options: this.options.slice(0, Math.min(this.options.length, 1000)),
};
this.dropdownVisible = true;
}
updateLinkText() {
let current = this.variable.current;
if (current.tags && current.tags.length) {
// filer out values that are in selected tags
let selectedAndNotInTag = _.filter(this.variable.options, option => {
if (!option.selected) {
return false;
}
for (let i = 0; i < current.tags.length; i++) {
let tag = current.tags[i];
if (_.indexOf(tag.values, option.value) !== -1) {
return false;
}
}
return true;
});
// convert values to text
let currentTexts = _.map(selectedAndNotInTag, 'text');
// join texts
this.linkText = currentTexts.join(' + ');
if (this.linkText.length > 0) {
this.linkText += ' + ';
}
} else {
this.linkText = this.variable.current.text;
}
}
clearSelections() {
_.each(this.options, option => {
option.selected = false;
});
this.selectionsChanged(false);
}
selectTag(tag) {
tag.selected = !tag.selected;
let tagValuesPromise;
if (!tag.values) {
tagValuesPromise = this.variable.getValuesForTag(tag.text);
} else {
tagValuesPromise = this.$q.when(tag.values);
}
tagValuesPromise.then(values => {
tag.values = values;
tag.valuesText = values.join(' + ');
_.each(this.options, option => {
if (_.indexOf(tag.values, option.value) !== -1) {
option.selected = tag.selected;
}
});
this.selectionsChanged(false);
});
}
keyDown(evt) {
if (evt.keyCode === 27) {
this.hide();
}
if (evt.keyCode === 40) {
this.moveHighlight(1);
}
if (evt.keyCode === 38) {
this.moveHighlight(-1);
}
if (evt.keyCode === 13) {
if (this.search.options.length === 0) {
this.commitChanges();
} else {
this.selectValue(this.search.options[this.highlightIndex], {}, true, false);
}
}
if (evt.keyCode === 32) {
this.selectValue(this.search.options[this.highlightIndex], {}, false, false);
}
}
moveHighlight(direction) {
this.highlightIndex = (this.highlightIndex + direction) % this.search.options.length;
}
selectValue(option, event, commitChange, excludeOthers) {
if (!option) {
return;
}
option.selected = this.variable.multi ? !option.selected : true;
commitChange = commitChange || false;
excludeOthers = excludeOthers || false;
let setAllExceptCurrentTo = newValue => {
_.each(this.options, other => {
if (option !== other) {
other.selected = newValue;
}
});
};
// commit action (enter key), should not deselect it
if (commitChange) {
option.selected = true;
}
if (option.text === 'All' || excludeOthers) {
setAllExceptCurrentTo(false);
commitChange = true;
} else if (!this.variable.multi) {
setAllExceptCurrentTo(false);
commitChange = true;
} else if (event.ctrlKey || event.metaKey || event.shiftKey) {
commitChange = true;
setAllExceptCurrentTo(false);
}
this.selectionsChanged(commitChange);
}
selectionsChanged(commitChange) {
this.selectedValues = _.filter(this.options, { selected: true });
if (this.selectedValues.length > 1) {
if (this.selectedValues[0].text === 'All') {
this.selectedValues[0].selected = false;
this.selectedValues = this.selectedValues.slice(1, this.selectedValues.length);
}
}
// validate selected tags
_.each(this.tags, tag => {
if (tag.selected) {
_.each(tag.values, value => {
if (!_.find(this.selectedValues, { value: value })) {
tag.selected = false;
}
});
}
});
this.selectedTags = _.filter(this.tags, { selected: true });
this.variable.current.value = _.map(this.selectedValues, 'value');
this.variable.current.text = _.map(this.selectedValues, 'text').join(' + ');
this.variable.current.tags = this.selectedTags;
if (!this.variable.multi) {
this.variable.current.value = this.selectedValues[0].value;
}
if (commitChange) {
this.commitChanges();
}
}
commitChanges() {
// if we have a search query and no options use that
if (this.search.options.length === 0 && this.search.query.length > 0) {
this.variable.current = { text: this.search.query, value: this.search.query };
} else if (this.selectedValues.length === 0) {
// make sure one option is selected
this.options[0].selected = true;
this.selectionsChanged(false);
}
this.dropdownVisible = false;
this.updateLinkText();
if (this.variable.current.text !== this.oldVariableText) {
this.onUpdated();
}
}
queryChanged() {
this.highlightIndex = -1;
this.search.options = _.filter(this.options, option => {
return option.text.toLowerCase().indexOf(this.search.query.toLowerCase()) !== -1;
});
this.search.options = this.search.options.slice(0, Math.min(this.search.options.length, 1000));
}
init() {
this.selectedTags = this.variable.current.tags || [];
this.updateLinkText();
}
}
/** @ngInject */
export function valueSelectDropdown($compile, $window, $timeout, $rootScope) {
return {
scope: { variable: '=', onUpdated: '&' },
templateUrl: 'public/app/partials/valueSelectDropdown.html',
controller: 'ValueSelectDropdownCtrl',
controllerAs: 'vm',
bindToController: true,
link: function(scope, elem) {
let bodyEl = angular.element($window.document.body);
let linkEl = elem.find('.variable-value-link');
let inputEl = elem.find('input');
function openDropdown() {
inputEl.css('width', Math.max(linkEl.width(), 80) + 'px');
inputEl.show();
linkEl.hide();
inputEl.focus();
$timeout(
function() {
bodyEl.on('click', bodyOnClick);
},
0,
false
);
}
function switchToLink() {
inputEl.hide();
linkEl.show();
bodyEl.off('click', bodyOnClick);
}
function bodyOnClick(e) {
if (elem.has(e.target).length === 0) {
scope.$apply(function() {
scope.vm.commitChanges();
});
}
}
scope.$watch('vm.dropdownVisible', newValue => {
if (newValue) {
openDropdown();
} else {
switchToLink();
}
});
let cleanUp = $rootScope.$on('template-variable-value-updated', () => {
scope.vm.updateLinkText();
});
scope.$on('$destroy', () => {
cleanUp();
});
scope.vm.init();
},
};
}
coreModule.controller('ValueSelectDropdownCtrl', ValueSelectDropdownCtrl);
coreModule.directive('valueSelectDropdown', valueSelectDropdown);

View File

@@ -1,13 +0,0 @@
define([
'./alert_srv',
'./util_srv',
'./context_srv',
'./timer',
'./analytics',
'./popover_srv',
'./segment_srv',
'./backend_srv',
'./dynamic_directive_srv',
'./bridge_srv'
],
function () {});

View File

@@ -0,0 +1,10 @@
import './alert_srv';
import './util_srv';
import './context_srv';
import './timer';
import './analytics';
import './popover_srv';
import './segment_srv';
import './backend_srv';
import './dynamic_directive_srv';
import './bridge_srv';

View File

@@ -10,6 +10,7 @@ import 'mousetrap-global-bind';
export class KeybindingSrv {
helpModal: boolean;
modalOpen = false;
timepickerOpen = false;
/** @ngInject */
constructor(private $rootScope, private $location) {
@@ -22,6 +23,8 @@ export class KeybindingSrv {
this.setupGlobal();
appEvents.on('show-modal', () => (this.modalOpen = true));
$rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true));
$rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false));
}
setupGlobal() {
@@ -73,7 +76,12 @@ export class KeybindingSrv {
appEvents.emit('hide-modal');
if (!this.modalOpen) {
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
if (this.timepickerOpen) {
this.$rootScope.appEvent('closeTimepicker');
this.timepickerOpen = false;
} else {
this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false });
}
} else {
this.modalOpen = false;
}

View File

@@ -1,111 +0,0 @@
define([
'angular',
'lodash',
'../core_module',
],
function (angular, _, coreModule) {
'use strict';
coreModule.default.service('uiSegmentSrv', function($sce, templateSrv) {
var self = this;
function MetricSegment(options) {
if (options === '*' || options.value === '*') {
this.value = '*';
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
this.type = options.type;
this.expandable = true;
return;
}
if (_.isString(options)) {
this.value = options;
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
return;
}
// temp hack to work around legacy inconsistency in segment model
this.text = options.value;
this.cssClass = options.cssClass;
this.custom = options.custom;
this.type = options.type;
this.fake = options.fake;
this.value = options.value;
this.selectMode = options.selectMode;
this.type = options.type;
this.expandable = options.expandable;
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
}
this.getSegmentForValue = function(value, fallbackText) {
if (value) {
return this.newSegment(value);
} else {
return this.newSegment({value: fallbackText, fake: true});
}
};
this.newSelectMeasurement = function() {
return new MetricSegment({value: 'select measurement', fake: true});
};
this.newFake = function(text, type, cssClass) {
return new MetricSegment({value: text, fake: true, type: type, cssClass: cssClass});
};
this.newSegment = function(options) {
return new MetricSegment(options);
};
this.newKey = function(key) {
return new MetricSegment({value: key, type: 'key', cssClass: 'query-segment-key' });
};
this.newKeyValue = function(value) {
return new MetricSegment({value: value, type: 'value', cssClass: 'query-segment-value' });
};
this.newCondition = function(condition) {
return new MetricSegment({value: condition, type: 'condition', cssClass: 'query-keyword' });
};
this.newOperator = function(op) {
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
};
this.newOperators = function(ops) {
return _.map(ops, function(op) {
return new MetricSegment({value: op, type: 'operator', cssClass: 'query-segment-operator' });
});
};
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
return function(results) {
var segments = _.map(results, function(segment) {
return self.newSegment({value: segment.text, expandable: segment.expandable});
});
if (addTemplateVars) {
_.each(templateSrv.variables, function(variable) {
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
}
});
}
return segments;
};
};
this.newSelectMetric = function() {
return new MetricSegment({value: 'select metric', fake: true});
};
this.newPlusButton = function() {
return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
};
});
});

View File

@@ -0,0 +1,111 @@
import _ from 'lodash';
import coreModule from '../core_module';
/** @ngInject */
export function uiSegmentSrv($sce, templateSrv) {
let self = this;
function MetricSegment(options) {
if (options === '*' || options.value === '*') {
this.value = '*';
this.html = $sce.trustAsHtml('<i class="fa fa-asterisk"><i>');
this.type = options.type;
this.expandable = true;
return;
}
if (_.isString(options)) {
this.value = options;
this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
return;
}
// temp hack to work around legacy inconsistency in segment model
this.text = options.value;
this.cssClass = options.cssClass;
this.custom = options.custom;
this.type = options.type;
this.fake = options.fake;
this.value = options.value;
this.selectMode = options.selectMode;
this.type = options.type;
this.expandable = options.expandable;
this.html = options.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
}
this.getSegmentForValue = function(value, fallbackText) {
if (value) {
return this.newSegment(value);
} else {
return this.newSegment({ value: fallbackText, fake: true });
}
};
this.newSelectMeasurement = function() {
return new MetricSegment({ value: 'select measurement', fake: true });
};
this.newFake = function(text, type, cssClass) {
return new MetricSegment({ value: text, fake: true, type: type, cssClass: cssClass });
};
this.newSegment = function(options) {
return new MetricSegment(options);
};
this.newKey = function(key) {
return new MetricSegment({ value: key, type: 'key', cssClass: 'query-segment-key' });
};
this.newKeyValue = function(value) {
return new MetricSegment({ value: value, type: 'value', cssClass: 'query-segment-value' });
};
this.newCondition = function(condition) {
return new MetricSegment({ value: condition, type: 'condition', cssClass: 'query-keyword' });
};
this.newOperator = function(op) {
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
};
this.newOperators = function(ops) {
return _.map(ops, function(op) {
return new MetricSegment({ value: op, type: 'operator', cssClass: 'query-segment-operator' });
});
};
this.transformToSegments = function(addTemplateVars, variableTypeFilter) {
return function(results) {
let segments = _.map(results, function(segment) {
return self.newSegment({ value: segment.text, expandable: segment.expandable });
});
if (addTemplateVars) {
_.each(templateSrv.variables, function(variable) {
if (variableTypeFilter === void 0 || variableTypeFilter === variable.type) {
segments.unshift(self.newSegment({ type: 'value', value: '$' + variable.name, expandable: true }));
}
});
}
return segments;
};
};
this.newSelectMetric = function() {
return new MetricSegment({ value: 'select metric', fake: true });
};
this.newPlusButton = function() {
return new MetricSegment({
fake: true,
html: '<i class="fa fa-plus "></i>',
type: 'plus-button',
cssClass: 'query-part',
});
};
}
coreModule.service('uiSegmentSrv', uiSegmentSrv);

View File

@@ -67,7 +67,7 @@ export function fetch(load): any {
return '';
}
// dont reload styles loaded in the head
// don't reload styles loaded in the head
for (var i = 0; i < linkHrefs.length; i++) {
if (load.address === linkHrefs[i]) {
return '';

View File

@@ -620,13 +620,13 @@ kbn.valueFormats.ms = function(size, decimals, scaledDecimals) {
// Less than 1 min
return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s');
} else if (Math.abs(size) < 3600000) {
// Less than 1 hour, devide in minutes
// Less than 1 hour, divide in minutes
return kbn.toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min');
} else if (Math.abs(size) < 86400000) {
// Less than one day, devide in hours
// Less than one day, divide in hours
return kbn.toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour');
} else if (Math.abs(size) < 31536000000) {
// Less than one year, devide in days
// Less than one year, divide in days
return kbn.toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day');
}
@@ -638,15 +638,15 @@ kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
return '';
}
// Less than 1 µs, devide in ns
// Less than 1 µs, divide in ns
if (Math.abs(size) < 0.000001) {
return kbn.toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns');
}
// Less than 1 ms, devide in µs
// Less than 1 ms, divide in µs
if (Math.abs(size) < 0.001) {
return kbn.toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs');
}
// Less than 1 second, devide in ms
// Less than 1 second, divide in ms
if (Math.abs(size) < 1) {
return kbn.toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms');
}
@@ -654,16 +654,16 @@ kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
if (Math.abs(size) < 60) {
return kbn.toFixed(size, decimals) + ' s';
} else if (Math.abs(size) < 3600) {
// Less than 1 hour, devide in minutes
// Less than 1 hour, divide in minutes
return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min');
} else if (Math.abs(size) < 86400) {
// Less than one day, devide in hours
// Less than one day, divide in hours
return kbn.toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour');
} else if (Math.abs(size) < 604800) {
// Less than one week, devide in days
// Less than one week, divide in days
return kbn.toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day');
} else if (Math.abs(size) < 31536000) {
// Less than one year, devide in week
// Less than one year, divide in week
return kbn.toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week');
}

View File

@@ -124,7 +124,7 @@ function joinEvalMatches(matches, separator: string) {
}
function getAlertAnnotationInfo(ah) {
// backward compatability, can be removed in grafana 5.x
// backward compatibility, can be removed in grafana 5.x
// old way stored evalMatches in data property directly,
// new way stores it in evalMatches property on new data object

View File

@@ -4,7 +4,7 @@ import { ThresholdMapper } from '../threshold_mapper';
describe('ThresholdMapper', () => {
describe('with greater than evaluator', () => {
it('can mapp query conditions to thresholds', () => {
it('can map query conditions to thresholds', () => {
var panel: any = {
type: 'graph',
alert: {
@@ -25,7 +25,7 @@ describe('ThresholdMapper', () => {
});
describe('with outside range evaluator', () => {
it('can mapp query conditions to thresholds', () => {
it('can map query conditions to thresholds', () => {
var panel: any = {
type: 'graph',
alert: {
@@ -49,7 +49,7 @@ describe('ThresholdMapper', () => {
});
describe('with inside range evaluator', () => {
it('can mapp query conditions to thresholds', () => {
it('can map query conditions to thresholds', () => {
var panel: any = {
type: 'graph',
alert: {

View File

@@ -1,9 +1,5 @@
export class ThresholdMapper {
static alertToGraphThresholds(panel) {
if (panel.type !== 'graph') {
return false;
}
for (var i = 0; i < panel.alert.conditions.length; i++) {
let condition = panel.alert.conditions[i];
if (condition.type !== 'query') {

View File

@@ -1,15 +0,0 @@
define([
'./panellinks/module',
'./dashlinks/module',
'./annotations/all',
'./templating/all',
'./plugins/all',
'./dashboard/all',
'./playlist/all',
'./snapshot/all',
'./panel/all',
'./org/all',
'./admin/admin',
'./alerting/all',
'./styleguide/styleguide',
], function () {});

View File

@@ -0,0 +1,13 @@
import './panellinks/module';
import './dashlinks/module';
import './annotations/all';
import './templating/all';
import './plugins/all';
import './dashboard/all';
import './playlist/all';
import './snapshot/all';
import './panel/all';
import './org/all';
import './admin/admin';
import './alerting/all';
import './styleguide/styleguide';

View File

@@ -56,7 +56,7 @@ function isStartOfRegion(event): boolean {
export function dedupAnnotations(annotations) {
let dedup = [];
// Split events by annotationId property existance
// Split events by annotationId property existence
let events = _.partition(annotations, 'id');
let eventsById = _.groupBy(events[0], 'id');

View File

@@ -129,7 +129,7 @@ export class DashboardModel {
this.meta = meta;
}
// cleans meta data and other non peristent state
// cleans meta data and other non persistent state
getSaveModelClone() {
// make clone
var copy: any = {};
@@ -606,7 +606,7 @@ export class DashboardModel {
if (panel.gridPos.x + panel.gridPos.w * 2 <= GRID_COLUMN_COUNT) {
newPanel.gridPos.x += panel.gridPos.w;
} else {
// add bellow
// add below
newPanel.gridPos.y += panel.gridPos.h;
}

View File

@@ -103,7 +103,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
render() {
return (
<div className="panel-container">
<div className="panel-container add-panel-container">
<div className="add-panel">
<div className="add-panel__header">
<i className="gicon gicon-add-panel" />

View File

@@ -133,7 +133,7 @@ export class HistoryListCtrl {
return this.historySrv
.getHistoryList(this.dashboard, options)
.then(revisions => {
// set formated dates & default values
// set formatted dates & default values
for (let rev of revisions) {
rev.createdDateString = this.formatDate(rev.created);
rev.ageString = this.formatBasicDate(rev.created);

View File

@@ -56,7 +56,7 @@ describe('DashboardImportCtrl', function() {
});
});
describe('when specifing grafana.com url', function() {
describe('when specifying grafana.com url', function() {
beforeEach(function() {
ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123';
// setup api mock
@@ -73,7 +73,7 @@ describe('DashboardImportCtrl', function() {
});
});
describe('when specifing dashbord id', function() {
describe('when specifying dashboard id', function() {
beforeEach(function() {
ctx.ctrl.gnetUrl = '2342';
// setup api mock

View File

@@ -44,7 +44,7 @@ describe('timeSrv', function() {
expect(time.raw.to).to.be('now');
});
it('should handle formated dates', function() {
it('should handle formatted dates', function() {
ctx.$location.search({ from: '20140410T052010', to: '20140520T031022' });
ctx.service.init(_dashboard);
var time = ctx.service.timeRange(true);
@@ -52,7 +52,7 @@ describe('timeSrv', function() {
expect(time.to.valueOf()).to.equal(new Date('2014-05-20T03:10:22Z').getTime());
});
it('should handle formated dates without time', function() {
it('should handle formatted dates without time', function() {
ctx.$location.search({ from: '20140410', to: '20140520' });
ctx.service.init(_dashboard);
var time = ctx.service.timeRange(true);

View File

@@ -22,7 +22,6 @@ export class TimePickerCtrl {
refresh: any;
isUtc: boolean;
firstDayOfWeek: number;
closeDropdown: any;
isOpen: boolean;
/** @ngInject */
@@ -32,6 +31,7 @@ export class TimePickerCtrl {
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
$rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
$rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
// init options
this.panel = this.dashboard.timepicker;
@@ -96,7 +96,7 @@ export class TimePickerCtrl {
openDropdown() {
if (this.isOpen) {
this.isOpen = false;
this.closeDropdown();
return;
}
@@ -112,6 +112,12 @@ export class TimePickerCtrl {
this.refresh.options.unshift({ text: 'off' });
this.isOpen = true;
this.$rootScope.appEvent('timepickerOpen');
}
closeDropdown() {
this.isOpen = false;
this.$rootScope.appEvent('timepickerClosed');
}
applyCustom() {
@@ -120,7 +126,7 @@ export class TimePickerCtrl {
}
this.timeSrv.setTime(this.editTimeRaw);
this.isOpen = false;
this.closeDropdown();
}
absoluteFromChanged() {
@@ -143,7 +149,7 @@ export class TimePickerCtrl {
}
this.timeSrv.setTime(range);
this.isOpen = false;
this.closeDropdown();
}
}

View File

@@ -35,12 +35,12 @@ export class Tracker {
$window.onbeforeunload = () => {
if (this.ignoreChanges()) {
return null;
return undefined;
}
if (this.hasChanges()) {
return 'There are unsaved changes to this dashboard';
}
return null;
return undefined;
};
scope.$on('$locationChangeStart', (event, next) => {

View File

@@ -38,7 +38,7 @@ export class DashboardViewState {
});
// this marks changes to location during this digest cycle as not to add history item
// dont want url changes like adding orgId to add browser history
// don't want url changes like adding orgId to add browser history
$location.replace();
this.update(this.getQueryStringState());
}
@@ -196,9 +196,10 @@ export class DashboardViewState {
this.oldTimeRange = ctrl.range;
this.fullscreenPanel = panelScope;
// Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id });
this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
}
registerPanel(panelScope) {

View File

@@ -5,7 +5,7 @@
New Organization
</h2>
<p class="playlist-description">Each organization contains their own dashboards, data sources and configuration, and cannot be shared between orgs. While users may belong to more than one, mutiple organization are most frequently used in multi-tenant deployments. </p>
<p class="playlist-description">Each organization contains their own dashboards, data sources and configuration, and cannot be shared between orgs. While users may belong to more than one, multiple organization are most frequently used in multi-tenant deployments. </p>
<form>
<div class="gf-form-group">

View File

@@ -1,9 +0,0 @@
define([
'./panel_header',
'./panel_directive',
'./solo_panel_ctrl',
'./query_ctrl',
'./panel_editor_tab',
'./query_editor_row',
'./query_troubleshooter',
], function () {});

View File

@@ -0,0 +1,7 @@
import './panel_header';
import './panel_directive';
import './solo_panel_ctrl';
import './query_ctrl';
import './panel_editor_tab';
import './query_editor_row';
import './query_troubleshooter';

View File

@@ -73,7 +73,7 @@ class MetricsPanelCtrl extends PanelCtrl {
if (this.panel.snapshotData) {
this.updateTimeRange();
var data = this.panel.snapshotData;
// backward compatability
// backward compatibility
if (!_.isArray(data)) {
data = data.data;
}

View File

@@ -1,6 +1,7 @@
import angular from 'angular';
import $ from 'jquery';
import Drop from 'tether-drop';
import PerfectScrollbar from 'perfect-scrollbar';
import baron from 'baron';
var module = angular.module('grafana.directives');
@@ -86,6 +87,9 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
function panelHeightUpdated() {
panelContent.css({ height: ctrl.height + 'px' });
}
function resizeScrollableContent() {
if (panelScrollbar) {
panelScrollbar.update();
}
@@ -100,9 +104,30 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
// update scrollbar after mounting
ctrl.events.on('component-did-mount', () => {
if (ctrl.__proto__.constructor.scrollable) {
panelScrollbar = new PerfectScrollbar(panelContent[0], {
wheelPropagation: true,
const scrollRootClass = 'baron baron__root baron__clipper panel-content--scrollable';
const scrollerClass = 'baron__scroller';
const scrollBarHTML = `
<div class="baron__track">
<div class="baron__bar"></div>
</div>
`;
let scrollRoot = panelContent;
let scroller = panelContent.find(':first').find(':first');
scrollRoot.addClass(scrollRootClass);
$(scrollBarHTML).appendTo(scrollRoot);
scroller.addClass(scrollerClass);
panelScrollbar = baron({
root: scrollRoot[0],
scroller: scroller[0],
bar: '.baron__bar',
barOnCls: '_scrollbar',
scrollingCls: '_scrolling',
});
panelScrollbar.scroll();
}
});
@@ -110,6 +135,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
ctrl.calculatePanelHeight();
panelHeightUpdated();
$timeout(() => {
resizeScrollableContent();
ctrl.render();
});
});
@@ -199,7 +225,7 @@ module.directive('grafanaPanel', function($rootScope, $document, $timeout) {
}
if (panelScrollbar) {
panelScrollbar.update();
panelScrollbar.dispose();
}
});
},

View File

@@ -1,7 +0,0 @@
define([
'./playlists_ctrl',
'./playlist_search',
'./playlist_srv',
'./playlist_edit_ctrl',
'./playlist_routes'
], function () {});

View File

@@ -0,0 +1,5 @@
import './playlists_ctrl';
import './playlist_search';
import './playlist_srv';
import './playlist_edit_ctrl';
import './playlist_routes';

View File

@@ -1,39 +0,0 @@
define([
'angular',
'lodash'
],
function (angular) {
'use strict';
var module = angular.module('grafana.routes');
module.config(function($routeProvider) {
$routeProvider
.when('/playlists', {
templateUrl: 'public/app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller : 'PlaylistsCtrl'
})
.when('/playlists/create', {
templateUrl: 'public/app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller : 'PlaylistEditCtrl'
})
.when('/playlists/edit/:id', {
templateUrl: 'public/app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller : 'PlaylistEditCtrl'
})
.when('/playlists/play/:id', {
templateUrl: 'public/app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller : 'PlaylistsCtrl',
resolve: {
init: function(playlistSrv, $route) {
var playlistId = $route.current.params.id;
playlistSrv.start(playlistId);
}
}
});
});
});

View File

@@ -0,0 +1,34 @@
import angular from 'angular';
/** @ngInject */
function grafanaRoutes($routeProvider) {
$routeProvider
.when('/playlists', {
templateUrl: 'public/app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller: 'PlaylistsCtrl',
})
.when('/playlists/create', {
templateUrl: 'public/app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller: 'PlaylistEditCtrl',
})
.when('/playlists/edit/:id', {
templateUrl: 'public/app/features/playlist/partials/playlist.html',
controllerAs: 'ctrl',
controller: 'PlaylistEditCtrl',
})
.when('/playlists/play/:id', {
templateUrl: 'public/app/features/playlist/partials/playlists.html',
controllerAs: 'ctrl',
controller: 'PlaylistsCtrl',
resolve: {
init: function(playlistSrv, $route) {
let playlistId = $route.current.params.id;
playlistSrv.start(playlistId);
},
},
});
}
angular.module('grafana.routes').config(grafanaRoutes);

View File

@@ -1,5 +1,3 @@
<div class="gf-form-group">
<h3 class="page-heading">HTTP</h3>
<div class="gf-form-group">
@@ -13,12 +11,12 @@
<info-popover mode="right-absolute">
<p>Specify a complete HTTP URL (for example http://your_server:8080)</p>
<span ng-show="current.access === 'direct'">
Your access method is <em>Direct</em>, this means the URL
Your access method is <em>Browser</em>, this means the URL
needs to be accessible from the browser.
</span>
<span ng-show="current.access === 'proxy'">
Your access method is currently <em>Proxy</em>, this means the URL
needs to be accessible from the grafana backend.
Your access method is <em>Server</em>, this means the URL
needs to be accessible from the grafana backend/server.
</span>
</info-popover>
</div>
@@ -27,14 +25,38 @@
<div class="gf-form-inline">
<div class="gf-form max-width-30">
<span class="gf-form-label width-7">Access</span>
<div class="gf-form-select-wrapper gf-form-select-wrapper--has-help-icon max-width-24">
<select class="gf-form-input" ng-model="current.access" ng-options="f for f in ['direct', 'proxy']"></select>
<info-popover mode="right-absolute">
Direct = URL is used directly from browser<br>
Proxy = Grafana backend will proxy the request
</info-popover>
<div class="gf-form-select-wrapper max-width-24">
<select class="gf-form-input" ng-model="current.access" ng-options="f.key as f.value for f in [{key: 'proxy', value: 'Server (Default)'}, { key: 'direct', value: 'Browser'}]"></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword pointer" ng-click="ctrl.showAccessHelp = !ctrl.showAccessHelp">
Help&nbsp;
<i class="fa fa-caret-down" ng-show="ctrl.showAccessHelp"></i>
<i class="fa fa-caret-right" ng-hide="ctrl.showAccessHelp">&nbsp;</i>
</label>
</div>
</div>
<div class="alert alert-info" ng-show="ctrl.showAccessHelp">
<div class="alert-body">
<p>
Access mode controls how requests to the data source will be handled.
<strong><i>Server</i></strong> should be the preferred way if nothing else stated.
</p>
<div class="alert-title">Server access mode (Default):</div>
<p>
All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to the data source
and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements.
The URL needs to be accessible from the grafana backend/server if you select this access mode.
</p>
<div class="alert-title">Browser access mode:</div>
<p>
All requests will be made from the browser directly to the data source and may be subject to
Cross-Origin Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this
access mode.
</p>
</div>
</div>
</div>
@@ -135,4 +157,3 @@
</div>
</div>
</div>

View File

@@ -29,7 +29,7 @@ export class DatasourceVariable implements Variable {
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
// dont persist options
// don't persist options
this.model.options = [];
return this.model;
}

View File

@@ -23,6 +23,8 @@ export class VariableEditorCtrl {
{ value: 2, text: 'Alphabetical (desc)' },
{ value: 3, text: 'Numerical (asc)' },
{ value: 4, text: 'Numerical (desc)' },
{ value: 5, text: 'Alphabetical (case-insensitive, asc)' },
{ value: 6, text: 'Alphabetical (case-insensitive, desc)' },
];
$scope.hideOptions = [{ value: 0, text: '' }, { value: 1, text: 'Label' }, { value: 2, text: 'Variable' }];

View File

@@ -197,6 +197,10 @@ export class QueryVariable implements Variable {
return parseInt(matches[1], 10);
}
});
} else if (sortType === 3) {
options = _.sortBy(options, opt => {
return _.toLower(opt.text);
});
}
if (reverseSort) {

View File

@@ -2,7 +2,7 @@ import { AdhocVariable } from '../adhoc_variable';
describe('AdhocVariable', function() {
describe('when serializing to url', function() {
it('should set return key value and op seperated by pipe', function() {
it('should set return key value and op separated by pipe', function() {
var variable = new AdhocVariable({
filters: [
{ key: 'key1', operator: '=', value: 'value1' },

View File

@@ -40,11 +40,11 @@ describe('QueryVariable', () => {
});
describe('can convert and sort metric names', () => {
var variable = new QueryVariable({}, null, null, null, null);
variable.sort = 3; // Numerical (asc)
const variable = new QueryVariable({}, null, null, null, null);
let input;
describe('can sort a mixed array of metric variables', () => {
var input = [
beforeEach(() => {
input = [
{ text: '0', value: '0' },
{ text: '1', value: '1' },
{ text: null, value: 3 },
@@ -58,11 +58,18 @@ describe('QueryVariable', () => {
{ text: '', value: undefined },
{ text: undefined, value: '' },
];
});
describe('can sort a mixed array of metric variables in numeric order', () => {
let result;
beforeEach(() => {
variable.sort = 3; // Numerical (asc)
result = variable.metricNamesToVariableValues(input);
});
var result = variable.metricNamesToVariableValues(input);
it('should return in same order', () => {
var i = 0;
expect(result.length).toBe(11);
expect(result[i++].text).toBe('');
expect(result[i++].text).toBe('0');
@@ -73,5 +80,27 @@ describe('QueryVariable', () => {
expect(result[i++].text).toBe('6');
});
});
describe('can sort a mixed array of metric variables in alphabetical order', () => {
let result;
beforeEach(() => {
variable.sort = 5; // Alphabetical CI (asc)
result = variable.metricNamesToVariableValues(input);
});
it('should return in same order', () => {
var i = 0;
console.log(result);
expect(result.length).toBe(11);
expect(result[i++].text).toBe('');
expect(result[i++].text).toBe('0');
expect(result[i++].text).toBe('1');
expect(result[i++].text).toBe('10');
expect(result[i++].text).toBe('3');
expect(result[i++].text).toBe('4');
expect(result[i++].text).toBe('5');
});
});
});
});

View File

@@ -282,7 +282,7 @@ describe('templateSrv', function() {
});
});
describe('can hightlight variables in string', function() {
describe('can highlight variables in string', function() {
beforeEach(function() {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
});

View File

@@ -204,7 +204,7 @@ export class TemplateSrv {
value = variable.current.value;
if (this.isAllValue(value)) {
value = this.getAllValue(variable);
// skip formating of custom all values
// skip formatting of custom all values
if (variable.allValue) {
return value;
}

View File

@@ -1,18 +1,18 @@
<div dash-class ng-if="ctrl.dashboard">
<dashnav dashboard="ctrl.dashboard"></dashnav>
<div class="scroll-canvas scroll-canvas--dashboard" grafana-scrollbar>
<dashboard-settings dashboard="ctrl.dashboard"
ng-if="ctrl.dashboardViewState.state.editview"
class="dashboard-settings">
</dashboard-settings>
<div class="scroll-canvas scroll-canvas--dashboard" page-scrollbar>
<dashboard-settings dashboard="ctrl.dashboard"
ng-if="ctrl.dashboardViewState.state.editview"
class="dashboard-settings">
</dashboard-settings>
<div class="dashboard-container">
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
</dashboard-submenu>
<div class="dashboard-container">
<dashboard-submenu ng-if="ctrl.dashboard.meta.submenuEnabled" dashboard="ctrl.dashboard">
</dashboard-submenu>
<dashboard-grid get-panel-container="ctrl.getPanelContainer">
</dashboard-grid>
</div>
</div>
<dashboard-grid get-panel-container="ctrl.getPanelContainer">
</dashboard-grid>
</div>
</div>
</div>

View File

@@ -392,7 +392,7 @@
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "2 yaxis and axis lables",
"title": "2 yaxis and axis labels",
"tooltip": {
"msResolution": false,
"shared": true,
@@ -894,7 +894,7 @@
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Legend Table Single Series Should Take Minium Height",
"title": "Legend Table Single Series Should Take Minimum Height",
"tooltip": {
"shared": true,
"sort": 0,

View File

@@ -175,7 +175,7 @@ export class ElasticResponse {
}
// This is quite complex
// neeed to recurise down the nested buckets to build series
// need to recurise down the nested buckets to build series
processBuckets(aggs, target, seriesList, table, props, depth) {
var bucket, aggDef, esAgg, aggId;
var maxDepth = target.bucketAggs.length - 1;

View File

@@ -27,7 +27,7 @@
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.tagsField' placeholder="tags"></input>
</div>
<div class="gf-form" ng-show="ctrl.annotation.titleField">
<span class="gf-form-label">Title <em class="muted">(depricated)</em></span>
<span class="gf-form-label">Title <em class="muted">(deprecated)</em></span>
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.titleField' placeholder="desc"></input>
</div>
</div>

View File

@@ -35,7 +35,7 @@
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-9">Min interval</span>
<span class="gf-form-label width-9">Min time interval</span>
<input type="text" class="gf-form-input width-6" ng-model="ctrl.current.jsonData.timeInterval" spellcheck='false' placeholder="10s"></input>
<info-popover mode="right-absolute">
A lower limit for the auto group by time interval. Recommended to be set to write frequency,

View File

@@ -53,7 +53,7 @@ describe('ElasticDatasource', function() {
});
});
describe('When issueing metric query with interval pattern', function() {
describe('When issuing metric query with interval pattern', function() {
var requestOptions, parts, header;
beforeEach(function() {
@@ -98,7 +98,7 @@ describe('ElasticDatasource', function() {
});
});
describe('When issueing document query', function() {
describe('When issuing document query', function() {
var requestOptions, parts, header;
beforeEach(function() {

View File

@@ -4,6 +4,7 @@ import $ from 'jquery';
import rst2html from 'rst2html';
import Drop from 'tether-drop';
/** @ngInject */
export function graphiteAddFunc($compile) {
const inputTemplate =
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
@@ -67,7 +68,7 @@ export function graphiteAddFunc($compile) {
});
$input.blur(function() {
// clicking the function dropdown menu wont
// clicking the function dropdown menu won't
// work if you remove class at once
setTimeout(function() {
$input.val('');

View File

@@ -3,6 +3,7 @@ import _ from 'lodash';
import $ from 'jquery';
import rst2html from 'rst2html';
/** @ngInject */
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
const paramTemplate =

View File

@@ -97,7 +97,7 @@ describe('GraphiteQueryCtrl', function() {
});
});
describe('when initalizing target without metric expression and only function', function() {
describe('when initializing target without metric expression and only function', function() {
beforeEach(function() {
ctx.ctrl.target.target = 'asPercent(#A, #B)';
ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
@@ -130,7 +130,7 @@ describe('GraphiteQueryCtrl', function() {
});
});
describe('when initalizing target without metric expression and function with series-ref', function() {
describe('when initializing target without metric expression and function with series-ref', function() {
beforeEach(function() {
ctx.ctrl.target.target = 'asPercent(metric.node.count, #A)';
ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));
@@ -146,7 +146,7 @@ describe('GraphiteQueryCtrl', function() {
});
});
describe('when getting altSegments and metricFindQuery retuns empty array', function() {
describe('when getting altSegments and metricFindQuery returns empty array', function() {
beforeEach(function() {
ctx.ctrl.target.target = 'test.count';
ctx.ctrl.datasource.metricFindQuery.returns(ctx.$q.when([]));

View File

@@ -54,7 +54,7 @@ export default class InfluxDatasource {
queryTargets.push(target);
// backward compatability
// backward compatibility
scopedVars.interval = scopedVars.__interval;
queryModel = new InfluxQuery(target, this.templateSrv, scopedVars);
@@ -82,7 +82,7 @@ export default class InfluxDatasource {
// replace templated variables
allQueries = this.templateSrv.replace(allQueries, scopedVars);
return this._seriesQuery(allQueries).then((data): any => {
return this._seriesQuery(allQueries, options).then((data): any => {
if (!data || !data.results) {
return [];
}
@@ -135,7 +135,7 @@ export default class InfluxDatasource {
var query = options.annotation.query.replace('$timeFilter', timeFilter);
query = this.templateSrv.replace(query, null, 'regex');
return this._seriesQuery(query).then(data => {
return this._seriesQuery(query, options).then(data => {
if (!data || !data.results || !data.results[0]) {
throw { message: 'No results in response from InfluxDB' };
}
@@ -164,30 +164,30 @@ export default class InfluxDatasource {
return false;
}
metricFindQuery(query) {
metricFindQuery(query: string, options?: any) {
var interpolated = this.templateSrv.replace(query, null, 'regex');
return this._seriesQuery(interpolated).then(_.curry(this.responseParser.parse)(query));
return this._seriesQuery(interpolated, options).then(_.curry(this.responseParser.parse)(query));
}
getTagKeys(options) {
var queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
var query = queryBuilder.buildExploreQuery('TAG_KEYS');
return this.metricFindQuery(query);
return this.metricFindQuery(query, options);
}
getTagValues(options) {
var queryBuilder = new InfluxQueryBuilder({ measurement: '', tags: [] }, this.database);
var query = queryBuilder.buildExploreQuery('TAG_VALUES', options.key);
return this.metricFindQuery(query);
return this.metricFindQuery(query, options);
}
_seriesQuery(query) {
_seriesQuery(query: string, options?: any) {
if (!query) {
return this.$q.when({ results: [] });
}
return this._influxRequest('GET', '/query', { q: query, epoch: 'ms' });
return this._influxRequest('GET', '/query', { q: query, epoch: 'ms' }, options);
}
serializeParams(params) {
@@ -225,21 +225,21 @@ export default class InfluxDatasource {
});
}
_influxRequest(method, url, data) {
var self = this;
_influxRequest(method: string, url: string, data: any, options?: any) {
const currentUrl = this.urls.shift();
this.urls.push(currentUrl);
var currentUrl = self.urls.shift();
self.urls.push(currentUrl);
let params: any = {};
var params: any = {};
if (self.username) {
params.u = self.username;
params.p = self.password;
if (this.username) {
params.u = this.username;
params.p = this.password;
}
if (self.database) {
params.db = self.database;
if (options && options.database) {
params.db = options.database;
} else if (this.database) {
params.db = this.database;
}
if (method === 'GET') {
@@ -247,7 +247,7 @@ export default class InfluxDatasource {
data = null;
}
var options: any = {
let req: any = {
method: method,
url: currentUrl + url,
params: params,
@@ -257,15 +257,15 @@ export default class InfluxDatasource {
paramSerializer: this.serializeParams,
};
options.headers = options.headers || {};
req.headers = req.headers || {};
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
req.withCredentials = true;
}
if (self.basicAuth) {
options.headers.Authorization = self.basicAuth;
if (this.basicAuth) {
req.headers.Authorization = this.basicAuth;
}
return this.backendSrv.datasourceRequest(options).then(
return this.backendSrv.datasourceRequest(req).then(
result => {
return result.data;
},

View File

@@ -230,7 +230,7 @@ export default class InfluxQuery {
for (i = 0; i < this.groupByParts.length; i++) {
var part = this.groupByParts[i];
if (i > 0) {
// for some reason fill has no seperator
// for some reason fill has no separator
groupBySection += part.def.type === 'fill' ? ' ' : ', ';
}
groupBySection += part.render('');

View File

@@ -17,7 +17,7 @@
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.tagsColumn' placeholder=""></input>
</div>
<div class="gf-form" ng-show="ctrl.annotation.titleColumn">
<span class="gf-form-label width-4">Title <em class="muted">(depricated)</em></span>
<span class="gf-form-label width-4">Title <em class="muted">(deprecated)</em></span>
<input type="text" class="gf-form-input max-width-10" ng-model='ctrl.annotation.titleColumn' placeholder=""></input>
</div>
</div>

View File

@@ -23,6 +23,20 @@
</div>
</div>
<div class="gf-form-group">
<div class="grafana-info-box">
<h5>Database Access</h5>
<p>
Setting the database for this datasource does not deny access to other databases. The InfluxDB query syntax allows
switching the database in the query. For example:
<code>SHOW MEASUREMENTS ON _internal</code> or <code>SELECT * FROM "_internal".."database" LIMIT 10</code>
<br/><br/>
To support data isolation and security, make sure appropriate permissions are configured in InfluxDB.
</p>
</div>
</div>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">

View File

@@ -10,7 +10,7 @@
- When stacking is enabled it is important that points align
- If there are missing points for one series it can cause gaps or missing bars
- You must use fill(0), and select a group by time low limit
- Use the group by time option below your queries and specify for example &gt;10s if your metrics are written every 10 seconds
- Use the group by time option below your queries and specify for example 10s if your metrics are written every 10 seconds
- This will insert zeros for series that are missing measurements and will make stacking work properly
#### Group by time
@@ -18,8 +18,7 @@
- Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
- If you use fill(0) or fill(null) set a low limit for the auto group by time interval
- The low limit can only be set in the group by time option below your queries
- You set a low limit by adding a greater sign before the interval
- Example: &gt;60s if you write metrics to InfluxDB every 60 seconds
- Example: 60s if you write metrics to InfluxDB every 60 seconds
#### Documentation links:

View File

@@ -97,7 +97,7 @@ describe('InfluxQueryBuilder', function() {
expect(query).toBe('SHOW TAG VALUES FROM "one_week"."cpu" WITH KEY = "app" WHERE "host" = \'server1\'');
});
it('should not includ policy when policy is default', function() {
it('should not include policy when policy is default', function() {
var builder = new InfluxQueryBuilder({
measurement: 'cpu',
policy: 'default',

View File

@@ -18,7 +18,7 @@
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info"><h6>Annotation Query Format</h6>
An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the <b>time</b> column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
An annotation is an event that is overlaid on top of graphs. The query can have up to three columns per row, the <b>time</b> column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time</b> for the annotation event time. Use epoch time or any native date data type.
- column with alias: <b>text</b> for the annotation text.
@@ -28,7 +28,7 @@ An annotation is an event that is overlayed on top of graphs. The query can have
Macros:
- $__time(column) -&gt; column AS time
- $__timeEpoch(column) -&gt; DATEDIFF(second, '1970-01-01', column) AS time
- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &lt;= DATEADD(s, 18446744066914187038, '1970-01-01')
- $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
Or build your own conditionals using these macros which just return the values:

View File

@@ -49,7 +49,7 @@ Table:
Macros:
- $__time(column) -&gt; column AS time
- $__timeEpoch(column) -&gt; DATEDIFF(second, '1970-01-01', column) AS time
- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
- $__timeFilter(column) -&gt; column &gt;= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &lt;= DATEADD(s, 18446744066914187038, '1970-01-01')
- $__unixEpochFilter(column) -&gt; column &gt;= 1492750877 AND column &lt;= 1492750877
- $__timeGroup(column, '5m'[, fillvalue]) -&gt; CAST(ROUND(DATEDIFF(second, '1970-01-01', column)/300.0, 0) as bigint)*300. Providing a <i>fillValue</i> of <i>NULL</i> or floating value will automatically fill empty series in timerange with that value.

View File

@@ -18,7 +18,7 @@
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info"><h6>Annotation Query Format</h6>
An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the <i>time</i> or <i>time_sec</i> column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
An annotation is an event that is overlaid on top of graphs. The query can have up to three columns per row, the <i>time</i> or <i>time_sec</i> column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time</b> or <i>time_sec</i> for the annotation event time. Use epoch time or any native date data type.
- column with alias: <b>text</b> for the annotation text

View File

@@ -3,7 +3,7 @@
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="432.071pt" height="445.383pt" viewBox="0 0 432.071 445.383" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
<g id="orginal" style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
<g id="original" style="fill-rule:nonzero;clip-rule:nonzero;stroke:#000000;stroke-miterlimit:4;">
</g>
<g id="Layer_x0020_3" style="fill-rule:nonzero;clip-rule:nonzero;fill:none;stroke:#FFFFFF;stroke-width:12.4651;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;">
<path style="fill:#000000;stroke:#000000;stroke-width:37.3953;stroke-linecap:butt;stroke-linejoin:miter;" d="M323.205,324.227c2.833-23.601,1.984-27.062,19.563-23.239l4.463,0.392c13.517,0.615,31.199-2.174,41.587-7c22.362-10.376,35.622-27.7,13.572-23.148c-50.297,10.376-53.755-6.655-53.755-6.655c53.111-78.803,75.313-178.836,56.149-203.322 C352.514-5.534,262.036,26.049,260.522,26.869l-0.482,0.089c-9.938-2.062-21.06-3.294-33.554-3.496c-22.761-0.374-40.032,5.967-53.133,15.904c0,0-161.408-66.498-153.899,83.628c1.597,31.936,45.777,241.655,98.47,178.31 c19.259-23.163,37.871-42.748,37.871-42.748c9.242,6.14,20.307,9.272,31.912,8.147l0.897-0.765c-0.281,2.876-0.157,5.689,0.359,9.019c-13.572,15.167-9.584,17.83-36.723,23.416c-27.457,5.659-11.326,15.734-0.797,18.367c12.768,3.193,42.305,7.716,62.268-20.224 l-0.795,3.188c5.325,4.26,4.965,30.619,5.72,49.452c0.756,18.834,2.017,36.409,5.856,46.771c3.839,10.36,8.369,37.05,44.036,29.406c29.809-6.388,52.6-15.582,54.677-101.107"/>
@@ -19,4 +19,4 @@
<path d="M350.676,123.432c0.863,15.994-3.445,26.888-3.988,43.914c-0.804,24.748,11.799,53.074-7.191,81.435"/>
<path style="stroke-width:3;" d="M0,60.232"/>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -18,7 +18,7 @@
<div class="gf-form" ng-show="ctrl.showHelp">
<pre class="gf-form-pre alert alert-info"><h6>Annotation Query Format</h6>
An annotation is an event that is overlayed on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
An annotation is an event that is overlaid on top of graphs. The query can have up to three columns per row, the time column is mandatory. Annotation rendering is expensive so it is important to limit the number of rows returned.
- column with alias: <b>time</b> for the annotation event time. Use epoch time or any native date data type.
- column with alias: <b>text</b> for the annotation text

View File

@@ -6,8 +6,12 @@ import * as dateMath from 'app/core/utils/datemath';
import PrometheusMetricFindQuery from './metric_find_query';
import { ResultTransformer } from './result_transformer';
function prometheusSpecialRegexEscape(value) {
return value.replace(/[\\^$*+?.()|[\]{}]/g, '\\\\$&');
export function prometheusRegularEscape(value) {
return value.replace(/'/g, "\\\\'");
}
export function prometheusSpecialRegexEscape(value) {
return prometheusRegularEscape(value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]+?.()]/g, '\\\\$&'));
}
export class PrometheusDatasource {
@@ -80,7 +84,7 @@ export class PrometheusDatasource {
interpolateQueryExpr(value, variable, defaultFormatFn) {
// if no multi or include all do not regexEscape
if (!variable.multi && !variable.includeAll) {
return value;
return prometheusRegularEscape(value);
}
if (typeof value === 'string') {

View File

@@ -14,7 +14,7 @@
data-min-length=0 data-items=1000 ng-model-onblur ng-change="ctrl.refreshMetricData()">
</input>
<info-popover mode="right-absolute">
Controls the name of the time series, using name or pattern. For example {{hostname}} will be replaced with label value for
Controls the name of the time series, using name or pattern. For example <span ng-non-bindable>{{hostname}}</span> will be replaced with label value for
the label hostname.
</info-popover>
</div>

View File

@@ -1,7 +1,7 @@
import _ from 'lodash';
import moment from 'moment';
import q from 'q';
import { PrometheusDatasource } from '../datasource';
import { PrometheusDatasource, prometheusSpecialRegexEscape, prometheusRegularEscape } from '../datasource';
describe('PrometheusDatasource', () => {
let ctx: any = {};
@@ -101,4 +101,41 @@ describe('PrometheusDatasource', () => {
});
});
});
describe('Prometheus regular escaping', function() {
it('should not escape simple string', function() {
expect(prometheusRegularEscape('cryptodepression')).toEqual('cryptodepression');
});
it("should escape '", function() {
expect(prometheusRegularEscape("looking'glass")).toEqual("looking\\\\'glass");
});
it('should escape multiple characters', function() {
expect(prometheusRegularEscape("'looking'glass'")).toEqual("\\\\'looking\\\\'glass\\\\'");
});
});
describe('Prometheus regexes escaping', function() {
it('should not escape simple string', function() {
expect(prometheusSpecialRegexEscape('cryptodepression')).toEqual('cryptodepression');
});
it('should escape $^*+?.()\\', function() {
expect(prometheusSpecialRegexEscape("looking'glass")).toEqual("looking\\\\'glass");
expect(prometheusSpecialRegexEscape('looking{glass')).toEqual('looking\\\\{glass');
expect(prometheusSpecialRegexEscape('looking}glass')).toEqual('looking\\\\}glass');
expect(prometheusSpecialRegexEscape('looking[glass')).toEqual('looking\\\\[glass');
expect(prometheusSpecialRegexEscape('looking]glass')).toEqual('looking\\\\]glass');
expect(prometheusSpecialRegexEscape('looking$glass')).toEqual('looking\\\\$glass');
expect(prometheusSpecialRegexEscape('looking^glass')).toEqual('looking\\\\^glass');
expect(prometheusSpecialRegexEscape('looking*glass')).toEqual('looking\\\\*glass');
expect(prometheusSpecialRegexEscape('looking+glass')).toEqual('looking\\\\+glass');
expect(prometheusSpecialRegexEscape('looking?glass')).toEqual('looking\\\\?glass');
expect(prometheusSpecialRegexEscape('looking.glass')).toEqual('looking\\\\.glass');
expect(prometheusSpecialRegexEscape('looking(glass')).toEqual('looking\\\\(glass');
expect(prometheusSpecialRegexEscape('looking)glass')).toEqual('looking\\\\)glass');
expect(prometheusSpecialRegexEscape('looking\\glass')).toEqual('looking\\\\\\\\glass');
});
it('should escape multiple special characters', function() {
expect(prometheusSpecialRegexEscape('+looking$glass?')).toEqual('\\\\+looking\\\\$glass\\\\?');
});
});
});

View File

@@ -1,17 +1,19 @@
<div class="dashlist" ng-repeat="group in ctrl.groups">
<div class="dashlist-section" ng-if="group.show">
<h6 class="dashlist-section-header" ng-show="ctrl.panel.headings">
{{group.header}}
</h6>
<div class="dashlist-item" ng-repeat="dash in group.list">
<a class="dashlist-link dashlist-link-{{dash.type}}" href="{{dash.url}}">
<span class="dashlist-title">
{{dash.title}}
</span>
<span class="dashlist-star" ng-click="ctrl.starDashboard(dash, $event)">
<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
</span>
</a>
<div>
<div class="dashlist" ng-repeat="group in ctrl.groups">
<div class="dashlist-section" ng-if="group.show">
<h6 class="dashlist-section-header" ng-show="ctrl.panel.headings">
{{group.header}}
</h6>
<div class="dashlist-item" ng-repeat="dash in group.list">
<a class="dashlist-link dashlist-link-{{dash.type}}" href="{{dash.url}}">
<span class="dashlist-title">
{{dash.title}}
</span>
<span class="dashlist-star" ng-click="ctrl.starDashboard(dash, $event)">
<i class="fa" ng-class="{'fa-star': dash.isStarred, 'fa-star-o': dash.isStarred === false}"></i>
</span>
</a>
</div>
</div>
</div>
</div>

View File

@@ -1,292 +0,0 @@
define([
'jquery',
'app/core/core',
],
function ($, core) {
'use strict';
var appEvents = core.appEvents;
function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
var self = this;
var ctrl = scope.ctrl;
var panel = ctrl.panel;
var $tooltip = $('<div class="graph-tooltip">');
this.destroy = function() {
$tooltip.remove();
};
this.findHoverIndexFromDataPoints = function(posX, series, last) {
var ps = series.datapoints.pointsize;
var initial = last*ps;
var len = series.datapoints.points.length;
for (var j = initial; j < len; j += ps) {
// Special case of a non stepped line, highlight the very last point just before a null point
if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
//normal case
|| series.datapoints.points[j] > posX) {
return Math.max(j - ps, 0)/ps;
}
}
return j/ps - 1;
};
this.findHoverIndexFromData = function(posX, series) {
var lower = 0;
var upper = series.data.length - 1;
var middle;
while (true) {
if (lower > upper) {
return Math.max(upper, 0);
}
middle = Math.floor((lower + upper) / 2);
if (series.data[middle][0] === posX) {
return middle;
} else if (series.data[middle][0] < posX) {
lower = middle + 1;
} else {
upper = middle - 1;
}
}
};
this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
if (xMode === 'time') {
innerHtml = '<div class="graph-tooltip-time">'+ absoluteTime + '</div>' + innerHtml;
}
$tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
};
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
var value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
var results = [[],[],[]];
//now we know the current X (j) position for X and Y values
var last_value = 0; //needed for stacked values
var minDistance, minTime;
for (i = 0; i < seriesList.length; i++) {
series = seriesList[i];
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
// Init value so that it does not brake series sorting
results[0].push({ hidden: true, value: 0 });
continue;
}
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
// Init value so that it does not brake series sorting
results[0].push({ hidden: true, value: 0 });
continue;
}
hoverIndex = this.findHoverIndexFromData(pos.x, series);
hoverDistance = pos.x - series.data[hoverIndex][0];
pointTime = series.data[hoverIndex][0];
// Take the closest point before the cursor, or if it does not exist, the closest after
if (! minDistance
|| (hoverDistance >=0 && (hoverDistance < minDistance || minDistance < 0))
|| (hoverDistance < 0 && hoverDistance > minDistance)) {
minDistance = hoverDistance;
minTime = pointTime;
}
if (series.stack) {
if (panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1];
} else if (!series.stack) {
value = series.data[hoverIndex][1];
} else {
last_value += series.data[hoverIndex][1];
value = last_value;
}
} else {
value = series.data[hoverIndex][1];
}
// Highlighting multiple Points depending on the plot type
if (series.lines.steps || series.stack) {
// stacked and steppedLine plots can have series with different length.
// Stacked series can increase its length on each new stacked serie if null points found,
// to speed the index search we begin always on the last found hoverIndex.
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
}
// Be sure we have a yaxis so that it does not brake series sorting
yaxis = 0;
if (series.yaxis) {
yaxis = series.yaxis.n;
}
results[yaxis].push({
value: value,
hoverIndex: hoverIndex,
color: series.color,
label: series.aliasEscaped,
time: pointTime,
distance: hoverDistance,
index: i
});
}
// Contat the 3 sub-arrays
results = results[0].concat(results[1],results[2]);
// Time of the point closer to pointer
results.time = minTime;
return results;
};
elem.mouseleave(function () {
if (panel.tooltip.shared) {
var plot = elem.data().plot;
if (plot) {
$tooltip.detach();
plot.unhighlight();
}
}
appEvents.emit('graph-hover-clear');
});
elem.bind("plothover", function (event, pos, item) {
self.show(pos, item);
// broadcast to other graph panels that we are hovering!
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
appEvents.emit('graph-hover', {pos: pos, panel: panel});
});
elem.bind("plotclick", function (event, pos, item) {
appEvents.emit('graph-click', {pos: pos, panel: panel, item: item});
});
this.clear = function(plot) {
$tooltip.detach();
plot.clearCrosshair();
plot.unhighlight();
};
this.show = function(pos, item) {
var plot = elem.data().plot;
var plotData = plot.getData();
var xAxes = plot.getXAxes();
var xMode = xAxes[0].options.mode;
var seriesList = getSeriesFn();
var allSeriesMode = panel.tooltip.shared;
var group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
// if panelRelY is defined another panel wants us to show a tooltip
// get pageX from position on x axis and pageY from relative position in original panel
if (pos.panelRelY) {
var pointOffset = plot.pointOffset({x: pos.x});
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
self.clear(plot);
return;
}
pos.pageX = elem.offset().left + pointOffset.left;
pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
var isVisible = pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
if (!isVisible) {
self.clear(plot);
return;
}
plot.setCrosshair(pos);
allSeriesMode = true;
if (dashboard.sharedCrosshairModeOnly()) {
// if only crosshair mode we are done
return;
}
}
if (seriesList.length === 0) {
return;
}
if (seriesList[0].hasMsResolution) {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
} else {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
}
if (allSeriesMode) {
plot.unhighlight();
var seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
seriesHtml = '';
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
// Dynamically reorder the hovercard for the current time point if the
// option is enabled.
if (panel.tooltip.sort === 2) {
seriesHoverInfo.sort(function(a, b) {
return b.value - a.value;
});
} else if (panel.tooltip.sort === 1) {
seriesHoverInfo.sort(function(a, b) {
return a.value - b.value;
});
}
for (i = 0; i < seriesHoverInfo.length; i++) {
hoverInfo = seriesHoverInfo[i];
if (hoverInfo.hidden) {
continue;
}
var highlightClass = '';
if (item && hoverInfo.index === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight';
}
series = seriesList[hoverInfo.index];
value = series.formatValue(hoverInfo.value);
seriesHtml += '<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
seriesHtml += '<i class="fa fa-minus" style="color:' + hoverInfo.color +';"></i> ' + hoverInfo.label + ':</div>';
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
}
self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
}
// single series tooltip
else if (item) {
series = seriesList[item.seriesIndex];
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
group += '<i class="fa fa-minus" style="color:' + item.series.color +';"></i> ' + series.aliasEscaped + ':</div>';
if (panel.stack && panel.tooltip.value_type === 'individual') {
value = item.datapoint[1] - item.datapoint[2];
}
else {
value = item.datapoint[1];
}
value = series.formatValue(value);
absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
group += '<div class="graph-tooltip-value">' + value + '</div>';
self.renderAndShow(absoluteTime, group, pos, xMode);
}
// no hit
else {
$tooltip.detach();
}
};
}
return GraphTooltip;
});

View File

@@ -0,0 +1,289 @@
import $ from 'jquery';
import { appEvents } from 'app/core/core';
export default function GraphTooltip(elem, dashboard, scope, getSeriesFn) {
let self = this;
let ctrl = scope.ctrl;
let panel = ctrl.panel;
let $tooltip = $('<div class="graph-tooltip">');
this.destroy = function() {
$tooltip.remove();
};
this.findHoverIndexFromDataPoints = function(posX, series, last) {
let ps = series.datapoints.pointsize;
let initial = last * ps;
let len = series.datapoints.points.length;
let j;
for (j = initial; j < len; j += ps) {
// Special case of a non stepped line, highlight the very last point just before a null point
if (
(!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null) ||
//normal case
series.datapoints.points[j] > posX
) {
return Math.max(j - ps, 0) / ps;
}
}
return j / ps - 1;
};
this.findHoverIndexFromData = function(posX, series) {
let lower = 0;
let upper = series.data.length - 1;
let middle;
while (true) {
if (lower > upper) {
return Math.max(upper, 0);
}
middle = Math.floor((lower + upper) / 2);
if (series.data[middle][0] === posX) {
return middle;
} else if (series.data[middle][0] < posX) {
lower = middle + 1;
} else {
upper = middle - 1;
}
}
};
this.renderAndShow = function(absoluteTime, innerHtml, pos, xMode) {
if (xMode === 'time') {
innerHtml = '<div class="graph-tooltip-time">' + absoluteTime + '</div>' + innerHtml;
}
$tooltip.html(innerHtml).place_tt(pos.pageX + 20, pos.pageY);
};
this.getMultiSeriesPlotHoverInfo = function(seriesList, pos) {
let value, i, series, hoverIndex, hoverDistance, pointTime, yaxis;
// 3 sub-arrays, 1st for hidden series, 2nd for left yaxis, 3rd for right yaxis.
let results: any = [[], [], []];
//now we know the current X (j) position for X and Y values
let last_value = 0; //needed for stacked values
let minDistance, minTime;
for (i = 0; i < seriesList.length; i++) {
series = seriesList[i];
if (!series.data.length || (panel.legend.hideEmpty && series.allIsNull)) {
// Init value so that it does not brake series sorting
results[0].push({ hidden: true, value: 0 });
continue;
}
if (!series.data.length || (panel.legend.hideZero && series.allIsZero)) {
// Init value so that it does not brake series sorting
results[0].push({ hidden: true, value: 0 });
continue;
}
hoverIndex = this.findHoverIndexFromData(pos.x, series);
hoverDistance = pos.x - series.data[hoverIndex][0];
pointTime = series.data[hoverIndex][0];
// Take the closest point before the cursor, or if it does not exist, the closest after
if (
!minDistance ||
(hoverDistance >= 0 && (hoverDistance < minDistance || minDistance < 0)) ||
(hoverDistance < 0 && hoverDistance > minDistance)
) {
minDistance = hoverDistance;
minTime = pointTime;
}
if (series.stack) {
if (panel.tooltip.value_type === 'individual') {
value = series.data[hoverIndex][1];
} else if (!series.stack) {
value = series.data[hoverIndex][1];
} else {
last_value += series.data[hoverIndex][1];
value = last_value;
}
} else {
value = series.data[hoverIndex][1];
}
// Highlighting multiple Points depending on the plot type
if (series.lines.steps || series.stack) {
// stacked and steppedLine plots can have series with different length.
// Stacked series can increase its length on each new stacked serie if null points found,
// to speed the index search we begin always on the last found hoverIndex.
hoverIndex = this.findHoverIndexFromDataPoints(pos.x, series, hoverIndex);
}
// Be sure we have a yaxis so that it does not brake series sorting
yaxis = 0;
if (series.yaxis) {
yaxis = series.yaxis.n;
}
results[yaxis].push({
value: value,
hoverIndex: hoverIndex,
color: series.color,
label: series.aliasEscaped,
time: pointTime,
distance: hoverDistance,
index: i,
});
}
// Contat the 3 sub-arrays
results = results[0].concat(results[1], results[2]);
// Time of the point closer to pointer
results.time = minTime;
return results;
};
elem.mouseleave(function() {
if (panel.tooltip.shared) {
let plot = elem.data().plot;
if (plot) {
$tooltip.detach();
plot.unhighlight();
}
}
appEvents.emit('graph-hover-clear');
});
elem.bind('plothover', function(event, pos, item) {
self.show(pos, item);
// broadcast to other graph panels that we are hovering!
pos.panelRelY = (pos.pageY - elem.offset().top) / elem.height();
appEvents.emit('graph-hover', { pos: pos, panel: panel });
});
elem.bind('plotclick', function(event, pos, item) {
appEvents.emit('graph-click', { pos: pos, panel: panel, item: item });
});
this.clear = function(plot) {
$tooltip.detach();
plot.clearCrosshair();
plot.unhighlight();
};
this.show = function(pos, item) {
let plot = elem.data().plot;
let plotData = plot.getData();
let xAxes = plot.getXAxes();
let xMode = xAxes[0].options.mode;
let seriesList = getSeriesFn();
let allSeriesMode = panel.tooltip.shared;
let group, value, absoluteTime, hoverInfo, i, series, seriesHtml, tooltipFormat;
// if panelRelY is defined another panel wants us to show a tooltip
// get pageX from position on x axis and pageY from relative position in original panel
if (pos.panelRelY) {
let pointOffset = plot.pointOffset({ x: pos.x });
if (Number.isNaN(pointOffset.left) || pointOffset.left < 0 || pointOffset.left > elem.width()) {
self.clear(plot);
return;
}
pos.pageX = elem.offset().left + pointOffset.left;
pos.pageY = elem.offset().top + elem.height() * pos.panelRelY;
let isVisible =
pos.pageY >= $(window).scrollTop() && pos.pageY <= $(window).innerHeight() + $(window).scrollTop();
if (!isVisible) {
self.clear(plot);
return;
}
plot.setCrosshair(pos);
allSeriesMode = true;
if (dashboard.sharedCrosshairModeOnly()) {
// if only crosshair mode we are done
return;
}
}
if (seriesList.length === 0) {
return;
}
if (seriesList[0].hasMsResolution) {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss.SSS';
} else {
tooltipFormat = 'YYYY-MM-DD HH:mm:ss';
}
if (allSeriesMode) {
plot.unhighlight();
let seriesHoverInfo = self.getMultiSeriesPlotHoverInfo(plotData, pos);
seriesHtml = '';
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
// Dynamically reorder the hovercard for the current time point if the
// option is enabled.
if (panel.tooltip.sort === 2) {
seriesHoverInfo.sort(function(a, b) {
return b.value - a.value;
});
} else if (panel.tooltip.sort === 1) {
seriesHoverInfo.sort(function(a, b) {
return a.value - b.value;
});
}
for (i = 0; i < seriesHoverInfo.length; i++) {
hoverInfo = seriesHoverInfo[i];
if (hoverInfo.hidden) {
continue;
}
let highlightClass = '';
if (item && hoverInfo.index === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight';
}
series = seriesList[hoverInfo.index];
value = series.formatValue(hoverInfo.value);
seriesHtml +=
'<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
seriesHtml +=
'<i class="fa fa-minus" style="color:' + hoverInfo.color + ';"></i> ' + hoverInfo.label + ':</div>';
seriesHtml += '<div class="graph-tooltip-value">' + value + '</div></div>';
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
}
self.renderAndShow(absoluteTime, seriesHtml, pos, xMode);
} else if (item) {
// single series tooltip
series = seriesList[item.seriesIndex];
group = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
group +=
'<i class="fa fa-minus" style="color:' + item.series.color + ';"></i> ' + series.aliasEscaped + ':</div>';
if (panel.stack && panel.tooltip.value_type === 'individual') {
value = item.datapoint[1] - item.datapoint[2];
} else {
value = item.datapoint[1];
}
value = series.formatValue(value);
absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat);
group += '<div class="graph-tooltip-value">' + value + '</div>';
self.renderAndShow(absoluteTime, group, pos, xMode);
} else {
// no hit
$tooltip.detach();
}
};
}

View File

@@ -52,14 +52,14 @@ function ($, _, angular, Drop) {
var eventManager = plot.getOptions().events.manager;
if (eventManager.editorOpen) {
// update marker element to attach to (needed in case of legend on the right
// when there is a double render pass and the inital marker element is removed)
// when there is a double render pass and the initial marker element is removed)
markerElementToAttachTo = element;
return;
}
// mark as openend
eventManager.editorOpened();
// set marker elment to attache to
// set marker element to attache to
markerElementToAttachTo = element;
// wait for element to be attached and positioned

View File

@@ -1,7 +1,7 @@
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
import PerfectScrollbar from 'perfect-scrollbar';
import baron from 'baron';
var module = angular.module('grafana.directives');
@@ -16,11 +16,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
var i;
var legendScrollbar;
const legendRightDefaultWidth = 10;
let legendElem = elem.parent();
scope.$on('$destroy', function() {
if (legendScrollbar) {
legendScrollbar.destroy();
}
destroyScrollbar();
});
ctrl.events.on('render-legend', () => {
@@ -112,7 +111,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
}
function render() {
let legendWidth = elem.width();
let legendWidth = legendElem.width();
if (!ctrl.panel.legend.show) {
elem.empty();
firstRender = true;
@@ -130,9 +129,12 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
elem.empty();
// Set min-width if side style and there is a value, otherwise remove the CSS propery
var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
elem.css('min-width', width);
// Set min-width if side style and there is a value, otherwise remove the CSS property
// Set width so it works with IE11
var width: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + 'px' : '';
var ieWidth: any = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth - 1 + 'px' : '';
legendElem.css('min-width', width);
legendElem.css('width', ieWidth);
elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true);
@@ -238,8 +240,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
tbodyElem.append(tableHeaderElem);
tbodyElem.append(seriesElements);
elem.append(tbodyElem);
tbodyElem.wrap('<div class="graph-legend-scroll"></div>');
} else {
elem.append(seriesElements);
elem.append('<div class="graph-legend-scroll"></div>');
elem.find('.graph-legend-scroll').append(seriesElements);
}
if (!panel.legend.rightSide || (panel.legend.rightSide && legendWidth !== legendRightDefaultWidth)) {
@@ -250,23 +254,45 @@ module.directive('graphLegend', function(popoverSrv, $timeout) {
}
function addScrollbar() {
const scrollbarOptions = {
// Number of pixels the content height can surpass the container height without enabling the scroll bar.
scrollYMarginOffset: 2,
suppressScrollX: true,
wheelPropagation: true,
const scrollRootClass = 'baron baron__root';
const scrollerClass = 'baron__scroller';
const scrollBarHTML = `
<div class="baron__track">
<div class="baron__bar"></div>
</div>
`;
let scrollRoot = elem;
let scroller = elem.find('.graph-legend-scroll');
// clear existing scroll bar track to prevent duplication
scrollRoot.find('.baron__track').remove();
scrollRoot.addClass(scrollRootClass);
$(scrollBarHTML).appendTo(scrollRoot);
scroller.addClass(scrollerClass);
let scrollbarParams = {
root: scrollRoot[0],
scroller: scroller[0],
bar: '.baron__bar',
track: '.baron__track',
barOnCls: '_scrollbar',
scrollingCls: '_scrolling',
};
if (!legendScrollbar) {
legendScrollbar = new PerfectScrollbar(elem[0], scrollbarOptions);
legendScrollbar = baron(scrollbarParams);
} else {
legendScrollbar.update();
destroyScrollbar();
legendScrollbar = baron(scrollbarParams);
}
legendScrollbar.scroll();
}
function destroyScrollbar() {
if (legendScrollbar) {
legendScrollbar.destroy();
legendScrollbar.dispose();
legendScrollbar = undefined;
}
}

View File

@@ -31,7 +31,7 @@ export class SeriesOverridesCtrl {
$scope.override[item.propertyName] = subItem.value;
// automatically disable lines for this series and the fill bellow to series
// automatically disable lines for this series and the fill below to series
// can be removed by the user if they still want lines
if (item.propertyName === 'fillBelowTo') {
$scope.override['lines'] = false;

View File

@@ -11,6 +11,7 @@ var scope = {
var elem = $('<div></div>');
var dashboard = {};
var getSeriesFn;
function describeSharedTooltip(desc, fn) {
var ctx: any = {};
@@ -30,7 +31,7 @@ function describeSharedTooltip(desc, fn) {
describe(desc, function() {
beforeEach(function() {
ctx.setupFn();
var tooltip = new GraphTooltip(elem, dashboard, scope);
var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
ctx.results = tooltip.getMultiSeriesPlotHoverInfo(ctx.data, ctx.pos);
});
@@ -39,7 +40,7 @@ function describeSharedTooltip(desc, fn) {
}
describe('findHoverIndexFromData', function() {
var tooltip = new GraphTooltip(elem, dashboard, scope);
var tooltip = new GraphTooltip(elem, dashboard, scope, getSeriesFn);
var series = {
data: [[100, 0], [101, 0], [102, 0], [103, 0], [104, 0], [105, 0], [106, 0], [107, 0]],
};

View File

@@ -3,7 +3,9 @@ var template = `
<div class="graph-panel__chart" grafana-graph ng-dblclick="ctrl.zoomOut()">
</div>
<div class="graph-legend" graph-legend></div>
<div class="graph-legend">
<div class="graph-legend-content" graph-legend></div>
</div>
</div>
`;

View File

@@ -163,10 +163,10 @@
<span>
Use special variables to specify cell values:
<br>
<em>$__cell</em> refers to current cell value
<em>${__cell}</em> refers to current cell value
<br>
<em>$__cell_n</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
<em>$__cell_1</em> refers to second column's value.
<em>${__cell_n}</em> refers to Nth column value in current row. Column indexes are started from 0. For instance,
<em>${__cell_1}</em> refers to second column's value.
</span>
</info-popover>
</div>

View File

@@ -221,7 +221,7 @@ describe('when transforming time series table', () => {
expect(table.rows[0][2]).toBe(42);
});
it('should return 2 rows for a mulitple queries with same label values plus one extra row', () => {
it('should return 2 rows for a multiple queries with same label values plus one extra row', () => {
table = transformDataToTable(multipleQueriesDataSameLabels, panel);
expect(table.rows.length).toBe(2);
expect(table.rows[0][0]).toBe(time);
@@ -238,7 +238,7 @@ describe('when transforming time series table', () => {
expect(table.rows[1][5]).toBe(7);
});
it('should return 2 rows for mulitple queries with different label values', () => {
it('should return 2 rows for multiple queries with different label values', () => {
table = transformDataToTable(multipleQueriesDataDifferentLabels, panel);
expect(table.rows.length).toBe(2);
expect(table.columns.length).toBe(6);

View File

@@ -243,7 +243,7 @@ transformers['table'] = {
row[columnIndex] = matchedRow[columnIndex];
}
}
// Dont visit this row again
// Don't visit this row again
mergedRows[match] = matchedRow;
// Keep looking for more rows to merge
offset = match + 1;

View File

@@ -22,7 +22,7 @@ var dashboard;
// All url parameters are available via the ARGS object
var ARGS;
// Intialize a skeleton with nothing but a rows array and service object
// Initialize a skeleton with nothing but a rows array and service object
dashboard = {
rows : [],
schemaVersion: 13,

View File

@@ -59,9 +59,8 @@ $critical: #ec2128;
$body-bg: $gray-7;
$page-bg: $gray-7;
$body-color: $gray-1;
//$text-color: $dark-4;
$text-color: $gray-1;
$text-color-strong: $white;
$text-color-strong: $dark-2;
$text-color-weak: $gray-2;
$text-color-faint: $gray-4;
$text-color-emphasis: $dark-5;

View File

@@ -1,5 +1,13 @@
.add-panel-container {
height: 100%;
}
.add-panel {
height: 100%;
.baron__root {
height: calc(100% - 43px);
}
}
.add-panel__header {
@@ -39,7 +47,6 @@
flex-direction: row;
flex-wrap: wrap;
overflow: auto;
height: calc(100% - 43px);
align-content: flex-start;
justify-content: space-around;
position: relative;

View File

@@ -49,6 +49,7 @@
}
.graph-legend {
display: flex;
flex: 0 1 auto;
max-height: 30%;
margin: 0;
@@ -56,11 +57,27 @@
padding-top: 6px;
position: relative;
// fix for Firefox (white stripe on the right of scrollbar)
width: calc(100% - 1px);
.popover-content {
padding: 0;
}
}
.graph-legend-content {
position: relative;
// fix for Firefox (white stripe on the right of scrollbar)
width: calc(100% - 1px);
}
.graph-legend-scroll {
position: relative;
overflow: auto !important;
padding: 1px;
}
.graph-legend-icon {
position: relative;
padding-right: 4px;
@@ -115,8 +132,20 @@
// fix for phantomjs
.body--phantomjs {
.graph-panel--legend-right {
.graph-legend {
display: inline-block;
}
.graph-panel__chart {
display: flex;
}
.graph-legend-table {
display: table;
.graph-legend-scroll {
display: table;
}
}
}
}
@@ -124,9 +153,9 @@
.graph-legend-table {
tbody {
display: block;
position: relative;
overflow-y: auto;
overflow-x: hidden;
height: 100%;
padding-bottom: 1px;
padding-right: 5px;
padding-left: 5px;

View File

@@ -9,6 +9,11 @@
-ms-touch-action: auto;
}
// ._scrollbar {
// overflow-x: hidden !important;
// overflow-y: auto;
// }
/*
* Scrollbar rail styles
*/
@@ -101,7 +106,7 @@
opacity: 0.9;
}
// Srollbars
// Scrollbars
//
::-webkit-scrollbar {
@@ -172,3 +177,120 @@
border-top: 1px solid $scrollbarBorder;
border-left: 1px solid $scrollbarBorder;
}
// Baron styles
.baron {
// display: inline-block; // this brakes phantomjs rendering (width becomes 0)
overflow: hidden;
}
// Fix for side menu on mobile devices
.main-view.baron {
width: unset;
}
.baron__clipper {
position: relative;
overflow: hidden;
}
.baron__scroller {
overflow-y: scroll;
-ms-overflow-style: none;
-moz-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
border: 0;
padding: 0;
width: 100%;
height: 100%;
-webkit-overflow-scrolling: touch;
/* remove line to customize scrollbar in iOs */
}
.baron__scroller::-webkit-scrollbar {
width: 0;
height: 0;
}
.baron__track {
display: none;
position: absolute;
top: 0;
right: 0;
bottom: 0;
}
.baron._scrollbar .baron__track {
display: block;
}
.baron__free {
position: absolute;
top: 0;
bottom: 0;
right: 0;
}
.baron__bar {
display: none;
position: absolute;
right: 0;
z-index: 1;
// width: 10px;
background: #999;
// height: 15px;
width: 15px;
transition: background-color 0.2s linear, opacity 0.2s linear;
opacity: 0;
}
.baron._scrollbar .baron__bar {
display: block;
@include gradient-vertical($scrollbarBackground, $scrollbarBackground2);
border-radius: 6px;
width: 6px;
/* there must be 'right' for ps__thumb-y */
right: 0px;
/* please don't change 'position' */
position: absolute;
// background-color: transparent;
// opacity: 0.6;
&:hover,
&:focus {
// background-color: transparent;
opacity: 0.9;
}
}
.panel-hover-highlight .baron__track .baron__bar {
opacity: 0.6;
}
.baron._scrolling > .baron__track .baron__bar {
opacity: 0.9;
}
// fix for phantomjs
.body--phantomjs .baron__track .baron__bar {
opacity: 0 !important;
}
.baron__control {
display: none;
}
.baron.panel-content--scrollable {
// Width needs to be set to prevent content width issues
// Set to less than 100% for fixing Firefox issue (white stripe on the right of scrollbar)
width: calc(100% - 2px);
.baron__scroller {
padding-top: 1px;
}
}

View File

@@ -31,7 +31,6 @@
//padding: 0.5rem 1.5rem 0.5rem 0;
padding: 1rem 1rem 0.75rem 1rem;
height: 51px;
line-height: 51px;
box-sizing: border-box;
outline: none;
background: $side-menu-bg;
@@ -61,6 +60,10 @@
display: flex;
flex-direction: column;
flex-grow: 1;
.search-item--indent {
margin-left: 14px;
}
}
.search-dropdown__col_2 {
@@ -99,14 +102,21 @@
}
}
.search-results-scroller {
display: flex;
position: relative;
}
.search-results-container {
height: 100%;
display: block;
padding: $spacer;
position: relative;
flex-grow: 10;
margin-bottom: 1rem;
// Fix for search scroller in mobile view
height: unset;
.label-tag {
margin-left: 6px;
font-size: 11px;

View File

@@ -123,6 +123,8 @@
position: relative;
opacity: 0.7;
font-size: 130%;
height: 22px;
width: 22px;
}
.fa {
@@ -147,6 +149,15 @@
color: #ebedf2;
}
.sidemenu-subtitle {
padding: 0.5rem 1rem 0.5rem;
font-size: $font-size-sm;
color: $text-color-weak;
border-bottom: 1px solid $dropdownDividerBottom;
margin-bottom: 0.25rem;
white-space: nowrap;
}
li.sidemenu-org-switcher {
border-bottom: 1px solid $dropdownDividerBottom;
}
@@ -178,6 +189,7 @@ li.sidemenu-org-switcher {
padding: 0.4rem 1rem 0.4rem 0.65rem;
min-height: $navbarHeight;
position: relative;
height: $navbarHeight - 1px;
&:hover {
background: $navbarButtonBackgroundHighlight;

View File

@@ -43,7 +43,7 @@
font-size: 120%;
}
&:hover {
color: $white;
color: $text-color-strong;
}
}

View File

@@ -28,12 +28,20 @@
width: 100%;
overflow: auto;
height: 100%;
-webkit-overflow-scrolling: touch;
&--dashboard {
height: calc(100% - 56px);
}
}
// fix for phantomjs
.body--phantomjs {
.scroll-canvas {
overflow: hidden;
}
}
.page-body {
padding-top: $spacer*2;
min-height: 500px;

View File

@@ -108,7 +108,8 @@
justify-content: center;
align-items: center;
width: 40px;
padding: 0 28px 0 16px;
//margin-right: 8px;
padding: 0 4px 0 2px;
.icon-gf,
.fa {
font-size: 200%;

Some files were not shown because too many files have changed in this diff Show More