mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into dashboard-acl-ux2
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
41
public/app/core/components/scroll/page_scroll.ts
Normal file
41
public/app/core/components/scroll/page_scroll.ts
Normal 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);
|
||||
@@ -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();
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
31
public/app/core/directives/dash_class.ts
Normal file
31
public/app/core/directives/dash_class.ts
Normal 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);
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
244
public/app/core/directives/dropdown_typeahead.ts
Normal file
244
public/app/core/directives/dropdown_typeahead.ts
Normal 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);
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
263
public/app/core/directives/metric_segment.ts
Normal file
263
public/app/core/directives/metric_segment.ts
Normal 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);
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
305
public/app/core/directives/value_select_dropdown.ts
Normal file
305
public/app/core/directives/value_select_dropdown.ts
Normal 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);
|
||||
@@ -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 () {});
|
||||
10
public/app/core/services/all.ts
Normal file
10
public/app/core/services/all.ts
Normal 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';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
111
public/app/core/services/segment_srv.ts
Normal file
111
public/app/core/services/segment_srv.ts
Normal 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);
|
||||
@@ -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 '';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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 () {});
|
||||
13
public/app/features/all.ts
Normal file
13
public/app/features/all.ts
Normal 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';
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
define([
|
||||
'./panel_header',
|
||||
'./panel_directive',
|
||||
'./solo_panel_ctrl',
|
||||
'./query_ctrl',
|
||||
'./panel_editor_tab',
|
||||
'./query_editor_row',
|
||||
'./query_troubleshooter',
|
||||
], function () {});
|
||||
7
public/app/features/panel/all.ts
Normal file
7
public/app/features/panel/all.ts
Normal 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';
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
define([
|
||||
'./playlists_ctrl',
|
||||
'./playlist_search',
|
||||
'./playlist_srv',
|
||||
'./playlist_edit_ctrl',
|
||||
'./playlist_routes'
|
||||
], function () {});
|
||||
5
public/app/features/playlist/all.ts
Normal file
5
public/app/features/playlist/all.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import './playlists_ctrl';
|
||||
import './playlist_search';
|
||||
import './playlist_srv';
|
||||
import './playlist_edit_ctrl';
|
||||
import './playlist_routes';
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
34
public/app/features/playlist/playlist_routes.ts
Normal file
34
public/app/features/playlist/playlist_routes.ts
Normal 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);
|
||||
@@ -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
|
||||
<i class="fa fa-caret-down" ng-show="ctrl.showAccessHelp"></i>
|
||||
<i class="fa fa-caret-right" ng-hide="ctrl.showAccessHelp"> </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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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' }];
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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' } }]);
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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([]));
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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('');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 >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: >60s if you write metrics to InfluxDB every 60 seconds
|
||||
- Example: 60s if you write metrics to InfluxDB every 60 seconds
|
||||
|
||||
#### Documentation links:
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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) -> column AS time
|
||||
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
||||
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
|
||||
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column <= DATEADD(s, 18446744066914187038, '1970-01-01')
|
||||
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
||||
|
||||
Or build your own conditionals using these macros which just return the values:
|
||||
|
||||
@@ -49,7 +49,7 @@ Table:
|
||||
Macros:
|
||||
- $__time(column) -> column AS time
|
||||
- $__timeEpoch(column) -> DATEDIFF(second, '1970-01-01', column) AS time
|
||||
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column &t;= DATEADD(s, 18446744066914187038, '1970-01-01')
|
||||
- $__timeFilter(column) -> column >= DATEADD(s, 18446744066914186738, '1970-01-01') AND column <= DATEADD(s, 18446744066914187038, '1970-01-01')
|
||||
- $__unixEpochFilter(column) -> column >= 1492750877 AND column <= 1492750877
|
||||
- $__timeGroup(column, '5m'[, fillvalue]) -> 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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
@@ -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
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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\\\\?');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
289
public/app/plugins/panel/graph/graph_tooltip.ts
Normal file
289
public/app/plugins/panel/graph/graph_tooltip.ts
Normal 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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]],
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
font-size: 120%;
|
||||
}
|
||||
&:hover {
|
||||
color: $white;
|
||||
color: $text-color-strong;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user